feat(x): agent snapshot inheritance, session inspection, runtime docs
Third application of the reference mechanism: session turns whose
system prompt + tools are byte-identical to the context predecessor's
materialized snapshot persist { agentId, model, inheritedFrom } instead
of ~70KB per turn (measured: turn 2 of a session is now ~1.1KB total).
Inheritance is decided at createTurn by equality against the
materialized predecessor; the model stays concrete, and on
materialization the inherited record's own agentId/model win — a rule
the new test matrix caught as a real bug (the chain base's model was
overriding a mid-session model switch). Reducer invariants: inherited
snapshots must reference the context predecessor; tool identity arrives
via invocation events.
Test matrix per review: prompt-diff -> full snapshot, tools-diff ->
full snapshot, model-switch -> inherits with concrete model, multi-hop
chains materialize, standalone turns never inherit, unreadable
predecessor falls back to full, cyclic inheritance is corruption,
sessions denormalize the model from inherited snapshots.
The inspector now handles sessions too (auto-detected): overview with
per-turn status/size/input preview, --turns to cascade full turn
inspection. Documented in a new repo-root AGENTS.md (storage layout,
reference model, inspector usage, invariants) with a CLAUDE.md pointer.
Breaking for dev turn files: wipe ~/.rowboat/storage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 13:17:30 +05:30
|
|
|
# AGENTS.md — Working on the Rowboat runtime
|
|
|
|
|
|
|
|
|
|
Context for AI coding agents (and humans) working on this repo. General
|
|
|
|
|
codebase orientation lives in `CLAUDE.md`; this file covers the **new
|
|
|
|
|
turn/session runtime** in `apps/x` — its storage, its debugging tools, and
|
|
|
|
|
the invariants you must not break.
|
|
|
|
|
|
|
|
|
|
## The runtime in one paragraph
|
|
|
|
|
|
|
|
|
|
Chats are **sessions**; each user message starts a **turn**. Both are
|
|
|
|
|
append-only JSONL event logs under `~/.rowboat/storage/{turns,sessions}/YYYY/MM/DD/`.
|
|
|
|
|
All state is derived by pure reducers (`reduceTurn`, `reduceSession` in
|
|
|
|
|
`@x/shared/src/turns.ts` / `sessions.ts`) shared byte-for-byte between the
|
|
|
|
|
main process and the renderer. Design specs:
|
|
|
|
|
`apps/x/packages/core/docs/turn-runtime-design.md` and `session-design.md` —
|
|
|
|
|
read the relevant spec before changing runtime behavior.
|
|
|
|
|
|
|
|
|
|
## Storage is reference-based — files store each fact once
|
|
|
|
|
|
|
|
|
|
Three applications of the same mechanism:
|
|
|
|
|
|
|
|
|
|
1. **Context**: a session turn's `context` is `{ previousTurnId }`; the
|
|
|
|
|
conversation prefix is materialized by walking the chain
|
|
|
|
|
(`TurnRepoContextResolver`).
|
|
|
|
|
2. **Model requests**: `model_call_requested.request.messages` is a list of
|
|
|
|
|
string refs into the turn's own events — `"context"`, `"input"`,
|
|
|
|
|
`"assistant:<index>"`, `"toolResult:<toolCallId>"` — recording only what
|
|
|
|
|
is NEW since the previous call.
|
|
|
|
|
3. **Agent snapshots**: when a turn's system prompt + tools are
|
|
|
|
|
byte-identical to its predecessor's, `turn_created.agent.resolved` is
|
|
|
|
|
`{ agentId, model, inheritedFrom }` instead of re-persisting ~70KB. The
|
|
|
|
|
model stays concrete (mid-session model switches still inherit).
|
|
|
|
|
|
|
|
|
|
The exact provider payload is rebuilt by `composeModelRequest`
|
|
|
|
|
(`packages/core/src/turns/compose-model-request.ts`) — **the same code path
|
|
|
|
|
the loop transmits through**, so the file plus the composer reproduce the
|
|
|
|
|
wire bytes exactly (there is a property test asserting composed == sent).
|
|
|
|
|
|
|
|
|
|
## Inspecting turns and sessions
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
cd apps/x/packages/core
|
|
|
|
|
|
|
|
|
|
# A whole session: title, per-turn status/size/input preview
|
|
|
|
|
npm run inspect -- <sessionId | path/to/session.jsonl>
|
|
|
|
|
|
|
|
|
|
# One turn: per model call, the EXACT provider payload — resolved system
|
|
|
|
|
# prompt, tool list, wire-form messages (user-message context woven in,
|
|
|
|
|
# tool-result envelopes), and the response/failure
|
|
|
|
|
npm run inspect -- <turnId | path/to/turn.jsonl> [modelCallIndex] [--full]
|
|
|
|
|
|
|
|
|
|
# Cascade full turn inspection across a session
|
|
|
|
|
npm run inspect -- <sessionId> --turns
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`--full` prints untruncated system prompts and message contents. Turn vs
|
|
|
|
|
session ids are auto-detected. This is the intended way to see "what did the
|
|
|
|
|
model actually receive" — the raw JSONL deliberately stores structural facts
|
|
|
|
|
and references, never the derived wire form.
|
|
|
|
|
|
2026-07-02 14:19:24 +05:30
|
|
|
## The legacy runs runtime is code-mode-only
|
|
|
|
|
|
|
|
|
|
The old runs runtime (`packages/core/src/runs/`, `AgentRuntime`/`streamAgent`
|
|
|
|
|
in `packages/core/src/agents/runtime.ts`, the `runs:*` IPC channels, and the
|
|
|
|
|
renderer's legacy chat-tab state machine in `App.tsx`) remains in the tree
|
|
|
|
|
**solely for code-mode sessions** — a deliberate, documented carve-out:
|
|
|
|
|
|
|
|
|
|
- A code session IS a run (`sessionId === runId`). Direct-mode prompts drive
|
|
|
|
|
an external ACP agent and hand-assemble `code-run-event`s into the run log
|
|
|
|
|
(`code-mode/sessions/service.ts`); rowboat-mode code tabs go through
|
|
|
|
|
`runs:createMessage` → the old `AgentRuntime` loop.
|
|
|
|
|
- Everything else — main chat, background tasks, live notes, knowledge
|
|
|
|
|
pipelines, scheduled agents — runs on turns/sessions. Do NOT add new
|
|
|
|
|
callers of `createRun`/`createMessage`/`streamAgent`; headless work uses
|
|
|
|
|
`packages/core/src/agents/headless.ts` (`startHeadlessAgent`/
|
|
|
|
|
`runHeadlessAgent`).
|
|
|
|
|
- Temporary bridges that die when code-mode is unified: the renderer
|
|
|
|
|
transcript loader's `runs:fetch` fallback (`lib/agent-transcript.ts`), the
|
|
|
|
|
`runs:downloadLog` fallback in the chat sidebar, and the `notify-user`
|
|
|
|
|
gate's `fetchRun` fallback (`application/lib/builtin-tools.ts`).
|
|
|
|
|
- The remaining migration (designed but deferred): port code sessions onto
|
|
|
|
|
sessions/turns — rowboat-mode prompts become normal turns with
|
|
|
|
|
`codeMode`/`codeCwd` composition (already supported by the agent
|
|
|
|
|
resolver's composition overrides), direct-mode prompts become a delegated
|
|
|
|
|
turn kind carrying opaque code events. That project deletes `runs/`
|
|
|
|
|
entirely.
|
|
|
|
|
|
feat(x): agent snapshot inheritance, session inspection, runtime docs
Third application of the reference mechanism: session turns whose
system prompt + tools are byte-identical to the context predecessor's
materialized snapshot persist { agentId, model, inheritedFrom } instead
of ~70KB per turn (measured: turn 2 of a session is now ~1.1KB total).
Inheritance is decided at createTurn by equality against the
materialized predecessor; the model stays concrete, and on
materialization the inherited record's own agentId/model win — a rule
the new test matrix caught as a real bug (the chain base's model was
overriding a mid-session model switch). Reducer invariants: inherited
snapshots must reference the context predecessor; tool identity arrives
via invocation events.
Test matrix per review: prompt-diff -> full snapshot, tools-diff ->
full snapshot, model-switch -> inherits with concrete model, multi-hop
chains materialize, standalone turns never inherit, unreadable
predecessor falls back to full, cyclic inheritance is corruption,
sessions denormalize the model from inherited snapshots.
The inspector now handles sessions too (auto-detected): overview with
per-turn status/size/input preview, --turns to cascade full turn
inspection. Documented in a new repo-root AGENTS.md (storage layout,
reference model, inspector usage, invariants) with a CLAUDE.md pointer.
Breaking for dev turn files: wipe ~/.rowboat/storage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 13:17:30 +05:30
|
|
|
## Invariants to respect
|
|
|
|
|
|
|
|
|
|
- Turn/session files are **append-only**; reducers reject impossible
|
|
|
|
|
histories loudly (`TurnCorruptionError`). Never hand-edit files.
|
|
|
|
|
- Durable events are persisted **before** side effects (model calls, tool
|
|
|
|
|
invocations). Deltas (`text_delta`, …) are stream-only, never persisted.
|
|
|
|
|
- The reducers in `@x/shared` must stay pure (no I/O, no node imports) —
|
|
|
|
|
the renderer imports them directly.
|
|
|
|
|
- Every behavior change needs tests: reducers in `packages/shared`,
|
|
|
|
|
runtime/sessions in `packages/core`, renderer stores/views in
|
|
|
|
|
`apps/renderer` (all vitest; run `npm test` per package).
|
|
|
|
|
- Schema changes: the schema is pre-release (`schemaVersion: 1` throughout);
|
|
|
|
|
breaking changes are acceptable but require wiping `~/.rowboat/storage`
|
|
|
|
|
and a note in the commit message.
|