feat(resume): carry a session across models and machines - #80
Merged
Conversation
`aether resume` could replay a prior session's transcript to the screen, but the brain never saw a byte of it: continuing meant re-typing the story so far, and only ever on the machine where the work started. This makes the resume path mean what it says. A handoff (core/handoff.ts) is the machine-facing half of a session log — a small JSON document distilled from one run: the task, the model that ran it, the verify gate's verdict, the failing-test count, the files it changed, the verification command, and the repository identity (origin remote, branch, HEAD). - `aether agent --resume <id>` now prepends a continuation brief built from that record to the task the brain receives, so a different model picks the thread up with the project context in hand. With no new task, the run continues the original one (it used to fail with "nothing to do"). - `aether resume export [id] [--out <file>]` writes the handoff to a file. Nothing in it is keyed to an absolute path, so it can be copied to another checkout, machine, or OS, and `aether agent --resume <file>` continues there. A file reference is deliberately not workspace-scoped; a session id still is. - The handoff is a summary, not a transcript: no file contents, no shell commands, no credential-shaped values. Untrusted files are validated field by field on the way back in, and one written by a newer Agent is refused with an upgrade hint rather than half-read. Three fixes the above needed, each a bug in its own right: - `aether agent --local "<task>"` spawned the separately-installed Python brain unconditionally, so a plain `npm i -g aether-agents` could only ever answer `spawn python ENOENT`. The one-shot offline path now drives the Ollama brain that ships inside the package — the same one the REPL's `--local` turns already used. `AETHER_LOCAL_BRAIN=python` opts back in, and the choice is a pure decision in core/backend.ts beside chooseBackend. - The session log's credential filter matched `pat` as a substring, so `path` (and PATH, patch, pattern) was stored as "[REDACTED]". Every log could say a file changed but not which one. `pat` is now anchored to a whole segment; pat, gh_pat and pat_token are still redacted. - The Ollama brain reported a placeholder `remaining: 1` on any unsuccessful run, so an unreachable-Ollama turn printed "1 test failing" when no test had run. The failing count comes only from the host's own verify run. `npm run demo:handoff` (scripts/handoff-demo.ts, docs/demo/handoff.md) is the end-to-end proof and takes about five seconds: it builds a throwaway git repo with two genuinely failing tests, runs the real CLI on model A until half the work is done, exports the handoff, creates a second checkout at a different path, deletes the first checkout AND its logs, then finishes the job on model B with `--resume <file>` and no restated task. The model — and only the model — is a scripted local stub so the run is deterministic and needs no download or account; AETHER_DEMO_REAL=1 runs the identical script against real Ollama models. It asserts that session B's prompt carried the brief, that `node --test` is green when run independently of the agent, and that the verify gate exited 0, so it works as a CI gate as well as a demo.
…log once
Cleanup pass over the handoff feature. No intended behaviour change except
where a reused helper is strictly better than the hand-rolled version — those
are called out below.
Reuse:
- `writeHandoff` uses `atomicWriteFile` (core/durable_store.ts), the same
write-then-rename every other durable file in this CLI already uses. An
interrupted `resume export` can no longer destroy a good handoff or leave
half a JSON document, and `--out reports/handoff.json` now creates the
missing parent instead of failing with ENOENT.
- `readHandoff` uses `readJsonFile`, which already distinguishes missing from
unreadable from corrupt — so the hand-rolled ENOENT check is gone and an
empty file reads as "corrupt", the codebase's word for it, rather than
"Unexpected end of JSON input".
- Untrusted strings go through `sanitizeTerm` (ui/text.ts) and `clipCodePoints`
(ui/theme.ts) instead of a local clipper. A handoff arrives from another
machine and its strings are BOTH printed and prepended to the brain's prompt,
so it was the only untrusted-input surface in the CLI not passing through the
terminal sanitizer. `clipCodePoints` also stops a truncation from cutting a
surrogate pair in half.
- `wroteFile()` is now the one definition of "the run changed a file", called
by both a handoff's `filesTouched` and cmdCode's live blast-radius set, which
had the predicate written out twice.
`isHandoffPath` had a real defect: its separator class held only a forward
slash, so `--resume C:\work\handoff` (no .json) was classified as a session id
and died as "invalid session id". It now inverts `requireOpaqueId` — the same
rule `loadSession` enforces a moment later — so the two branches provably
partition the input, and Windows separators, `..`, and `~/` fall out for free.
Read the session log once. `resolveHandoff` loaded the session, distilled it,
and threw it away; `replaySession` then re-derived the file-vs-id decision and
loaded the same session again to render it. `resolveResume` returns
`{handoff, session}` and `resumeReplayLines` renders from it — one read, one
decision, and `replaySession`'s `ref`/`cwd` parameters disappear. On a
2000-event log that is 2000 fewer JSON parses and decodes per resume.
`aether resume` dispatches on argv like every other subcommand-bearing command
in main.ts, instead of joining `rest` into a string and re-splitting it by
prefix. `cmdResumeExport` is its own exported function. Side effect: `aether
resume abc def` reports `no such session: abc` rather than looking up the id
`"abc def"`.
Smaller: `summarizeEvents` bounds `highlights` as it goes rather than growing
to N and slicing to 40, and uses a Set for `filesTouched`; `parseHandoff`
bounds arrays before clipping them, so a hostile file with 10k highlights costs
40 clips rather than 10k; one `repoFrom` and one `asObject` replace two
spellings each; the demo's two model scripts are one shape with two
substitutions; the demo header no longer restates docs/demo/handoff.md.
Tests: the four resume-command cases share one `withLogRoot` fixture instead of
pasting the same 10-line prologue and env restore four times, and there are new
cases for the Windows path classification, the sanitizer, the shared write
predicate, and `--out` into a missing directory.
Deliberately not done: collapsing readRepoIdentity's three git spawns into two
(one saved spawn, subtler parsing); `latestSession`'s full-log scan (real, but
pre-existing and outside this diff); rebuilding `summarizeEvents` on top of
`monologueLine` (its tree-rendering shape is not a summary's).
AetherAI3
force-pushed
the
feat/growth-release
branch
from
August 19, 2026 12:11
a9fea93 to
0b115cb
Compare
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.
The one capability to remember
Start a task on one model. Finish it on another, on another machine. Your tests decide when it's done.
aether resumecould already replay a prior session's transcript to the screen — but the brain never saw a byte of it. Continuing meant re-typing the story so far, and only ever on the machine where the work started. This PR makes the resume path mean what it says.What's in it
A handoff (
src/core/handoff.ts) is the machine-facing half of a session log: a small JSON document distilled from one run — the task, the model that ran it, the verify gate's verdict, the failing-test count, the files it changed, the verification command, and the repository identity (origin remote, branch, HEAD).aether agent --resume <id>prepends a continuation brief built from that record to the task the brain receives, so a different model picks the thread up with the project context in hand. With no new task the run continues the original one — it used to fail withnothing to do.aether resume export [id] [--out <file>]writes the handoff to a file. Nothing in it is keyed to an absolute path, so it can be copied to another checkout, machine, or OS, andaether agent --resume <file>continues there. A file reference is deliberately not workspace-scoped; a session id still is.Three bugs this needed, each real on its own
aether agent --local "<task>"was dead out of the boxnpm i -g aether-agentscould only answerspawn python ENOENT. The one-shot offline path now drives the Ollama brain that ships in the package — the same one the REPL's--localturns already used.AETHER_LOCAL_BRAIN=pythonopts back in; the choice is a pure decision besidechooseBackend.patas a substring, sopath(andPATH,patch,pattern) was stored as[REDACTED]. Every log could say a file changed but not which one.patis now anchored to a whole segment;pat,gh_pat,pat_tokenare still redacted.remaining: 1, so an unreachable-Ollama turn printed1 test failingwhen nothing had run. The count now comes only from the host's own verify run.Proof
npm run demo:handoff # ~5s, no account, no model downloadscripts/handoff-demo.tsbuilds a throwaway git repo with two genuinely failing tests, runs the real CLI on model A until half the work is done, exports the handoff, creates a second checkout at a different absolute path, deletes the first checkout and its session logs, then finishes the job on model B with--resume <file>and no restated task.The model — and only the model — is a scripted local HTTP server speaking Ollama's OpenAI-compatible endpoint, which is what makes the run deterministic enough to be a gate. Everything else is real: the CLI, the git repos, the file edits, the permission gate, the handoff file,
node --test, and the verify gate. It asserts three things independently:src/slug.js;node --testis green when run by the script, outside the agent;AETHER_DEMO_REAL=1runs the identical script against real Ollama models. Documented indocs/demo/handoff.md, including the 20–45 second recording sequence.Sample run:
Verification
npm run typecheck— cleanhandoff,resume_cmd,backend_select,session_log,session_resume,cli_registry,command_docs_parity,brain_ollama,ui,version— green locallynpm run demo:handoff— PASSEDnpm run smoke— 3 pass / 4 skip / 0 fail (skips need Ollama, network, or sign-in)npm pack --dry-run— clean; the production verifier's pack-install-launch test is greenFull-suite CI is the gate here — one
tool_executortest is pathologically slow on the authoring machine and was not run locally.Notes for review
src/core/brain_ollama.tsis also touched by fix(brain): correlate Ollama tool results by id, generate schemas, stop faking steer [SC-A4.1] #75; the overlap is the singleremainingline.