Custody without wiring: keep every session, read it back by id #86

Merged
mph merged 24 commits from transcript-custody into main 2026-10-16 09:18:33 +00:00
Member

The slice

Goal (the use). I run Claude Code sessions through the SDK without wiring anything for custody, and I can open any of them by its session id after the vendor has deleted its copy.

Fit test (close gate). In a procedures worktree built against a locally published 0.7.0-SNAPSHOT, the worker's ClaudeCode.session(options) call stays as it is — no custody argument. Run a real worker session, move its vendor transcript tree aside, open that session's page by its id, put the tree back.

Fit passed (2026-10-13, read by Michal): a worker session in the offline harness, kept with no custody argument, opened whole on its page after its vendor project directory left ~/.claude-gladys/projects. Card: project-management/efforts/transcript-custody/slices/01-custody-without-wiring/card.md.

What ships

  • Custody is on by default. ClaudeCode.query(options, archive: Custody = Custody.Keep) and ClaudeCode.session(...) keep every session they run. Custody.KeepUnder(root) keeps elsewhere, Custody.Off keeps nothing.
  • Scoped by installation, named by id (TC-D1, TC-D3). The installation is the config directory the CLI runs under: CLAUDE_CONFIG_DIR from the session's options, else the inherited environment, else ~/.claude. Records go to $XDG_DATA_HOME/agent-transcripts/claude-code/<installation key>/ as <id>.jsonl, <id>/ and <id>.custody.json, beside an installation marker that refuses a second installation with the same lossy key.
  • Read back with nothing wired. ZioSessionCatalog.default(cwd); the vendor tree resolves first when a cwd is known, the kept copy by id alone. ConversationArchive/ZioConversationArchive are renamed SessionCatalog/ZioSessionCatalog.
  • Custody-borne facts only. Installation, harness and when custody was taken — written once, on the first keep. The transcript itself is the vendor's bytes.
  • Two defects fixed. Closing a session's scope never kept the transcript (4f80c8c), and a close killed the CLI before it could write its tail — it now waits for the exit up to SessionProcess.CloseTimeout, and a Stop still kills at once (3f9a359).
  • One log line per session kept, naming the place; a failed keep is logged and never breaks the session or the query.
  • Docs. README's Session Custody and "Upgrading from 0.6"; ARCHITECTURE's custody section; ADR 0001 decision 8 carries the TC-D1 amendment note.

Review

Ten review skills, run on the whole diff; report in slices/01-custody-without-wiring/review-slice-20261012-1215.md. One critical finding — TestSystem's environment is one map per spec, so the suites that name their own data home now run sequentially — plus nine warnings closed in-loop: the guarded custody-facts write, the installation claim made atomic, symlink refusal on the marker and the facts file, bounded log values, one owner for the kept-directory arithmetic, and the documentation gaps (the ArchiveConfig upgrade shape, the installation-wide read scope, both close ceilings).

Gate: ./mill __.compile + __.test + __.checkFormat green.

Follow-ups (not in this PR)

project-management/efforts/transcript-custody/FOLLOW-UPS.md, grouped by where the fix lands:

  • The keep's two custody files and the shape of ArchiveMirror — claimKeptDir throws where the signature promises a report; the marker and facts file are written with the umask; a directory at the facts path counts as written; mirror would read as five steps.
  • The vendor directory name we encode — the CLI may replace more than /.
  • The ui.demo host with an open follow stream never exits (diagnosed, cause not found).
  • ./mill __.test prints a duplicate-logback.xml warning (not from this branch).

Next in the effort map: open-a-session-from-custody (re-enter a kept session, not only read it), custody-for-direct-and-effectful, move-kept-transcripts, and procedures-on-0.7 — the live dashboard, whose change is written and green in a worktree but needs a published 0.7.0 and the old records copied first.

🤖 Generated with Claude Code

