Skip to content

plan: A4 compaction — phase 2 — checkpoint persist seam + resume (source #552) #949

Description

@btipling

Plan header

Field Value
Status IMPLEMENTED — PR #954 (open, merge-ready; not merged)
Date 2026-09-05
Type phase 2
Parent #947#947
Source issue #552#552 (A4 compaction engine)
Layers Vercel backend (persist seam + session store + route read)
Reusability impact none
Production mutate? no
Cloud ops path N/A
Living docs N/A this phase (parent Phase 5 owns docs)

Review notes (2026-09-05)

# Axis Score Notes
1 Correctness 5 All four baseline claims verified @ main (writeSegment + model_messages_write_failed in lib/agent/turnPersistSeam.ts; isObjectIdBoundTo read guard in app/api/turns/route.ts; RESERVED_META_KEYS + copy-forward helpers in lib/sessions/sessionStore.ts; route seed chain). Fallback order locked (checkpoint → modelMessages → legacy promptHistory).
2 Performance 4 One extra bounded Blob read on the route pre-start; object cap MODEL_MSG_SEED_MAX_BYTES + 256 KiB (2.25 MiB — composes with the Phase-1 tail rail); no polling/loop additions. Persist seam writes only when a compaction ran.
3 Architecture 5 Vercel-backend-only; checkpoint server-side; reuses the landed sibling pattern (modelMessagesPointer/freshnessReminderPointer/workingNotes); no new DI seam. Worker-overlay seam (WORKER_META_KEYS in lib/agent/workerMetaOverlay.ts) accepts patch.compactionPointer via sanitizeWorkerKeyValue.
4 Testing 4 Locked DoD matrix (1,2,4,5,6): persist, route preference, copy-forward, confused-deputy, gates. Case 3 (fallback chain) over-and-above.
5 Cloud ops N/A Header + DoD explicitly lock "no Production mutate" — correct.
6 Living docs N/A This phase defers docs to parent Phase 4 (#951) — consistent with the parent's post-#944 re-split; parent's own Living-docs row was superseded by the re-plan.
7 Caps 5 Caps table present: 1 NEW cap (COMPACTION_CHECKPOINT_MAX_BYTES = MODEL_MSG_SEED_MAX_BYTES + 256 KiB / 2.25 MiB — raised on PR #954 adversarial from the original 1 MiB so a Phase-1-legal findCompactionCut tail can persist; NEW unmerged, no existing-on-main ceiling changed); HARNESS_SESSION_MAX_META_BYTES unchanged. No existing cap raised/lowered → no human gate.
8 Parent adherence 5 Phase number correct vs parent map; scope ⊆ parent; no lock re-open; refinements table declares the lock-tightenings (seed fallback order, no-new-DI-seam).
9 Layer/UI N/A No DOM/Wasm surface; Wasm paints nothing new; DOM host unchanged. Forbidden wiring locked.

Findings (all informational; none block):

Verdict: HANDOFF-READY — no Blockers; all Majors cleared by baseline verification; scores ≥4 on all required axes; parent adherence clean; cloud ops N/A (no Production mutate); living docs correctly N/A; caps table complete, no human gate.

Intent lock

In scope: persist the checkpoint as its own session-bound Blob object + reserved meta key compactionPointer (worker-authored, copy-forward on envelope PUT), and make POST /api/turns prefer the checkpoint on seed (read, re-validate, re-pair, fall back to modelMessagesPointer).

Out of scope: the compaction trigger decision and the summarizer step (Phase 3). This phase only lands the write-once/read carrier so a persisted checkpoint (produced by Phase 3, or by a prior session) seeds correctly.

Forbidden: dual DOM chat · secrets in Wasm · laptop-only Production ops. The checkpoint stays server-side; Wasm paints nothing new.

Depends on

Phase 1 merged (needs COMPACTION_* caps + renderSummaryRow / checkpoint type from lib/agent/compaction.ts).

Current baseline (live code @ main)

Claim Path / symbol Notes
writeSegment + fail-closed model_messages_write_failed sibling pattern lib/agent/turnPersistSeam.ts verified @ main — mirror it
Confused-deputy isObjectIdBoundTo(oid, scope) on read app/api/turns/route.ts, lib/sessions/blobStore.ts verified @ main
RESERVED_META_KEYS + copy-forward on envelope PUT lib/sessions/sessionStore.ts (copyForwardModelMessagesPointer, copyForwardFreshnessReminderPointer) verified @ main
Route prefers pointer → rebuilds → seeds priorMessages app/api/turns/route.ts verified @ main

Design

  • lib/sessions/sessionStore.ts — add compactionPointer to RESERVED_META_KEYS and a copyForwardCompactionPointer helper inside upsertEnvelope (worker-authored sibling, same exception class as modelMessagesPointer / workingNotes / freshnessReminderPointer). Host cloudMetaFor never emits the key; an explicit incoming value (worker overlay) wins; Clear is DELETE.
  • lib/agent/turnPersistSeam.ts — on the terminal persist, write the checkpoint object (when a compaction ran this turn) via writeSegment with maxBytes: COMPACTION_CHECKPOINT_MAX_BYTES, then patch.compactionPointer = <objectId> on the worker meta overlay. Fail-closed compaction_write_failed. (Phase 3 supplies the in-turn compaction result; the persist seam accepts it on the derivePersistFold input.)
  • app/api/turns/route.ts — on seed, prefer envelope.meta?.compactionPointer when present and isObjectIdBoundTo bound; read, JSON.parse, buildModelMessages-rebuild + re-pair the checkpoint's retainedTail, and seed priorMessages = [renderSummaryRow(...), ...tail]. Fall back to meta.modelMessagesPointer (today's behavior) when absent/malformed/unbound; when both are missing, fall back to the legacy promptHistory sidecar. The route reads via services.createBlobTranscriptStore() exactly as the plan: structured truncated tool_result on the wire (A1, source #549) #936 projection read does today — no new DI seam.

Caps table

Cap / ceiling Value Rationale Code location
NEW COMPACTION_CHECKPOINT_MAX_BYTES MODEL_MSG_SEED_MAX_BYTES + 256 KiB (2.25 MiB) The checkpoint Blob object ({summary, filesTouched, retainedTail}) is its own session-bound object, never envelope meta (only the ≤512-char pointer id rides meta). Must compose with the Phase-1 findCompactionCut tail rail (MODEL_MSG_SEED_MAX_BYTES = 2 MiB): a legal cut’s retainedTail may serialize to that size; 256 KiB slack covers summary (≤8 000 chars + honesty suffixes), filesTouched (≤256 short paths), and object keys. Raised on PR #954 adversarial from 1 MiB so a Phase-1-legal checkpoint can persist — NEW unmerged cap, not a change to an existing-on-main ceiling. Still well under the 8 MiB model-messages object. An oversized write fail-closes (compaction_write_failed), never a truncation lie. lib/sessionCloudCaps.ts
HARNESS_SESSION_MAX_META_BYTESunchanged 1 MiB compactionPointer is a ≤512-char id inside the existing whole-meta budget. No change lib/sessionCloudCaps.ts (existing)

No existing cap changed → no human gate.

Implementation order

  1. lib/sessions/sessionStore.tscompactionPointer reserved key + copyForwardCompactionPointer.
  2. lib/agent/turnPersistSeam.ts — checkpoint writeSegment + patch.compactionPointer.
  3. app/api/turns/route.ts — prefer-checkpoint seed with fallback chain (checkpoint → modelMessages → legacy sidecar).
  4. Unit tests.

Testing

# Case Layer Type Command / method
1 Persist writes a bound checkpoint Blob + compactionPointer; fail-closed compaction_write_failed backend unit vitest run lib/agent/turnPersistSeam.test.ts
2 Route prefers compactionPointer over modelMessagesPointer and seeds [summaryRow, ...retainedTail] backend unit vitest run app/api/turns (route test file)
3 Malformed/unbound checkpoint → fall back to modelMessagesPointer; both missing → legacy promptHistory sidecar backend unit vitest run app/api/turns
4 Envelope PUT copy-forward: host flatten PUT omitting compactionPointer does not delete the checkpoint (copy-forward on the LWW read) backend unit vitest run lib/sessions/sessionStore.test.ts
5 Confused-deputy: a checkpoint id not bound to the session scope is rejected on read backend unit vitest run app/api/turns
6 Gates all gate npm run typecheck, npm test

Minimum locked for DoD: 1, 2, 4, 5, 6.

Definition of done

  • Maps to parent checklist: "Checkpoint persisted as its own session-bound Blob + meta.compactionPointer; next turn seeds checkpoint + retained tail"
  • Cloud ops: N/A — no Production mutate
  • Living docs: N/A this phase (parent Phase 5 owns docs)
  • No existing cap value changed; no human gate

Corrections / refinements vs parent

Topic Parent said This phase locks
Seed preference Parent: "prefer checkpoint when present" Exact fallback order locked: checkpoint → modelMessages → legacy promptHistory sidecar
Route DI Parent: "reads the checkpoint" Reuses services.createBlobTranscriptStore()no new DI seam, mirroring the #936 projection read

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions