Skip to content

feat(cli): genie recover-orphans — attach orphaned Claude JSONLs to executor rows#1699

Merged
namastex888 merged 1 commit into
mainfrom
feat/recover-orphans-cli
May 7, 2026
Merged

feat(cli): genie recover-orphans — attach orphaned Claude JSONLs to executor rows#1699
namastex888 merged 1 commit into
mainfrom
feat/recover-orphans-cli

Conversation

@namastex888
Copy link
Copy Markdown
Contributor

Summary

Algorithm

  1. Scan <claudeConfigDir>/projects/<encoded-cwd>/*.jsonl
  2. Cross-reference each session UUID against executors.claude_session_id — already-attached files are reported, not re-attached (idempotent)
  3. Map encoded dir → agent via agents.repo_path (re-encoded to match Claude Code's dir-naming scheme); prefer dir: master rows; lex-smallest UUID otherwise
  4. Apply: insert executors row with claudeSessionId = <uuid>, state = 'terminated', metadata.source = 'recover-orphans'; set agents.current_executor_id only if currently null; refuse if a live executor exists (heal-not-wipe)
  5. Audit: emit executor.recovered_from_orphan per attach

Manual smoke against ~/.claude/projects/

$ genie recover-orphans --list
(encoded: -home-genie-workspace-agents-genie)
  agent:    unmapped
  orphans:  122    attached: 0
  …
(encoded: -home-genie-workspace-agents-felipe)
  orphans:  117    attached: 0
(encoded: -home-genie-workspace-agents-felipe--genie-agents-scout)
  orphans:  117    attached: 0
(encoded: -home-genie-workspace-agents-genie-configure)
  orphans:  80    attached: 0

436 orphans surfaced across the four main genie agent dirs — comfortably above the 181 baseline from the original brain plan. (The "unmapped" rows mean the agents in those dirs have a repo_path other than the cwd Claude was launched in — operator can update or use --uuid for those.)

Test plan

  • Pure helpers: encodeCwdForClaudeProjects matches Claude Code's encoder, isSessionJsonl rejects backups + trimmed copies + non-UUID names, readFirstUserMessagePreview extracts first user text + tolerates malformed JSON, pickCanonicalAgent prefers dir:* then lex-smallest
  • Integration (DB-gated): scan + parse maps fixture JSONL to seeded agent; --apply --newest attaches the newest orphan and links it as current_executor_id; rerun is a no-op (idempotent); refuses to overwrite a live executor; --list never mutates
  • bun run typecheck — clean
  • bun run lint — no new errors (only pre-existing warnings in unrelated files)
  • Manual smoke: genie recover-orphans --list against real ~/.claude/projects/ lists 436 orphans across genie agent dirs
  • --help renders all flags

Out of scope

  • Cross-host migration
  • JSONL corruption repair
  • Auto-mapping unmapped dirs (operator updates agents.repo_path themselves, or uses --uuid)

Closes task #213.

🤖 Generated with Claude Code

…xecutor rows

Backfills `executors.claude_session_id` for Claude Code session JSONLs
that survived an executor crash, host reboot, or pre-#1684 spawn path
that forgot to write the session row. Without this, `genie agent resume`
cannot find the on-disk transcript even though Claude itself would
happily resume it.

Surface:
  genie recover-orphans                  # default: list orphans grouped by agent cwd
  genie recover-orphans --list           # explicit dry-run
  genie recover-orphans --dir <cwd>      # restrict to one agent cwd
  genie recover-orphans --apply --newest # auto-attach the newest orphan per dir
  genie recover-orphans --apply --uuid <id>

Algorithm:
  1. Scan <claudeConfigDir>/projects/<encoded-cwd>/*.jsonl.
  2. Cross-reference each session UUID against `executors.claude_session_id`
     (already-attached files are reported, not re-attached — idempotent).
  3. Map encoded dir → agent via `agents.repo_path` (re-encoded to match
     Claude Code's directory-naming scheme); prefer `dir:` master rows.
  4. Apply: insert/update `executors` row with `claudeSessionId = <uuid>`,
     `state = 'terminated'`, `metadata.source = 'recover-orphans'`. Sets
     `agents.current_executor_id` only if currently null. Refuses to
     overwrite a live executor (heal-not-wipe).
  5. Audit: emit `executor.recovered_from_orphan` per attach.

Manual smoke against ~/.claude/projects/ surfaces 436 orphans across the
four main genie agent dirs (genie/felipe/scout/configure) — more than the
181-baseline this CLI was built to recover.

Tests cover: filename/UUID gating, first-message preview parsing,
canonical-agent picker, scan + parse round-trip, --apply --newest
attaches and links, idempotency on rerun, refusal to overwrite a
live executor, and --list non-mutation.

Closes task #213. Companion to #1698 (P0 hotfix that prevents
future leaks).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Warning

Rate limit exceeded

@namastex888 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 49 minutes and 12 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 543431ad-26f6-47cb-87cb-1765a2517c4a

📥 Commits

Reviewing files that changed from the base of the PR and between 32c56e6 and 8bf50c0.

📒 Files selected for processing (3)
  • src/genie-commands/__tests__/recover-orphans.test.ts
  • src/genie-commands/recover-orphans.ts
  • src/genie.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/recover-orphans-cli

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8bf50c03ff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +329 to +331
claudeSessionId: candidate.sessionId,
state: 'terminated',
metadata,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Set ended_at when creating terminated recovered executors

This insert creates executors with state: 'terminated' but never sets ended_at, which leaves them looking live to this command’s own liveness check (isExecutorLive returns true when ended_at is null). In practice, after one recovery attach, later --apply/--uuid runs can be incorrectly skipped as “live executor” for that agent even though the recovered row is terminated; the row should be created with a non-null ended_at (or transitioned through the normal termination path).

Useful? React with 👍 / 👎.

Comment on lines +94 to +95
const fd = readFileSync(jsonlPath, { encoding: 'utf-8', flag: 'r' });
head = fd.slice(0, 16384);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Read only the JSONL head instead of loading entire files

The helper says it only inspects the first ~16 KiB, but readFileSync loads the full JSONL into memory before slicing. When scanning many large transcripts, this can cause significant memory and latency spikes during recover-orphans runs, especially in repos with long Claude histories; use a bounded read (e.g., open/read fixed-size buffer) so cost stays proportional to the intended head size.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the recover-orphans command to Genie, which scans for orphaned Claude session JSONL files and attaches them to database executor rows to enable session resumption. The implementation includes directory scanning, session mapping, and a dry-run mode. Review feedback suggests optimizing file I/O in readFirstUserMessagePreview by using openSync and readSync to avoid loading large files entirely into memory. Furthermore, it was noted that redundant database queries in the apply logic could be eliminated by reusing data already fetched during the initial scan.

* `ended_at IS NULL`, the orphan is reported but not attached.
*/

