refactor: share snapshot capture persistence#69
Conversation
|
/coder-agents-review |
There was a problem hiding this comment.
Clean refactoring, well-scoped to its stated goal. The shared capture module eliminates real duplication between the host and CLI paths, the test suite is genuine (real filesystem, no mock theatre), and the validation-before-persistence ordering is correct and tested. The PR also adds schema validation to the host snapshot path where it was previously absent, which is strictly an improvement.
Panel: 10 reviewers (Bisky, Hisoka, Mafu-san, Mafuuu, Pariston, Ging-ts, Chopper, Robin, Meruem, Melody). Ging-ts and Melody returned with no findings; Hisoka and Pariston confirmed the design holds under adversarial pressure.
1 P2, 4 P3, 1 Nit.
"The (snapshot.scrollbackLines ?? []) fallback in createSnapshotResult is exercised only with scrollback present; the empty-array spread is unproven through the capture path." (Bisky)
🤖 This review was automatically generated with Coder Agents.
|
/coder-agents-review |
There was a problem hiding this comment.
All 6 R1 findings addressed in commit 6503400. Fixes verified by all 6 R2 panel reviewers (Bisky, Mafuuu, Mafu-san, Chopper, Robin, Meruem). Each fix addresses the root cause: parameterized error message instead of hardcoded string, structured CliError instead of bare invariant, exported shared symbols instead of duplication, complete test coverage for the text-without-scrollback path, and centralized test helpers.
One new Nit (AGTTY69-7): the createSemanticSnapshot test factory is duplicated in 4 files, and given this PR already extracted createTemporarySessionDir into test/helpers.ts, extracting the snapshot factory would be the natural companion.
CI passes (11 checks). No P0-P2 findings.
"The fix commit is 6 files, 123+/101-. Proportional to 6 findings. No scope drift. No unrelated changes smuggled in." (Mafu-san)
🤖 This review was automatically generated with Coder Agents.
|
/coder-agents-review |
There was a problem hiding this comment.
AGTTY69-7 verified fixed: createTestSemanticSnapshot consolidated in test/helpers.ts, used by all three test files. Robin confirmed, no duplication remains.
One new Nit (AGTTY69-8). All prior findings remain resolved. CI pending.
🤖 This review was automatically generated with Coder Agents.
|
/coder-agents-review |
There was a problem hiding this comment.
AGTTY69-8 verified fixed: validation failure test now asserts message: 'Snapshot result validation failed.', closing the regression gap for the R1 P2 fix.
All 8 findings across 4 rounds are resolved. No new findings. CI pending.
🤖 This review was automatically generated with Coder Agents.
2e7daa3 to
efa9818
Compare
|
/coder-agents-review |
There was a problem hiding this comment.
Head SHA changed via rebase; the PR-scoped delta since R4 is a CONTEXT.md addition ("Event Log" domain term). No code changes to the snapshot capture module or tests.
All 8 findings from R1-R3 remain resolved. Panel (Bisky, Mafuuu, Robin) reviewed the full diff. No new findings across 5 rounds.
🤖 This review was automatically generated with Coder Agents.
Fixes #60.
Summary
src/snapshot/capture.ts.CONTEXT.md.User-facing / automation-facing behavior
No intended public JSON, RPC, filename, artifact, or manifest shape changes. Snapshot artifacts still contain exactly the emitted snapshot result JSON with two-space formatting and a trailing newline.
Validation
mise run cinpx vitest run test/unit/snapshot/capture.test.ts test/unit/commands/snapshot.test.tsnpx vitest run --maxWorkers=1 test/integration/host-renderer-rpc.test.tsnpm run typechecknpm run lintnpm run format:checknpm run buildnpm run testnpm run smoke:install -- --skip-buildgit diff --checkDogfooding
Used an isolated
AGENT_TTY_HOMEto run both live-host and offline-replay snapshot flows:Verified each emitted snapshot
.resultexactly matched its persisted snapshot artifact JSON.Dogfood proof root from the implementation workspace:
Screenshot proof artifacts:
WebM proof artifacts:
Design-doc deviations
None. This keeps the existing CLI/RPC/artifact contracts while consolidating duplicated snapshot behavior.
📋 Implementation Plan
Plan: Share snapshot capture across live and offline paths
Goal
Implement GitHub issue #60: live host snapshot RPCs and CLI offline replay snapshots should share one snapshot-specific implementation for deriving public
SnapshotResultpayloads and persisting matching snapshot artifacts, while preserving the existing CLI/RPC JSON contract and artifact behavior.Evidence gathered
src/cli/commands/snapshot.tsvia localcreateSnapshotResult()andpersistSnapshotArtifact()helpers.src/host/hostMain.ts.SnapshotResultSchemainsrc/protocol/schemas.tsremains the public validation contract for structured/text snapshot results.RendererBackendexposes a non-emptyrendererBackendidentifier used for artifact metadata.src/storage/artifactPaths.tsownssnapshotFilename(seq, format)and artifact path safety.test/integration/host-renderer-rpc.test.tsverifies returned RPC snapshot results, persisted artifact JSON equality, manifest metadata, scrollback metadata, and defaults.test/unit/commands/snapshot.test.tscurrently duplicates low-level offline replay artifact expectations that should move to the shared module where practical.Resolved design decisions
Shared module boundary
backend.snapshot({ includeScrollback, includeCells }).SnapshotResult, validating it, writing the JSON artifact, and appending the artifact manifest entry.Module location
src/snapshot/capture.ts.src/cli/orsrc/host/because both are callers.Layered API
captureSnapshotResult(...).Renderer backend metadata invariant
rendererBackendis required by the shared persistence/capture API.Validation order
createSnapshotResult(...)must validate throughSnapshotResultSchemabefore any artifact persistence happens.Session identity guard
captureSnapshotResult(...)accepts optionalexpectedSessionId.snapshot.sessionId === expectedSessionIdbefore result/persistence work.sessionId.manifest.sessionId; replay setup already asserts derived session id and manifest id alignment, and passing it keeps the guard symmetric across live/offline paths.Domain language captured during grilling
CONTEXT.mdnow distinguishes Semantic Snapshot, Snapshot Result, Snapshot Artifact, and Snapshot Capture.Implementation steps
1. Add shared snapshot capture module
Create
src/snapshot/capture.ts.Implementation details:
src/protocol/messages.tsandsrc/renderer/types.ts.SnapshotResultSchemafromsrc/protocol/schemas.ts.ERROR_CODESandmakeCliErrorfromsrc/protocol/errors.tsso shared validation failures preserve current protocol-error semantics.appendArtifact,createArtifactEntryfromsrc/storage/artifactManifest.tsartifactPath,ensureArtifactsDir,snapshotFilenamefromsrc/storage/artifactPaths.tswriteTextFileAtomicfromsrc/storage/manifests.tsinvariantfromsrc/util/assert.tsfor defensive checks.createSnapshotResult(snapshot, format)should preserve current behavior exactly:format === 'structured', return{ format: 'structured', ...snapshot }after schema validation.format === 'text', join(snapshot.scrollbackLines ?? [])followed bysnapshot.visibleLines, mapping each line to.textand joining with\n.SnapshotResultSchema.safeParse(...)before returning.makeCliError(ERROR_CODES.PROTOCOL_ERROR, ...)with schema issues when validation fails, instead of replacing protocol errors with a generic invariant failure.persistSnapshotArtifact(...)should preserve current behavior exactly:rendererBackend.length > 0.result.format === formatand core result fields matchsnapshot(sessionId,capturedAtSeq,cols,rows,cursorRow,cursorCol). This prevents future callers/tests from writing artifact JSON for one result while deriving metadata from another snapshot.snapshotFilename(snapshot.capturedAtSeq, format).${JSON.stringify(result, null, 2)}\nto the artifact path.createArtifactEntry({ kind: 'snapshot', filename, sessionId: snapshot.sessionId, capturedAtSeq: snapshot.capturedAtSeq, metadata: ... }).formatrendererBackendcolsrowscursorRowcursorColscrollbackLineCountonly whensnapshot.scrollbackLines !== undefinedcaptureSnapshotResult(...)should:expectedSessionIdwhen provided.createSnapshotResult(...).persistSnapshotArtifact(...)with the validated result.2. Refactor CLI offline replay caller
Update
src/cli/commands/snapshot.ts:createSnapshotResult,persistSnapshotArtifact, and now-unused storage/artifact imports.runOfflineSnapshot(...), destructuremanifestfromwithOfflineReplayRenderer(...); afterbackend.snapshot({ includeScrollback, includeCells }), call:runRpcSnapshot(...)response parsing because RPC responses are still external protocol data.formatSnapshotLines(...).3. Refactor live host snapshot RPC caller
Update
src/host/hostMain.ts:snapshotRPC handler.backend.snapshot({ includeScrollback, includeCells })unchanged.src/host/hostMain.tsstill needs artifact/storage helpers for screenshot/export paths.4. Add shared module unit tests
Add
test/unit/snapshot/capture.test.ts.Test responsibilities:
createSnapshotResult(...)returns structured results without changing existing shape.createSnapshotResult(...)returns text results with scrollback lines prepended before visible lines.includeCellsbehavior is preserved by retainingsnapshot.cellsin structured results and artifacts when present.expectedSessionIdmismatch.persistSnapshotArtifact(...)writessnapshot-<seq>-<format>.jsoncontent exactly equal to the returned/public result JSON plus trailing newline.rendererBackend, dimensions, cursor coordinates, and optionalscrollbackLineCount.scrollbackLineCountbecause metadata comes from the sourceSemanticSnapshot, not only from structured result fields.rendererBackendis required and non-empty.format/core snapshot-result fields before writing.Mock storage/artifact helper dependencies in this test where needed so exact calls can be asserted once at the shared interface boundary.
5. Adjust caller tests without weakening integration coverage
Update
test/unit/commands/snapshot.test.ts:includeScrollbackandincludeCellsoptions passed to live RPC and offline renderertest/unit/snapshot/capture.test.ts.captureSnapshotResult(...)for offline replay routing tests so command tests assert the shared interface is invoked withsessionDir,format,snapshot,rendererBackend, andexpectedSessionId. If that adds fragility, keep one command test using the real shared capture with mocked storage helpers and let shared module tests own the exhaustive metadata matrix.Update
test/integration/host-renderer-rpc.test.ts:6. Preserve public contracts and out-of-scope boundaries
Do not change:
SnapshotParamsSchemaorSnapshotResultSchemapublic shapes.Acceptance criteria
snapshot-<seq>-<format>.json.kind,filename,sessionId,capturedAtSeq, renderer backend metadata, dimensions, cursor coordinates, and optionalscrollbackLineCountbehavior.includeCells: truestill requests renderer cell data in both caller paths and preserves cells in structured snapshot results/artifacts.includeCellsremains omitted from structured results by default when renderer snapshots omit cells.SemanticSnapshot.sessionIddiffers from the expected session id fails fast before persistence.Validation plan
Run the narrowest useful checks first, then broaden as needed:
If touched code or failures indicate broader risk, run:
npm run test npm run buildBefore committing or opening a PR, run the project-level gate. Prefer the project-standard
misetask when available:If
miseis unavailable, run the npm fallback:Dogfooding plan
Use an isolated absolute home so no real sessions are touched:
Run one live-host snapshot flow and one offline-replay snapshot flow from the source tree:
Review artifacts:
Locate both session directories under
$AGENT_TTY_HOME/sessions/$LIVE_SESSION_IDand$AGENT_TTY_HOME/sessions/$OFFLINE_SESSION_ID.Confirm live and offline snapshot artifact files use
snapshot-<seq>-structured.jsonandsnapshot-<seq>-text.jsonnames.Confirm each artifact JSON equals the corresponding emitted
.resultpayload fromLIVE_STRUCTURED_JSON,LIVE_TEXT_JSON,OFFLINE_STRUCTURED_JSON, andOFFLINE_TEXT_JSON.Confirm each
artifacts.jsoncontains snapshot entries withrendererBackend, dimensions, cursor coordinates, andscrollbackLineCountonly for captures that included scrollback.Capture screenshot PNG artifacts and WebM replay artifacts as reviewer-facing proof, even though snapshot JSON equality remains the primary behavior check.
Attach at least one generated screenshot PNG with
attach_filein the implementation report if visual proof is requested/available; include WebM artifact paths because the attachment tool may not support video.Because this issue is a refactor of JSON result/artifact construction and not a rendered-output change, JSON artifact equality and automated tests are the primary behavior proof; screenshots and WebM recordings are included as reviewable dogfood evidence rather than as indicators of changed visual-rendering behavior.
Risks and mitigations
Risk: introducing a circular import between protocol, renderer, storage, and snapshot modules.
src/snapshot/capture.tsdependency direction one-way into protocol/renderer/storage/util only; do not import CLI or host code.Risk: caller tests become too mocked and miss integration regressions.
test/integration/host-renderer-rpc.test.tsartifact equality and manifest smoke assertions.Risk: stricter required
rendererBackendbreaks mocks.RendererBackendcontract; failures here are desirable.Risk: validation-before-persistence changes failure behavior.
Advisor review incorporated
The plan was reviewed with the advisor and updated to include:
expectedSessionId: manifest.sessionId,ERROR_CODES.PROTOCOL_ERRORsemantics for shared result validation failures,ADR decision
No ADR is currently warranted. The decisions are local, easy to reverse, and unsurprising: extract duplicated snapshot result/artifact behavior into a shared snapshot-specific module while preserving the public contract.
Generated with
mux• Model:openai:gpt-5.5• Thinking:xhigh