feat(intercept): reflect edited requests in the dashboard (+ forensic original, edited badge)#55
Merged
Merged
Conversation
…original + edited badge The dashboard turn detail showed the pre-edit prompt because _req.json is written at receipt time (server/index.js) and never rewritten after an intercept edit. The edited body was correctly sent upstream (verified live: editing to "say BANANA" produced "BANANA"), but the persisted record — and thus the dashboard and any consumer that reads _req.json directly — stayed stale. This persists the request as-sent: after the forward-time body mutations, _req.json is rewritten full-format from the edited body, chained after the original write (never Promise.all, so the original cannot land last and restore stale bytes). The pre-edit body is preserved in a non-authoritative _req.received.json sidecar that never participates in delta reconstruction. A server-authoritative edit summary (buildEditSummary, reused for the existing SSE injection notice) is persisted and surfaced. The delta chain is re-anchored from a clone of the edited messages on success and cleared on any failure, so the next turn re-anchors full rather than splicing against an unrecoverable base (split-brain mitigation). sessionLastReq stays private to index.js via an injected recorder. The dashboard renders an EDITED badge plus the server summary, with a collapsible "original before edit" shown only on the message that actually changed. Anthropic-only. Codex (OpenAI Responses API) intercept editing is not yet developed and is out of scope here. Verified: a differential read-contract test (fail-on-old -> pass-on-new), 792/792 unit+integration tests, a real running-server endpoint smoke, and a full real-Chrome DOM/lazy-load check of the badge and per-message toggle.
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.
繁中摘要
修掉一個 dashboard 顯示 bug,並補上鑑識用原文與「已編輯」標記。
say BANANA→ Claude 回BANANA),但_req.json在收到當下就寫死、之後從不更新,所以 dashboard(及任何直接讀_req.json的消費者)都看到舊的。_req.json以 as-sent(編輯後、full 格式)重寫,鏈接在原始寫入之後(不用Promise.all,避免原始寫入後落地蓋回舊值)。原文保留在非權威的_req.received.jsonsidecar,絕不參與 delta 重建。buildEditSummary(與既有 SSE 注入提示共用同一份),持久化進 index 並呈現。dashboard 顯示✎ EDITED標記,「原文對照」只在真的有改的那則訊息出現。sessionLastReq維持index.jsprivate,經注入 recorder 操作。Problem
The dashboard turn detail showed the pre-edit prompt after an intercept edit.
_req.jsonis written at receipt time (server/index.js) — before the intercept hold and the user's edit — andforwardRequestonly ever rewrites_res.json, never_req.json. The edited body was sent upstream correctly (confirmed live: editing the last message tosay BANANAproducedBANANA), but the persisted request, and thereforeloadEntryReqResand the dashboard it serves, stayed stale. The Claude Code CLI also shows the original because it renders its own local input, independent of the proxy rewrite.Solution
Persist the request as-sent, keep the original for forensics, and surface the edit:
_req.json= as-sent. AfterstripInjectedStats/applyModelPrefixmutateparsedBody,persistEditedRequest(ctx)rewrites_req.jsonfull-format from the edited body (via the purebuildEditedReqRecord). The rewrite is chained ontoctx.reqWritePromise(prior.then(rewrite, rewrite)), neverPromise.all, so the receipt-time write cannot land last and restore stale bytes.loadEntryReqResalready awaitsentry._writePromise, so the read path observes the rewrite._req.received.jsonsidecar. The pre-edit body is written before_req.jsonis overwritten. It is non-authoritative: never a delta anchor, never aprevIdsplice source, never a token/cost source — purely the "original before edit" view.buildInterceptModSummaryis renamedbuildEditSummaryand reused for both the existing SSE injection notice and the persistedentry.editSummary. The client is a dumb renderer — no client-side diffing, so restored historical entries show the same summary they had when written.deltaCount: 0); on any persistence failure the anchor is cleared so the next turn re-anchors full instead of splicing against an unrecoverable base (split-brain mitigation).sessionLastReqstays private toindex.js;forward.jsmutates it only through an injectedsetSessionAnchorRecorder.sysHash/toolsHashare recomputed andctxis updated synchronously at the forward gate so the entry/index reference the as-sent content; the sharedsys_*/tools_*files are written (compared against the original hash) inside the same chained promise.Architecture
UI
✎ EDITEDbadge plus the server summary render in the request detail (turn-level signal).public/messages.js(renderEditedBanner) + a few compact styles; the dashboard consumes the server-provided fields.Verification
test/intercept-edit-reflect.test.js): the read-contract check is a same-check that fails on the pre-fix code and passes on the fix (the edited flag / original / summary are absent before). Write-seam and split-brain tests coverpersistEditedRequestand the anchor-clear path.:5602returnsreq.edited, the as-sent body,req.original, andreq.editSummarythrough the realrestore → loadEntryReqRes → /_api/entry/:idpath.buildMergedSteps→renderStepDetailHtml→renderEditedBanner).Scope & related
public/intercept-ui.jsis still Anthropic-shaped and Codex's main traffic is WebSocket, so it needs its own design. Future parity work under the multi-provider direction in Discuss provider-agnostic abstraction to support codex / gemini..etc #20.Trade-offs
_req.json, received sidecar, rewritten_req.json) and is always persisted full-format — extra I/O and lost delta compression for that single turn, accepted because edits are rare and an edit resets the chain anchor anyway.buildInterceptModSummary→buildEditSummaryrename (existing test updated).