fix(chat-routes): redact persisted parts, surface run-failure, close manifest hole (audit P1)#201
Merged
Merged
Conversation
…manifest hole (audit P1) - transformFinalText now applies to every persisted assistant TEXT part, not just the scalar finalText — closes a PII-redaction-at-rest leak where message.parts kept raw PII the /redact transform had scrubbed from the scalar. - onTurnComplete receives failed/failureReason derived from a terminal error/session.run.failed event, so products skip billing an errored turn and render an error row instead of marking an empty turn complete. - browser-safe manifest is enumerated from package.json exports (react family + /lazy splits + pure-mechanism subpaths) instead of a top-level *-react readdir, covering web-react/terminal, design-canvas-react/engine, and every *-react/lazy — the #256/#372 client-bundle-crash blind spot. - mergePersistedPart tool branch overlays only DEFINED incoming fields and never downgrades a settled tool to running, so a later empty update no longer clobbers captured tool output/error. - lifecycle terminal hook is hoisted + idempotent and fires onTurnError on a synchronous pre-stream throw; withStreamHeartbeat clears its timer in finally so a rejected source never orphans a setTimeout. - vertical suite gains a turnLock + lifecycle + contextGate composition scenario.
tangletools
approved these changes
Jul 17, 2026
tangletools
left a comment
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — 1322af15
This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.
tangletools · auto-approval · reason: drewstone_author · 2026-07-17T17:40:43Z
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.
Confirmed audit findings in the assembled chat vertical, each with a test proving the fix. New seams are optional — #194/#200 back-compat preserved.
P1
transformFinalTextused to touch only the scalarfinalText;createSandboxChatProducer's persistedmessage.partsstreamed straight off the raw producer, so a legal product's redaction left raw PII in the stored text parts. The transform now runs over every persisted assistant TEXT part before persistence, consistent with the scalar. Test: a redactor forSSN 123— the persisted text PART is redacted, not justfinalText.error/session.run.failedevent (model 402, rate-limit) reachedhandleChatTurnas a clean completion, soonTurnCompletebilled an empty turn and marked it done.onTurnCompletenow receivesfailed: boolean+failureReason?, so products skip the deduct and render an error row. Test: a producer emitting a terminal error →onTurnCompletefires withfailed:true+ reason; a clean turn reportsfailed:false.*-reactreaddir, missing nested browser entries and reopening the #256/#372 client-bundle-crash class. It now enumerates every browser-intended entry frompackage.jsonexports(react family +/lazysplits + pure-mechanism subpaths), explicitly coveringweb-react/terminal,design-canvas-react/engine, and every*-react/lazy. All enumerated entries pass thenode:/agent-runtimeimport-graph walk. Test: enumeration + walk + a regression guard listing the nested entries.P2
mergePersistedParttool clobber. A normalized tool part always carriesoutput/errorkeys (undefined when uncaptured); a plain spread overwrote a completed tool's output on a later empty update. The tool branch now overlays only defined incoming fields and never downgrades a settled tool back torunning. Test: complete-then-empty-update keeps output; error-then-empty keeps the error message + status.tests/vertical/composition scenario wiringturnLock+lifecycle+contextGateover the fake-producer harness: acontextGatereject writes no assistant row and never runs the producer/lifecycle; theturnLockserializes a duplicate (refused 409, zero rows).P3
withStreamHeartbeatclears its pendingsetTimeoutinfinally, so a rejected source never orphans a timer that keeps the runtime alive.onTurnError(with lock release) even on a synchronous pre-stream throw — no dangling "started but unsettled" span. Test: a mocked engine throwing synchronously afteronTurnStart→['start','error']+ lock released.Docs
CLAUDE.md/AGENTS.md/chat-routesrow now lists the six seams (turnLock/contextGate/beforeTurn/lifecycle/heartbeat/onRawEvent) +transformFinalText+ run-failure surfacing;ARCHITECTURE.mdgains a chat-routes entry.createUploadRouteleft intact.Verification
Clean frozen-lockfile install →
pnpm typecheckclean →pnpm testgreen (2501 passing; the one build-gated scaffolder suite passes afterpnpm build) →NODE_OPTIONS=--max-old-space-size=8192 pnpm buildgreen. Publishes on merge — legal/tax consume the redaction + run-failure fixes next.