feat(chat): add reusable durable plan and interaction workflows#213
Conversation
tangletools
left a comment
There was a problem hiding this comment.
🟡 Value Audit — sound-with-nits
| Verdict | sound-with-nits |
| Concerns | 2 (2 weak-concern) |
| Heuristic | 0.0s |
| Duplication | 0.0s |
| Interrogation | 203.9s (2 bridge agents) |
| Total | 203.9s |
💰 Value — sound
Adds a browser-safe /plans chat projection + durable interaction-answer codecs so plan cards and accepted answers survive reload; mirrors the existing /interactions codec and /chat-routes beforeTurn seam — fits the codebase grain.
- What it does: Introduces a new additive /plans subpath (src/plans/index.ts:1-148) that parses the peer sandbox SDK's plan.submitted events into a ChatPlan union, persists them as type:'plan' transcript parts, and exposes key/turn-id/monotonic-status helpers. Wires the sandbox chat producer to capture plan.submitted events durably (src/chat-routes/sandbox-producer.ts:236-244), teaches chat-store parts to project
- Goals it achieves: Plan cards and accepted interaction answers were lost on page reload — the shell persisted only the ask, not the answer, and dropped plan.submitted events entirely. After merge: (1) plan cards render and restore from persisted parts just like interaction parts; (2) accepted interaction selections are strictly validated, persisted, and replayed; (3) products get a single pre-unblock persistence hoo
- Assessment: Good change, in the grain of the codebase. It hits every shell invariant in AGENTS.md: (a) additive subpath wired in all four places — tsup.config.ts, package.json exports, src/index.ts barrel, AGENTS.md module map row 32; (b) substrate-free — /plans imports nothing from @tangle-network/sandbox, structurally byte-matching the SDK's plan shape (the same approach /interactions takes per AGENTS.md mo
- Better / existing approach: none — this is the right approach. I checked: (1) src/missions/plan-parse.ts is a different thing — it parses the agent-authored :::mission text block for the missions orchestration feature, not the SDK's durable plan.submitted events; not reusable here. (2) @tangle-network/sandbox owns plan lifecycle/decisions and is the authoritative source, but importing it into /plans would violate the substra
- Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 2
- Bridge warning: opencode/kimi-for-coding/k2p7: bridge stream ended without value-audit content
🎯 Usefulness — sound-with-nits
A textbook contribute-down of gtm-agent's working plan + interaction-answers persistence into the shared chat shell, with the imminent consumer (gtm-agent PR #591) already running on a local byte-equivalent copy.
- Integration: Fully wired and reachable.
createSandboxChatProducerconsumesplan.submittedevents at src/chat-routes/sandbox-producer.ts:236, persists them viaplanToPersistedPart, and yields them on the live stream.createInteractionAnswerRouteexposes the newbeforeAnswerseam at src/interactions/route.ts:151 — a product opts in by passing the option, no other change.ChatPlanPartis added to the - Fit with existing patterns: Matches the codebase's grain precisely. The shell's
/plansmodule is a structural mirror of the consumer'schat-plans.ts(ChatPlan union, parsePlanSubmittedEvent, planToPersistedPart, persistedPartToPlan, planPartKey, canTransitionPlanStatus — all one-for-one). It composes cleanly into the existing/streamnormalizer (plan key + merge with monotonic status, parallel to the interaction-status - Real-world viability: Robust on the non-happy paths.
parsePlanSubmittedEventaccepts both the direct (data.plan) and session-enveloped (properties.plan) shapes and fails loud with a typed error on malformed payloads (the producer drops + logs).canTransitionPlanStatusis monotonic (only pending → terminal), andmergePersistedPartat src/stream/stream-normalizer.ts:315 enforces it on every overlay so a stale r - Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 1
💰 Value Audit
🟡 stampInteractionAnswers is exported with no in-shell caller [maintenance] ``
src/interactions/contract.ts:325 adds stampInteractionAnswers, which is referenced only by tests/interactions-contract.test.ts:50 inside this repo — no internal call site. It is a pure-mechanism helper for the consumer (gtm-agent#591) to rehydrate a transcript from its own store. Justified by the PR's explicit goal of moving that mechanism out of the GTM repo, but it is speculative API surface until the reference consumer lands — worth flagging only so a reviewer knows why an apparently-unused e
🎯 Usefulness Audit
🟡 InteractionAnswers narrows against the wire validator and the in-flight consumer [ergonomics] ``
src/interactions/route.ts:69 validates POST
datafields asInteractionData(string|number|boolean|string[]), but src/interactions/contract.ts:73 declaresInteractionAnswers = Record<string, string[]>for the PERSISTED shape, andinteractionToPersistedPart/stampInteractionAnswersenforce the strict shape viaparseInteractionAnswers. The in-flight consumer (gtm-agent#591 src/lib/chat-interactions.ts:26) deliberately keeps a looserparseInteractionAnswersthat accepts primitives, and
What this audit checks
It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.
| Pass | What it asks |
|---|---|
| Heuristic | Vague title? Whitespace-only or cruft-bearing diff? (content signals only) |
| Duplication | Do added function/class names already exist elsewhere in the repo? |
| Value Audit | What does it do? What goal does it achieve? Is it good? Better architecture or already-exists? |
| Usefulness Audit | Does it integrate and fit? Will it hold up in real use and actually get used? |
Findings are concerns, not blocks — the human reviewer decides what to do with them.
✅ No Blockers —
|
tangletools
left a comment
There was a problem hiding this comment.
✅ Approved — 21 non-blocking findings — 3929aed8
Full multi-shot audit completed 8/8 planned shots over 18 changed files. Global verifier still owns final merge decision.
Full immutable report for this review: trace
Summary comment for this run: full summary
tangletools · 2026-07-21T06:39:45Z · immutable trace
tangletools
left a comment
There was a problem hiding this comment.
🟡 Value Audit — sound-with-nits
| Verdict | sound-with-nits |
| Concerns | 4 (4 weak-concern) |
| Heuristic | 0.0s |
| Duplication | 0.0s |
| Interrogation | 547.2s (2 bridge agents) |
| Total | 547.2s |
💰 Value — sound-with-nits
Adds a reusable server workflow + browser cards that make plan decisions and interaction answers survive crashes/reloads; well-seamed and grain-aligned, with two minor architectural nits worth a human glance.
- What it does: Introduces three things. (1) A new ./durable-chat subpath: a server-side DurablePlanStore port with CAS claims for plan commands, plan effects, and answer intents; createDurablePlanRoutes() (GET current + POST decide with authority reconciliation and effect retry); createDurableInteractionSettlement() (prepare/ack/finalize/abort lifecycle); an InMemoryDurableChatStateStore reference adapter; and b
- Goals it achieves: Make plan decisions and accepted interaction answers survive worker restarts, browser reloads, ambiguous retries, and reconnects WITHOUT each product re-implementing the state machine. The product supplies storage + auth + sidecar connection + idempotent afterDecision effect; the shell owns the protocol (idempotency keys, CAS, receipt shapes, prepare→ack→finalize ordering, tombstones). Eliminates
- Assessment: Fits the codebase grain on every invariant that matters: new capability = new additive subpath (rule 5 ✓), structural port with no product imports (rule 2 ✓), /plans is substrate-free (rule 4 ✓), server-only logic stays out of browser bundles, and the /interactions seam is extended additively (DurableInteractionRoutePersistence is opt-in). The CAS-claim pattern echoes /missions but uses typed per-
- Better / existing approach: No materially better architecture found; this is the right shape. Searched for: (a) overlap with /missions' MissionStorePort — same CAS idea, different and well-typed-for-its-domain port, not a true duplicate; (b) overlap with /billing's KeyProvisioner/WorkspaceKeyStore/KeyCrypto — different concern (no crypto/storage seams here); (c) overlap with the sandbox SDK's durable primitives — AGENTS rule
- Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 2
- Bridge warning: opencode/kimi-for-coding/k2p7: bridge stream ended without value-audit content
🎯 Usefulness — sound-with-nits
A coherent, additive durable plan/interaction workflow layer that consolidates three fleet forks (gtm/legal/tax), fits the structural-port grain, and is reachable through every documented seam — with one docs gap where the canonical example wires interaction projection but not plan projection.
- Integration: Fully reachable.
/durable-chatand/plansare in package.json exports and re-exported fromsrc/index.ts:30-31. The sandbox producer accepts an optionalinteractionProjection(src/chat-routes/sandbox-producer.ts:60,233,272,307),/interactionsaccepts an optionaldurablepersistence seam (src/interactions/route.ts:183), andChatMessages.durableCardsrenders the new cards from persi - Fit with existing patterns: Matches the AGENTS.md 'engine=peerDep, app-shell=compose' rule exactly.
DurableChatScopeis a branded opaque type created only after auth (src/durable-chat/types.ts:13-18), the store is a structural port with CAS-shaped claim/finalize verbs, andInMemoryDurableChatStateStoreis explicitly marked non-production (src/durable-chat/memory.ts:17-21). This is the same shape as/billing, `/tang - Real-world viability: Handles the realistic edge cases: tombstone-before-ask so a late duplicate ask cannot reopen a cancelled card (
src/durable-chat/interactions.ts:61-68), competing-decision detection across the scope (src/durable-chat/memory.ts:111-118), stale plan revision rejection (src/durable-chat/plan-routes.ts:243-245), reconcile-on-ambiguous-retry when the sidecar says the ask is gone (`src/interactions - Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 1
💰 Value Audit
🟡 Two parallel CAS-store ports now exist (/missions + /durable-chat) with no shared primitive [duplication] ``
/missions/service.ts:145 (MissionStorePort with guarded update + conflict) and /durable-chat/types.ts:116 (DurablePlanStore with claimPlanCommand/claimAnswerIntent/claimPlanEffect + in-memory reference at memory.ts:22) implement the same CAS-with-conflict-detection idea twice. The per-resource typed claims in /durable-chat are more ergonomic for plan/answer semantics than a generic update would be, so this is not a refactor-now situation — but if a fourth or fifth such port appears, the typed-cl
🟡 1500+ LOC of new shell ships with no in-repo consumer; verification deferred to GTM PR #591 [maintenance] ``
grep shows createDurablePlanRoutes / createDurableInteractionRoutePersistence / DurablePlanStore / withDurableChatProjection / createDurableChatEventProjection appear only in their own module + tests, never consumed inside this repo. AGENTS.md 'When you add a module' step 5 says 'prove it on a reference consumer — it stays green.' examples/chat-app.md:177-210 is a code snippet in markdown, not a working consumer. Shell-first is an accepted pattern in this codebase and the prior fix (3929aed) alr
🟡 Two overlapping projection paths in /chat-routes (inline adapter vs generic wrapper) [better-architecture] ``
createSandboxChatProducer accepts options.interactionProjection (src/chat-routes/sandbox-producer.ts:57-63, fires only for interaction events) AND withDurableChatProjection wraps any producer to observe+materialize arbitrary events (src/chat-routes/durable-projection.ts:13-40, what createDurableChatEventProjection feeds). A consumer that wants durable plans AND interaction projection today must wire BOTH, and the example at examples/chat-app.md:188-191 only shows the inline adapter (no plan proj
🎯 Usefulness Audit
🟡 Canonical example wires interaction projection but not plan projection — plan decide will 503 on a literal follower [ergonomics] ``
examples/chat-app.md:186-191wirescreateDurableInteractionProjectionAdapterintocreateSandboxChatProducer({ interactionProjection }). But the sandbox producer'sinteractionProjectionoption only projects interaction events —src/chat-routes/sandbox-producer.ts:277-286showsplan.submittedonly callsrecordPersistedPart(planToPersistedPart(...))(chat-store parts), with no call to seed the durable plan store. SocreateDurablePlanRoutes.decide()will hit its own guard at `src/dur
What this audit checks
It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.
| Pass | What it asks |
|---|---|
| Heuristic | Vague title? Whitespace-only or cruft-bearing diff? (content signals only) |
| Duplication | Do added function/class names already exist elsewhere in the repo? |
| Value Audit | What does it do? What goal does it achieve? Is it good? Better architecture or already-exists? |
| Usefulness Audit | Does it integrate and fit? Will it hold up in real use and actually get used? |
Findings are concerns, not blocks — the human reviewer decides what to do with them.
|
Addressed the actionable findings from review 4742392773: removed the overlapping interaction-only projection path; plans and interactions now share the generic durable projection wrapper; updated the canonical sandbox chat example to install both projections; added an executable producer-to-projection-to-plan-decision composition test; coalesced same-tick plan decisions; and normalized replayed responses as idempotent. The CAS helper duplication remains intentionally deferred, as suggested in the review, until another production store port establishes the right shared primitive. Verification: focused durable-chat tests, typecheck, package build, and CI build all pass. The unrelated pre-existing shell-profile/base64 failures in src/sandbox/index.test.ts remain unchanged. |
✅ No Blockers —
|
tangletools
left a comment
There was a problem hiding this comment.
✅ Approved — 41 non-blocking findings — 5f1b8613
Full multi-shot audit completed 8/8 planned shots over 46 changed files. Global verifier still owns final merge decision.
Full immutable report for this review: trace
Summary comment for this run: full summary
tangletools · 2026-07-21T08:31:19Z · immutable trace
Summary
./durable-chatserver workflow for scoped plan projections, CAS decisions, stable follow-up receipts, retryable product effects, interaction tombstones, and crash-recoverable answer settlementChatMessagesThis expands the original persistence fix into the reusable application-shell design tracked by #212. Products now provide authorization, production storage, Sandbox/sidecar connections, idempotent effects, and follow-up attachment; they no longer need to reimplement the lifecycle state machines. GTM PR tangle-network/gtm-agent#591 remains the reference consumer for the follow-up migration.
Correctness details
planId+revision, never by content equalityValidation
pnpm typecheckNODE_OPTIONS=--max-old-space-size=8192 pnpm build(ESM and declarations)pnpm test: 2,596 passed; 3 pre-existing macOS deferred-profile retry tests exceeded the 5-second timeout, matching the branch baselinegit diff --checkCloses #212