## The slice **Goal (the use).** I run Claude Code sessions through the SDK without wiring anything for custody, and I can open any of them by its session id after the vendor has deleted its copy. **Fit test (close gate).** In a `procedures` worktree built against a locally published `0.7.0-SNAPSHOT`, the worker's `ClaudeCode.session(options)` call stays as it is — no custody argument. Run a real worker session, move its vendor transcript tree aside, open that session's page by its id, put the tree back. **Fit passed** (2026-10-13, read by Michal): a worker session in the offline harness, kept with no custody argument, opened whole on its page after its vendor project directory left `~/.claude-gladys/projects`. Card: `project-management/efforts/transcript-custody/slices/01-custody-without-wiring/card.md`. ## What ships - **Custody is on by default.** `ClaudeCode.query(options, archive: Custody = Custody.Keep)` and `ClaudeCode.session(...)` keep every session they run. `Custody.KeepUnder(root)` keeps elsewhere, `Custody.Off` keeps nothing. - **Scoped by installation, named by id** (TC-D1, TC-D3). The installation is the config directory the CLI runs under: `CLAUDE_CONFIG_DIR` from the session's options, else the inherited environment, else `~/.claude`. Records go to `$XDG_DATA_HOME/agent-transcripts/claude-code/<installation key>/` as `<id>.jsonl`, `<id>/` and `<id>.custody.json`, beside an `installation` marker that refuses a second installation with the same lossy key. - **Read back with nothing wired.** `ZioSessionCatalog.default(cwd)`; the vendor tree resolves first when a cwd is known, the kept copy by id alone. `ConversationArchive`/`ZioConversationArchive` are renamed `SessionCatalog`/`ZioSessionCatalog`. - **Custody-borne facts only.** Installation, harness and when custody was taken — written once, on the first keep. The transcript itself is the vendor's bytes. - **Two defects fixed.** Closing a session's scope never kept the transcript (`4f80c8c`), and a close killed the CLI before it could write its tail — it now waits for the exit up to `SessionProcess.CloseTimeout`, and a Stop still kills at once (`3f9a359`). - **One log line per session kept**, naming the place; a failed keep is logged and never breaks the session or the query. - **Docs.** README's Session Custody and "Upgrading from 0.6"; ARCHITECTURE's custody section; ADR 0001 decision 8 carries the TC-D1 amendment note. ## Review Ten review skills, run on the whole diff; report in `slices/01-custody-without-wiring/review-slice-20261012-1215.md`. One critical finding — `TestSystem`'s environment is one map per spec, so the suites that name their own data home now run sequentially — plus nine warnings closed in-loop: the guarded custody-facts write, the installation claim made atomic, symlink refusal on the marker and the facts file, bounded log values, one owner for the kept-directory arithmetic, and the documentation gaps (the `ArchiveConfig` upgrade shape, the installation-wide read scope, both close ceilings). Gate: `./mill __.compile + __.test + __.checkFormat` green. ## Follow-ups (not in this PR) `project-management/efforts/transcript-custody/FOLLOW-UPS.md`, grouped by where the fix lands: - [ ] The keep's two custody files and the shape of `ArchiveMirror` — `claimKeptDir` throws where the signature promises a report; the marker and facts file are written with the umask; a directory at the facts path counts as written; `mirror` would read as five steps. - [ ] The vendor directory name we encode — the CLI may replace more than `/`. - [ ] The `ui.demo` host with an open follow stream never exits (diagnosed, cause not found). - [ ] `./mill __.test` prints a duplicate-`logback.xml` warning (not from this branch). Next in the effort map: `open-a-session-from-custody` (re-enter a kept session, not only read it), `custody-for-direct-and-effectful`, `move-kept-transcripts`, and `procedures-on-0.7` — the live dashboard, whose change is written and green in a worktree but needs a published 0.7.0 and the old records copied first. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The installation is the config directory the CLI runs under, resolved from
the session's variables, then the inherited environment, then ~/.claude.
Records are kept under $XDG_DATA_HOME/agent-transcripts (else
~/.local/share/agent-transcripts), in claude-code/<installation key>.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- SessionCatalog (was ConversationArchive) is scoped by installation and
  opens a session by its id: vendor first when the cwd is known, the kept
  copy only otherwise (TC-D1, TC-D3).
- Records are kept under <root>/claude-code/<installation key>/ as the
  vendor's bytes, with <id>.custody.json written once and an installation
  marker that refuses a key another installation holds.
- ClaudeCode.query and session take archive: Custody = Custody.Keep;
  Custody.KeepUnder(root) and Custody.Off replace Option[ArchiveConfig].
