feat(session): expose runtime identity for external observers - #26149
feat(session): expose runtime identity for external observers#26149yeelam-gordon wants to merge 5 commits into
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request enhances the Gemini CLI by providing a standardized way for external applications to identify and monitor active CLI sessions. It achieves this by writing a small, self-describing JSON file at the start of each session, containing crucial runtime information. This enables better integration with third-party tools like terminal multiplexers and IDEs, allowing them to correlate running processes with specific Gemini CLI sessions. The implementation focuses on reliability, atomic operations, and minimal performance impact, ensuring that the CLI remains robust while offering increased observability. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a runtime status sidecar mechanism to allow external tools to observe active Gemini CLI sessions. It adds utility functions for writing and reading a runtime.json file containing session metadata—such as PID, session ID, and work directory—and integrates these into the CLI startup process. Feedback was provided regarding the use of synchronous file system operations within the asynchronous writeRuntimeStatus function, which could block the event loop during startup.
|
Update: pushed a follow-up commit (
Local verification: lint clean, typecheck clean, all 19 tests pass. |
|
Pushed a follow-up commit ( The bug being fixedWhen the same PID switches to serving a different session id mid-flight, the previous session's Gemini-cli paths that perform an in-process session switch
Both paths now do the same four-step dance: capture old session dir → switch id → Verification
|
|
Refactored to mirror the cleaner shape from the parallel qwen-code port (PR #3714) — pushed What changedThe same-PID session swap dance (
Why the ownership flagWithout the flag, a non-interactive entry point (e.g. Verification (all green)
Net effect on PR #26149PR is now 4 commits — the third ( |
|
/azp run |
Write a small JSON status file at <session-temp>/runtime.json while a session is alive that records (pid, sessionId, workDir, hostname, startedAt, geminiCliVersion). External tools (terminal multiplexers, tab managers, IDE integrations, observability daemons) can now reliably map a running PID to its session and answer "is there a gemini-cli process running against session X?" by reading the sidecar and verifying the PID is alive. The on-disk format uses snake_case keys to remain compatible with the runtime.json format already produced by sibling agentic CLIs (e.g. kimi-cli), so external observers can share parsing across tools. Lifecycle: - Written on session start (clean launch and resume); the resume case atomically overwrites the previous PID via tmp-file + fsync + rename. - NOT cleared on quit or crash. From an external observer's standpoint the recorded PID no longer exists in either case, so a liveness check is sufficient and explicit cleanup adds nothing on disk. - Removed naturally when the surrounding session directory is deleted, giving a natural bound on accumulation. The write is best-effort and wrapped in try/catch with debugLogger; a failure never blocks startup. The hook point is after the --list-extensions / --list-sessions / --delete-session early exits and before all three session-serving paths (interactive, ACP, non-interactive), so utility commands do not leave a sidecar claiming a session that never started. Includes 18 unit tests covering field round-trip, atomicity, missing / malformed / wrong-schema / wrong-types (incl. bool pid, non-integer pid, array work_dir, top-level array/null payloads), non-ASCII (Chinese) UTF-8 round-trip with on-disk byte assertion, invalid UTF-8 bytes returning null, atomic overwrite on resume, and CLI version field population.
Two reliability follow-ups mirroring the kimi-cli PR google-gemini#2082 review: - writeRuntimeStatus: switch the temp-file open from 'w' to 'wx' so the open succeeds only when the path does not already exist. The random UUID suffix already makes collision astronomically unlikely; the exclusive open (O_CREAT | O_EXCL) adds defense-in-depth against a pre-placed regular file or symlink at the temp path that 'w' would silently follow and overwrite. - tests: add a regression test asserting that no .tmp.* file is left behind when the underlying rename raises. We force the failure by pre-placing a non-empty directory at the target path, which makes fs.renameSync fail on every supported platform and exercises the catch-block tmp cleanup path.
…witch Mirrors kimi-cli PR google-gemini#2082 commit 0f79e348. When the same PID switches to serving a different session id mid-flight, the previous session's runtime.json must be dropped before the new session's record is written; otherwise an external observer running a PID-liveness check would see this PID mapped to BOTH sessions and treat both as live. Gemini CLI has two such in-process session switches: - '/clear' (clearCommand) -> resetNewSessionState(<freshUUID>) - session-browser resume (useSessionBrowser) -> setSessionId(<resumedId>) Both paths now: 1. Capture the OLD session dir before changing the session id. 2. Switch the session id (resetNewSessionState / setSessionId). 3. clearRuntimeStatus(oldSessionDir) to drop the stale claim. 4. writeRuntimeStatus(newSessionDir, ...) so the PID is observable under the new session id from this point on. All four steps are best-effort; the runtime-status I/O is wrapped in try/catch so a write failure on slow/network/read-only filesystems never blocks the user-visible operation. Adds clearRuntimeStatus() to the runtime-status module: synchronous, swallows ENOENT/ENOTDIR (idempotent), all other I/O errors silenced. Three new tests: removes existing file, idempotent on missing file, no-op on missing dir.
…onId Mirror the cleaner shape from the parallel qwen-code port (PR google-gemini#3714). Move the runtime.json clear+write dance out of the two callers and into Config.setSessionId itself, gated on a new ownership flag that the interactive UI bootstrap flips after the first successful write. Why --- Previous shape (3 commits ago) put the four-step swap dance at every caller of resetNewSessionState / setSessionId: 1. capture old session dir 2. switch session id 3. clearRuntimeStatus(old) 4. writeRuntimeStatus(new, ...) That pattern is correct but fragile: a future caller of setSessionId that forgets the dance will silently leave a stale runtime.json claiming this PID for the previous session id. Centralized shape ----------------- - Config gains a private runtimeStatusEnabled flag (default false) and a public markRuntimeStatusEnabled() method. - Config.setSessionId now captures the outgoing session dir before storage updates, then — only when the flag is set AND the session id actually changed — drops the previous session's sidecar and writes a fresh one for the incoming session as fire-and-forget best-effort. - gemini.tsx flips the flag immediately after the initial sidecar write succeeds, so all subsequent in-process session swaps are covered by the centralized dance. - clearCommand and useSessionBrowser revert to their original shape; no per-callsite runtime-status code remains. Why the ownership flag ---------------------- Without it, a non-interactive entry point (e.g. gemini --prompt --resume <id>) that calls setSessionId — or a future code path that does — would clobber a concurrent shell's runtime.json sharing the outgoing session id. The flag scopes the swap-time refresh to the process that genuinely owns a sidecar, mirroring qwen-code's fix (commit e1e8d29b) and the spirit of kimi-cli's later-reverted e237951f shell-mode-only guard. Tests ----- Adds one config.test.ts case verifying: - Before markRuntimeStatusEnabled, setSessionId leaves a pre-placed runtime.json untouched. - After markRuntimeStatusEnabled, a same-PID swap clears the outgoing sidecar and writes the incoming one (vi.waitFor for the fire-and- forget write). - Setting the SAME session id twice is a no-op (idempotent: no clear, no rewrite). All 22 runtime-status tests, 33 storage tests, 232 config tests, 2 clearCommand tests, and 9 useSessionBrowser tests pass.
Address gemini-code-assist review feedback: writeRuntimeStatus is
declared async but used synchronous fs APIs (mkdirSync, openSync,
writeSync, fsyncSync, closeSync, renameSync, unlinkSync). On the
session-startup hot path, those calls block the Node.js event loop
and contradict the function's async signature.
Switch the entire write pipeline to fs.promises.* equivalents:
- fs.promises.mkdir({recursive:true})
- fs.promises.open(tempPath, 'wx', 0o600) -> FileHandle
- handle.write / handle.sync / handle.close
- fs.promises.rename
- fs.promises.unlink for temp cleanup on failure
The 'wx' flag (O_CREAT | O_EXCL) and the fsync-before-rename
guarantee carry over unchanged. The catch block now awaits both
handle.close (if still open) and the temp unlink so failed writes
leave no leftover.
Tests unchanged: all 22 runtimeStatus.test.ts cases pass against the
async implementation, including the rename-failure leftover-cleanup
case (engineered via a pre-placed non-empty directory at the target).
66d5a4b to
fd72516
Compare
|
Hi there! Thank you for your interest in contributing to Gemini CLI. To ensure we maintain high code quality and focus on our prioritized roadmap, we only guarantee review and consideration of pull requests for issues that are explicitly labeled as 'help wanted'. This PR will be closed in 7 days if it remains without that designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding. |
|
This pull request is being closed as it has been open for 14 days without a 'help wanted' designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding. |
Summary
Adds a small JSON runtime-identity sidecar at
<session-temp>/runtime.jsonwhile a Gemini CLI session is alive, so external tools can answer:The file records
(pid, sessionId, workDir, hostname, startedAt, geminiCliVersion). Consumers parse the file and verify the recorded PID is alive (OS-specific). It is the most reliable cross-platform signal — argv does not embed the session id for fresh (non---resume) launches, and OS process titles get truncated.Use cases:
Prior art
Peer interactive agent CLIs already expose a per-session on-disk surface that external tooling has come to rely on, so the ecosystem expects it:
~/.copilot/session-state/<sessionId>/, and the running shell session is discoverable per session id from that layout.~/.claude/projects/<encodedCwd>/<sessionId>.jsonl, with the running process discoverable per project.~/.codex/sessions/.../rollout-*.jsonl) whose header records the session id for the active process.This PR gives gemini-cli an equivalent lightweight surface — a single JSON file per session — without adopting any one of those formats verbatim. Snake_case keys keep the file format language-agnostic and easy to consume from external tooling written in any language.
Sidecar contract
Path:
<global-gemini-temp>/<projectShortId>/<sessionId>/runtime.json(parallels the existing per-session subdirsplans/,tracker/,tasks/).JSON shape (snake_case for cross-tool consumption):
{ "schema_version": 1, "pid": 12345, "session_id": "uuid", "work_dir": "/path/to/workspace", "hostname": "machine", "started_at": 1714329600.123, "gemini_cli_version": "0.40.0" }External consumers:
~/.gemini/tmp/*/*/runtime.json.pid.Lifecycle
--resume). Resume case is an atomic overwrite via tmp +fsync+rename./clear, session browser resume) —Config.setSessionIdclears the outgoing sidecar and writes a fresh one for the incoming session, gated on aruntimeStatusEnabledownership flag set by the interactive UI bootstrap so non-interactive paths can't trample a sibling shell's sidecar.Hook point
After the
--list-extensions/--list-sessions/--delete-sessionearly-exit branches and before all three session-serving paths (interactive, ACP, non-interactive). Utility commands don't drop a false sidecar.The write is best-effort, wrapped in
try/catchwithdebugLogger.debug. A failure never blocks startup.Files touched
packages/core/src/utils/runtimeStatus.ts—writeRuntimeStatus,readRuntimeStatus,clearRuntimeStatus.packages/core/src/utils/runtimeStatus.test.ts— 22 unit tests (vitest,fs.mkdtemp).packages/core/src/config/storage.ts— addedgetSessionTempDir()andgetSessionRuntimeStatusPath().packages/core/src/config/config.ts— addedruntimeStatusEnabledownership flag,markRuntimeStatusEnabled(), swap-time refresh insidesetSessionId.packages/core/src/index.ts— exported the new module.packages/cli/src/gemini.tsx— wired the initial write and ownership-flag flip.Test coverage (all 22 passing)
.tmpleftover)nullon missing / malformed JSON / unknown schemanull(strictTextDecoder)nullsession_id, stringpid, non-integerpid, arraywork_dir, boolpid, top-level array, top-levelnullgemini_cli_versionpopulated fromCLI_VERSIONclearRuntimeStatusremoves existing file, idempotent on missing file, no-op on missing dirPlus a config integration test covering the same-PID swap behaviour: pre-flag no-op, post-flag clear+write, idempotent same-id call.
Risk review
plans/,tracker/,tasks/. Cleanup uses recursivermso the extra file does not break deletion.listProjectChatFilesandSessionSelectorboth walkchats/and are unaffected. No circular-import risk (runtimeStatusonly importsversion).gemini.test.tsxreproduce onmainand are unrelated.writeRuntimeStatususes fully asyncfs.promises.*(no event-loop blocking). Onefsync+renameper session start, on the startup hot path but only once and best-effort.0o600(owner-rw, matches existingtrust.tsand OAuth credential storage). Temp file uses'wx'(O_CREAT | O_EXCL) for defense-in-depth against pre-placed symlinks. Contents (pid,sessionId,workDir,hostname) are no more sensitive than what already lives in~/.gemini/projects.json.sessionIdoriginates fromrandomUUID()and is sanitized in session-artifact paths.Verification
npm run typecheck -w @google/gemini-cli-core✅npm run typecheck -w @google/gemini-cli✅npx eslinton touched files ✅npm test -w @google/gemini-cli-core -- src/utils/runtimeStatus.test.ts→ 22/22 ✅npm test -w @google/gemini-cli-core -- src/config/storage.test.ts✅npm test -w @google/gemini-cli-core -- src/config/config.test.ts✅ (incl. new swap-behaviour test)npm test -w @google/gemini-cli -- src/ui/commands/clearCommand.test.ts src/ui/hooks/useSessionBrowser.test.ts✅npm run build -w @google/gemini-cli✅