This repository was archived by the owner on Sep 23, 2026. It is now read-only.
Repository navigation
feat(session): expose runtime identity (pid + session id) for external observers #2082
Open
yeelam-gordon
wants to merge
20
commits into
MoonshotAI:main
Choose a base branch
from
yeelam-gordon:feat/runtime-session-identity
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 12 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
6b3c5ea
feat(session): expose runtime identity for external observers
1eda1ec
fix(session): plug runtime.json/proctitle gaps and harden write
Copilot eab5009
fix(proctitle): sanitize cwd token so spaces don't break split parsing
a034dbb
fix(proctitle): sanitize session id token alongside cwd
bd60edf
feat(runtime-status): add PID-keyed sidecar for O(1) lookup
8fad29a
fix(runtime-status): real prune_stale_pid_index + Windows-safe livene…
ed2a345
revert(runtime-status): drop pid-keyed mirror and prune sweep
05a8b99
fix(runtime-status): harden read_runtime_status against bad inputs
8aef4b7
refactor(runtime-status): drop cleanup-on-quit; consumers verify pid …
e7d8e42
fix(runtime-status): exclude bool from int type guards
29584ac
fix(runtime-status): clear runtime.json on /web and /vis switches
22df1db
refactor(2082): drop OS process title work; PR scope is runtime.json …
0f79e34
fix(runtime-status): clear runtime.json on cross-session Reload
cacce88
refactor(runtime-status): keep runtime.json on /web and /vis switches
51142de
style(runtime-status): trim verbose comments around exit paths
9591558
fix(runtime-status): guard cross-session-Reload clear by owning PID
5818f1e
revert: drop concurrency guard for cross-session-Reload clear
e237951
fix(runtime-status): write only from shell mode
4d495e0
chore: remove accidentally-committed commit message file
7083975
revert: write runtime.json for every UI mode that establishes a session
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,202 @@ | ||
| """Runtime status file for an active interactive Kimi session. | ||
|
|
||
| This module writes a small JSON file at ``<session_dir>/runtime.json`` while | ||
| an interactive Kimi session is alive. It exists so that **external** tools | ||
| (terminal multiplexers, tab managers, IDE integrations, observability | ||
| daemons) can answer the question: | ||
|
|
||
| "Which Kimi session is the running PID X serving?" | ||
|
|
||
| The CLI does not embed the session id in ``argv`` for fresh (non-resumed) | ||
| sessions, and the OS process title can be truncated, so a side-channel | ||
| file that records the explicit ``(pid, session_id, work_dir, ...)`` tuple | ||
| is the most reliable cross-platform signal. | ||
|
|
||
| Lifecycle: | ||
|
|
||
| * Written on session start (clean launch or ``--resume``); the resume case | ||
| atomically overwrites whatever the previous PID wrote. | ||
| * Not deleted on clean ``/quit`` or on crash. From an external observer's | ||
| standpoint the recorded PID no longer exists in either case, so a | ||
| liveness check is sufficient and an explicit cleanup adds nothing. | ||
| * **Is** deleted on ``/web`` and ``/vis`` mode switches via | ||
| :func:`clear_runtime_status`, because the same PID keeps running while | ||
| no longer serving the recorded session — without cleanup the file would | ||
| pass a naive liveness check and falsely claim the session is live. | ||
| * Removed naturally when the surrounding session directory itself is | ||
| deleted via ``/delete``, which gives a natural bound on accumulation. | ||
|
|
||
| The file is written atomically via :func:`atomic_json_write` and contains | ||
| a small, stable schema. External consumers should treat unknown fields as | ||
| forward-compatible additions. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| import os | ||
| import socket | ||
| import time | ||
| from dataclasses import asdict, dataclass | ||
| from pathlib import Path | ||
| from typing import cast | ||
|
|
||
| from kimi_cli.utils.io import atomic_json_write | ||
| from kimi_cli.utils.logging import logger | ||
|
|
||
| RUNTIME_STATUS_FILENAME = "runtime.json" | ||
| RUNTIME_STATUS_SCHEMA_VERSION = 1 | ||
|
|
||
|
|
||
| @dataclass(slots=True, kw_only=True) | ||
| class RuntimeStatus: | ||
| """Snapshot of a live Kimi session process for external observers.""" | ||
|
|
||
| schema_version: int | ||
| pid: int | ||
| session_id: str | ||
| work_dir: str | ||
| hostname: str | ||
| started_at: float | ||
| kimi_version: str | None | ||
|
|
||
|
|
||
| def _runtime_status_path(session_dir: Path) -> Path: | ||
| return session_dir / RUNTIME_STATUS_FILENAME | ||
|
|
||
|
|
||
| def _safe_kimi_version() -> str | None: | ||
| try: | ||
| from kimi_cli.constant import get_version | ||
|
|
||
| return get_version() | ||
| except Exception: | ||
| return None | ||
|
|
||
|
|
||
| def write_runtime_status( | ||
| session_dir: Path, | ||
| *, | ||
| session_id: str, | ||
| work_dir: str, | ||
| pid: int | None = None, | ||
| ) -> Path: | ||
| """Atomically write the runtime status file for this session. | ||
|
|
||
| Writes ``<session_dir>/runtime.json`` via tmp-file + ``os.replace`` so | ||
| an external observer never sees a partially written file: it sees | ||
| either the previous contents or the fully committed new contents. | ||
|
|
||
| Returns the path of the file that was written. Exceptions from the | ||
| underlying I/O (``OSError``, ``PermissionError``, read-only | ||
| filesystem, etc.) propagate to the caller; this function does not log | ||
| or swallow them. Callers that want best-effort semantics should wrap | ||
| the call in ``contextlib.suppress(OSError)``. On failure no leftover | ||
| ``.tmp`` file is kept on disk. | ||
| """ | ||
| status = RuntimeStatus( | ||
| schema_version=RUNTIME_STATUS_SCHEMA_VERSION, | ||
| pid=pid if pid is not None else os.getpid(), | ||
| session_id=session_id, | ||
| work_dir=work_dir, | ||
| hostname=socket.gethostname(), | ||
| started_at=time.time(), | ||
| kimi_version=_safe_kimi_version(), | ||
| ) | ||
| target = _runtime_status_path(session_dir) | ||
| atomic_json_write(asdict(status), target) | ||
| return target | ||
|
|
||
|
|
||
| def clear_runtime_status(session_dir: Path) -> None: | ||
| """Remove ``<session_dir>/runtime.json`` if present. | ||
|
|
||
| Intentionally **not** called on ``/quit`` — when the kimi process | ||
| exits, an external observer's PID-liveness check already detects | ||
| the missing process, so a stale record is harmless. This helper | ||
| exists for the narrow case where the **same PID continues running** | ||
| but stops serving the recorded session: notably the ``/web`` and | ||
| ``/vis`` mode switches in ``cli/__init__.py``. Without this call, | ||
| ``runtime.json`` would still claim a live PID-to-session mapping | ||
| that the process no longer honours, breaking the documented | ||
| observability contract. | ||
|
|
||
| Safe to call multiple times and on directories that no longer | ||
| exist; ``OSError`` is swallowed so cleanup cannot disrupt the | ||
| surrounding control flow. | ||
| """ | ||
| target = _runtime_status_path(session_dir) | ||
| try: | ||
| target.unlink(missing_ok=True) | ||
| except OSError: | ||
| logger.debug("Failed to remove runtime status file {file}", file=target) | ||
|
|
||
|
|
||
| def read_runtime_status(session_dir: Path) -> RuntimeStatus | None: | ||
| """Read the runtime status file from a session directory, if present. | ||
|
|
||
| Returns ``None`` if the file is missing, malformed, or written by a | ||
| schema version this code does not understand. "Malformed" includes | ||
| truncated UTF-8, syntactically invalid JSON, a non-object payload, | ||
| and any field whose JSON type does not match the dataclass — the | ||
| function never coerces ``None`` / list / dict into a string just to | ||
| satisfy the constructor. | ||
|
|
||
| Note: a returned record only proves that *some* Kimi process once | ||
| claimed this session. The PID may already be dead (clean quit or | ||
| crash). Consumers must verify liveness themselves before treating | ||
| the record as a currently-running session. | ||
| """ | ||
| target = _runtime_status_path(session_dir) | ||
| try: | ||
| raw = target.read_text(encoding="utf-8") | ||
| except FileNotFoundError: | ||
| return None | ||
|
yeelam-gordon marked this conversation as resolved.
|
||
| except (OSError, UnicodeDecodeError): | ||
| logger.debug("Failed to read runtime status file {file}", file=target) | ||
| return None | ||
|
|
||
| try: | ||
| data = json.loads(raw) | ||
| except json.JSONDecodeError: | ||
| logger.debug("Malformed runtime status file {file}", file=target) | ||
| return None | ||
|
|
||
| if not isinstance(data, dict): | ||
| return None | ||
| parsed = cast(dict[str, object], data) | ||
| if parsed.get("schema_version") != RUNTIME_STATUS_SCHEMA_VERSION: | ||
| return None | ||
|
|
||
| schema_version = parsed.get("schema_version") | ||
| pid = parsed.get("pid") | ||
| session_id = parsed.get("session_id") | ||
| work_dir = parsed.get("work_dir") | ||
| hostname = parsed.get("hostname") | ||
| started_at = parsed.get("started_at") | ||
| kimi_version = parsed.get("kimi_version") | ||
|
|
||
| if not isinstance(schema_version, int) or isinstance(schema_version, bool): | ||
| return None | ||
| if not isinstance(pid, int) or isinstance(pid, bool): | ||
| return None | ||
| if not isinstance(session_id, str): | ||
| return None | ||
| if not isinstance(work_dir, str): | ||
| return None | ||
| if not isinstance(hostname, str): | ||
| return None | ||
| if not isinstance(started_at, (int, float)) or isinstance(started_at, bool): | ||
| return None | ||
| if kimi_version is not None and not isinstance(kimi_version, str): | ||
| return None | ||
|
|
||
| return RuntimeStatus( | ||
| schema_version=schema_version, | ||
| pid=pid, | ||
| session_id=session_id, | ||
| work_dir=work_dir, | ||
| hostname=hostname, | ||
| started_at=float(started_at), | ||
| kimi_version=kimi_version, | ||
| ) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.