- ZioSessionCatalog.default opens the catalog with nothing wired.
- ui.demo wires no custody and reads every session through the catalog.
- Every zio spec gets its own data home, so no test keeps a session under
  the home directory of the machine running it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closing a session interrupts the stdout reader, so the keep in recordEnd
ran only when the CLI happened to exit first. A finalizer registered
before the kill now runs the final keep after the CLI has ended. The
custody tests wait for the keep a result schedules before writing, so a
late background keep can no longer pass them by timing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closing a session closed the CLI's stdin and killed it at once, so the
lines the CLI writes after its last result could be lost from its
transcript. A normal or failed close now waits for the CLI to exit, up to
SessionProcess.CloseTimeout, and kills it only after that; a close by
interruption (a Stop) still kills at once. The wait is interruptible and
disconnected, since a finalizer runs uninterruptibly and a CLI may ignore
SIGTERM.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The keep that first takes a session reports it (MirrorReport.firstKeep)
and the hook logs one info line naming the kept directory. The README
says where records go, how to opt out, how to read one by id, and how to
upgrade from 0.6.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review of the slice by ten skills; one critical finding and the warnings
worth closing before the pull request.

- Test isolation: TestSystem's environment is one map per spec, so the
  suites that name their own data home run sequentially. In parallel they
  read each other's root, and the keep lands in the wrong place.
- A facts file that cannot be written is one more failed file in the
  report, not the loss of a copy that landed.
- The installation claim is the create itself, so two keepers of one lossy
  key cannot both find it unclaimed; a marker that is a symlink is refused,
  and the facts file is never written through one.
- A value read out of a marker reaches a log line printable and bounded.
- The kept-directory arithmetic has one owner (CustodyRoot).
- README says what the upgrade does to ArchiveConfig, and that reading by
  id is scoped to the installation, not to a project.
- Session says a close pays both of its ceilings, and SessionProcess states
  the order its finalizers produce.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CLI resolves a session id across its whole projects directory, so a
rename inside that tree is not a prune: custody stops finding the vendor
copy, the CLI does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
workflow(sl): close transcript-custody/01-custody-without-wiring
Some checks failed
CI / Check Formatting (pull_request) Successful in 1m11s
CI / Compile (pull_request) Successful in 1m37s
CI / Check Linting (pull_request) Failing after 1m10s
CI / Check Bundle Size (pull_request) Has been cancelled
CI / Run Tests (pull_request) Has been cancelled
CI / Run Integration Tests (pull_request) Has been cancelled
7407392ca7
Fit passed. The parked items are flushed into the effort map, the review's
remaining findings are grouped in FOLLOW-UPS.md, and ADR 0001 decision 8
carries the amendment note TC-D1 asked for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
chore(transcript-custody): move review-state card out of the tree
Some checks failed
CI / Check Formatting (pull_request) Successful in 40s
CI / Compile (pull_request) Successful in 1m7s
CI / Check Linting (pull_request) Failing after 1m5s
CI / Run Integration Tests (pull_request) Successful in 1m38s
CI / Run Tests (pull_request) Failing after 2m34s
CI / Check Bundle Size (pull_request) Successful in 43s
8eee0178c8
fix: answer a refused keep instead of throwing it
All checks were successful
CI / Check Formatting (pull_request) Successful in 42s
CI / Compile (pull_request) Successful in 1m5s
CI / Run Integration Tests (pull_request) Successful in 1m34s
CI / Check Linting (pull_request) Successful in 1m9s
CI / Check Bundle Size (pull_request) Successful in 52s
CI / Run Tests (pull_request) Successful in 1m51s
2e05c13d39
CI's lint step refuses a throw (DisableSyntax.throw), which is also what
the review asked for: ArchiveMirror.mirror answers
Either[ArchiveError, MirrorReport], and the catalog absolves it. A claim
that fails for any other reason is a typed ArchiveIOError rather than an
exception crossing the effect boundary.

The facts-write test leans on a directory's own permissions, which do not
stop root, so it is ignored where they do not bite rather than passing for
the wrong reason in a container that runs as root.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mph merged commit 527697cfc3 into main 2026-10-16 09:18:33 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
iterative-works/claude-code-query!86
No description provided.