feat: resume/fork sessions by UUID or thread name#22
Merged
aannoo merged 12 commits intoaannoo:mainfrom Apr 18, 2026
Merged
Conversation
Extends `hcom r <name>` and `hcom f <name>` to accept:
1. A session UUID (e.g. `hcom r 521cfc2b-be38-403a-b32e-4a49c9551b27`)
— searches Claude, Codex, and Gemini transcript directories on
disk, extracts the last working directory from the transcript,
and relaunches with hcom wrapping.
2. A Claude Code thread name (e.g. `hcom r skills-work`) — scans
~/.claude/projects/*/*.jsonl for `{"type":"custom-title"}` entries
(created by Claude Code's /rename command), picks the most recent
match by file mtime.
3. A Codex thread name (e.g. `hcom r stabilization-review`) — looks
up ~/.codex/session_index.jsonl for a matching `thread_name`, picks
the most recent match by `updated_at`.
Resolution order: hcom instance → UUID → Claude thread → Codex thread.
Fork (`hcom f`) gets the same resolution for free. An active-instance
guard prevents resuming a session that is already running under another
hcom instance.
Makes `claude_config_dir` and `detect_agent_type` `pub(crate)` so they
can be reused from resume.rs (was the old pattern — duplication avoided).
5 new tests covering UUID detection and CWD extraction from Claude and
Codex transcripts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the parallel do_resume_by_session_id path with a single ResumeSource enum fed into prepare_resume_plan_from_source, so fork/resume/identity logic stays in one place. - Removed thread-name resolvers (Claude and Codex resolve titles natively via their own --resume flags) - Added opencode adoption: queries SQLite DB for session directory - CWD recovery reads first transcript line only (O(1) vs full walk) - Added Gemini CWD recovery via sha256(cwd) reverse lookup in projects.json (was incorrectly assumed impossible) - Adoption uses system_prompt: None — SessionStart hook handles bootstrap (no prior hcom identity in transcript to override) - Fixed exit code propagation in adoption path - Fixed append_reply_handoff for adoption-fork-codex - Fixed preview text: adoption says "new hcom identity" not "same"
Re-adds Claude custom-title and Codex thread-name resolution to `hcom r <name>`. Now errors when both tools match the same name instead of silently picking Claude. Routes resolved UUIDs through the shared adoption path (collision guard, CWD recovery, etc.).
Real Claude transcripts start with `{"type":"permission-mode",...}` which
carries no `cwd` — it first appears on line 2+. Reading only line 1 caused
adoption to silently fall back to $PWD. Scan up to 20 lines until a
non-empty `cwd` is found; leave codex's line-1 path unchanged.
Opencode uses the xdg-basedir npm package, which follows XDG on every platform — so sessions live at \`~/.local/share/opencode\` on macOS, not \`~/Library/Application Support/opencode\`. \`dirs::data_dir\` returned the latter, making adopt-by-UUID miss real sessions. Check \$XDG_DATA_HOME, then \`~/.local/share/opencode\`, then the platform default, returning the first that exists.
Adopting by UUID used to allocate a fresh 4-letter name whenever the prior binding had cascaded away — i.e. after every stop/kill. Fall back to scanning life.stopped events for a snapshot with the same session_id and delegate to the name-based resume path when found, so \`hcom r <uuid>\` resumes under the original identity. Also switch the collision-guard error messages from 'run hcom stop' to 'run hcom kill' to match project convention, and reword the surrounding comment to describe the lookaside vs. source-of-truth split honestly.
find_session_on_disk short-circuits for \`ses_\`-prefixed opencode IDs — only opencode is actually searched. Printing all four tool paths on miss was misleading. Scope the "Searched:" list to match the actual probe.
handle_remote_resume used to call run_local_resume_result which went straight to prepare_resume_plan with an Instance source — skipping the UUID/binding/events-fallback/thread-name/adoption chain that do_resume applies on the initiating device. hcom r <uuid>:DEVICE and hcom r <thread-name>:DEVICE bailed with "No stopped snapshot found" even when the session existed on the target. Rewrite run_local_resume_result to mirror do_resume's resolution order and return a LaunchResult via new sibling adopt_session_result. Extract the shared plan-building into build_adopt_plan so interactive and RPC adoption stay in lockstep.
- Claude: scan all custom-title entries per transcript and only match when the LAST one equals the requested name. Renaming a session A → B → C used to leave \`hcom r A\` resolving to it; now only \`hcom r C\` does. - Claude + Codex: when multiple distinct sessions currently share a thread name, bail with an ambiguity error listing the candidate session IDs and their last-touched timestamps. Previously the mtime/updated_at tiebreaker silently picked one — easy to accidentally resume the wrong session after duplicate renames.
- F1: last \`hcom stop\` straggler in prepare_resume_plan_from_source's active-instance bail is now \`hcom kill\`. - F2: stale session_bindings row no longer shortcuts to a name-based resume. Bail only on the active case; otherwise fall through to the life.stopped events lookup — events are the source of truth. - F3: extract resolve_name_to_plan. do_resume and run_local_resume_result each called resolve_display_name_or_stopped / is_session_id / binding check / events lookup / adoption separately; a bug fix (F1's kill wording, F2's stale-binding hazard) had to be threaded through both. Now one helper walks the chain as a bounded loop and returns (resolved_name, PreparedResume) for both paths. The old do_adopt_session / adopt_session_result wrappers go away.
- F4: load_stopped_snapshot now filters action='stopped' in SQL and ORDER BY id DESC LIMIT 1. Old version fetched the last 10 life events and scanned in Rust — the stopped snapshot could fall off the window after many relaunches. Mirrors find_stopped_instance_by_session_id. - F5: resolve_thread_name no longer prints "Resolved Claude/Codex thread" to stderr on every successful lookup. Routed to log_info. - F6: clarify in a comment that the fork cursor-anchoring block intentionally skips adoption-fork — there's no pre-reg row to protect; launcher::launch already sets HCOM_LAUNCH_EVENT_ID so the SessionStart hook creates the fresh row with the correct cursor.
Both help blocks used to say only "Resume a stopped agent by name" / "Fork an agent session" with no mention of UUID adoption, Claude /rename titles, Codex thread names, the :<device> suffix, or any of the --dir / --headless / --run-here / --hcom-prompt / --hcom-system-prompt flags that the resume parser accepts. A new user running \`hcom r --help\` had no way to discover those forms. Also factor SHARED_LAUNCH_FLAGS out so \`hcom <tool> --help\` and \`hcom r/f --help\` don't drift apart — the tool help now also lists --run-here, and both pick up flag additions in one place.
Owner
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Extends
hcom r <name>andhcom f <name>to accept three new forms in addition to hcom instance names:Session UUID —
hcom r 521cfc2b-be38-403a-b32e-4a49c9551b27— searches Claude, Codex, and Gemini transcript dirs on disk, extracts the last working directory from the transcript, and relaunches with full hcom wrapping (PTY, hooks, message delivery).Claude Code thread name —
hcom r skills-work— scans~/.claude/projects/*/*.jsonlfor{\"type\":\"custom-title\",\"customTitle\":\"...\"}entries (created by Claude Code's/renamecommand). Picks the most recently modified match.Codex thread name —
hcom r stabilization-review— looks up~/.codex/session_index.jsonlfor a matchingthread_name. Picks the most recently updated match.Resolution order: hcom instance → UUID → Claude thread → Codex thread.
hcom f(fork) gets the same resolution for free.Why
Useful when a session was started outside of hcom (or
/rename'd inside Claude Code) and you want to bring it under hcom management without remembering the UUID. Example:Implementation notes
claude_config_diranddetect_agent_typepromoted topub(crate)intranscript.rssoresume.rscan reuse them (instead of duplicating).build_resume_argsis shared with the normal resume path, so argv construction (--resume,-c experimental_resume, etc.) stays consistent.--diroverride).Tests
5 new unit tests:
test_is_session_id_valid— accepts UUIDstest_is_session_id_rejects_names— rejects 4-letter hcom names, dashed names, empty stringstest_extract_last_cwd_claude— picks the latestcwdfield across JSONL linestest_extract_last_cwd_codex— picks the latestpayload.cwdtest_extract_last_cwd_gemini— returns None (no CWD in Gemini transcripts)Codex/Claude thread-name resolution tests are inline in the original feature branch but were not included here because they can't override
dirs::home_dir()/claude_config_dir()cleanly. The parsing logic is covered by the integration path.All 33 existing + new
commands::resume::testspass (cargo test --release commands::resume).Test plan
cargo build --releasesucceedscargo test --release commands::resume— 33 passedhcom r <real-claude-uuid>relaunches in original directoryhcom r <renamed-claude-title>resolves via custom-title scanhcom r <codex-thread-name>resolves via session_index.jsonlhcom f <session-id>forks into current directory (not original)hcom r <active-session-id>errors out with a clear message🤖 Generated with Claude Code