import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add openSync, readSync, and closeSync to the imports to support more efficient file reading in readFirstUserMessagePreview.

import { closeSync, existsSync, openSync, readFileSync, readdirSync, readSync, statSync } from 'node:fs';

Comment on lines +94 to +95
const fd = readFileSync(jsonlPath, { encoding: 'utf-8', flag: 'r' });
head = fd.slice(0, 16384);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation of readFirstUserMessagePreview reads the entire JSONL file into memory as a string before slicing the first 16KB. For large session logs, this is inefficient and can lead to high memory usage. It is better to read only the necessary amount of data using openSync and readSync. Using a hardcoded limit here is acceptable to prevent performance issues.

    const buffer = Buffer.alloc(16384);
    const fd = openSync(jsonlPath, 'r');
    try {
      const bytesRead = readSync(fd, buffer, 0, 16384, 0);
      head = buffer.toString('utf-8', 0, bytesRead);
    } finally {
      closeSync(fd);
    }
References
  1. It is acceptable to use hardcoded numeric limits (magic numbers) in non-critical fallback logic, especially when they serve as intentional caps to prevent performance issues like excessive I/O.

Comment on lines +473 to +475
const rows = await sql<{ current_executor_id: string | null }[]>`
SELECT current_executor_id FROM agents WHERE id = ${agentRow.id} LIMIT 1
`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This database query is redundant. The current_executor_id for each agent was already fetched during the initial scan in loadAgentsByEncodedCwd. You can include this value in the ProjectDirSummary object to avoid re-querying the database for every agent directory during the apply phase.

Comment on lines +504 to +506
const rows = await sql<{ current_executor_id: string | null }[]>`
SELECT current_executor_id FROM agents WHERE id = ${s.agent.id} LIMIT 1
`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This database query is redundant. The current_executor_id was already fetched during the initial scan. Including it in the ProjectDirSummary would avoid this extra round-trip.

@namastex888 namastex888 merged commit e3d839c into main May 7, 2026
11 checks passed
namastex888 added a commit that referenced this pull request May 7, 2026
Brings main's session-id writer hotfix (#1698) and recover-orphans CLI
(#1699) onto dev so the next dev → main PR triggers Version workflow's
@latest npm publish (gated on '/dev' in commit message).

Conflict resolutions:
- src/genie-commands/session.ts: kept BOTH _deps injection from #1698
  AND findOrCreateAgent UUID identity from wish #175 G3. Hotfix's
  claudeSessionId plumbing into createAndLinkExecutor preserved.
- src/genie.ts: additive — recover-orphans subcommand registered.
- src/lib/agent-directory.ts, executor-registry.ts, protocol-router.ts:
  surrounding context kept consistent with both branches' direction.
- src/__tests__/agent-team-inheritance.test.ts: adapted seedTemplate
  helper to post-migration-061 UUID-id + name lookup schema.

Carries main's other in-flight fixes:
- migrations 054 + 055 (subagent team inheritance, auto_resume default)
- agent-team-inheritance test fixture (132 LOC)
- release.yml + 044 test refinements
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant