comisai 1.0.24 → 1.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/node_modules/@comis/agent/dist/bootstrap/sections/tool-descriptions.js +130 -10
- package/node_modules/@comis/agent/dist/bootstrap/sections/tooling-sections.d.ts +1 -1
- package/node_modules/@comis/agent/dist/bootstrap/sections/tooling-sections.js +9 -2
- package/node_modules/@comis/agent/dist/bridge/bridge-metrics.d.ts +8 -0
- package/node_modules/@comis/agent/dist/bridge/bridge-metrics.js +2 -0
- package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.d.ts +29 -0
- package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.js +242 -2
- package/node_modules/@comis/agent/dist/bridge/thinking-block-hash-invariant.d.ts +210 -0
- package/node_modules/@comis/agent/dist/bridge/thinking-block-hash-invariant.js +566 -0
- package/node_modules/@comis/agent/dist/context-engine/context-engine.js +8 -6
- package/node_modules/@comis/agent/dist/context-engine/signature-replay-scrubber.d.ts +51 -30
- package/node_modules/@comis/agent/dist/context-engine/signature-replay-scrubber.js +109 -36
- package/node_modules/@comis/agent/dist/executor/executor-context-engine-setup.js +5 -1
- package/node_modules/@comis/agent/dist/executor/executor-post-execution.js +22 -20
- package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.d.ts +2 -0
- package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.js +111 -15
- package/node_modules/@comis/agent/dist/executor/executor-response-filter.d.ts +20 -17
- package/node_modules/@comis/agent/dist/executor/executor-response-filter.js +132 -52
- package/node_modules/@comis/agent/dist/executor/executor-tool-assembly.js +16 -3
- package/node_modules/@comis/agent/dist/executor/model-retry.d.ts +14 -0
- package/node_modules/@comis/agent/dist/executor/model-retry.js +72 -1
- package/node_modules/@comis/agent/dist/executor/pi-executor.d.ts +3 -0
- package/node_modules/@comis/agent/dist/executor/pi-executor.js +68 -9
- package/node_modules/@comis/agent/dist/executor/post-batch-continuation.d.ts +82 -0
- package/node_modules/@comis/agent/dist/executor/post-batch-continuation.js +200 -0
- package/node_modules/@comis/agent/dist/executor/stream-wrappers/request-body-injector.js +1 -9
- package/node_modules/@comis/agent/dist/executor/tool-deferral.d.ts +37 -2
- package/node_modules/@comis/agent/dist/executor/tool-deferral.js +45 -3
- package/node_modules/@comis/agent/dist/executor/tool-parallelism.js +0 -1
- package/node_modules/@comis/agent/dist/executor/types.d.ts +11 -2
- package/node_modules/@comis/agent/dist/index.d.ts +3 -1
- package/node_modules/@comis/agent/dist/index.js +2 -0
- package/node_modules/@comis/agent/dist/model/last-known-model.d.ts +36 -0
- package/node_modules/@comis/agent/dist/model/last-known-model.js +49 -0
- package/node_modules/@comis/agent/dist/model/model-registry-adapter.d.ts +16 -4
- package/node_modules/@comis/agent/dist/model/model-registry-adapter.js +65 -21
- package/node_modules/@comis/agent/dist/planner/types.d.ts +0 -2
- package/node_modules/@comis/agent/dist/session/comis-session-manager.d.ts +10 -0
- package/node_modules/@comis/agent/dist/session/comis-session-manager.js +5 -0
- package/node_modules/@comis/agent/dist/spawn/pi-mono-adapters.js +7 -0
- package/node_modules/@comis/agent/package.json +1 -1
- package/node_modules/@comis/channels/package.json +1 -1
- package/node_modules/@comis/cli/dist/client/rpc-client.js +6 -1
- package/node_modules/@comis/cli/dist/commands/doctor.js +5 -3
- package/node_modules/@comis/cli/dist/commands/health.js +5 -2
- package/node_modules/@comis/cli/dist/wizard/json-output.js +7 -3
- package/node_modules/@comis/cli/dist/wizard/steps/11-daemon-start.js +130 -0
- package/node_modules/@comis/cli/package.json +1 -1
- package/node_modules/@comis/core/dist/bootstrap.js +5 -0
- package/node_modules/@comis/core/dist/config/env-layer.d.ts +31 -0
- package/node_modules/@comis/core/dist/config/env-layer.js +41 -0
- package/node_modules/@comis/core/dist/config/immutable-keys.d.ts +2 -2
- package/node_modules/@comis/core/dist/config/immutable-keys.js +8 -3
- package/node_modules/@comis/core/dist/config/layered.d.ts +9 -0
- package/node_modules/@comis/core/dist/config/layered.js +11 -0
- package/node_modules/@comis/core/dist/config/managed-sections.d.ts +43 -4
- package/node_modules/@comis/core/dist/config/managed-sections.js +100 -6
- package/node_modules/@comis/core/dist/config/schema-agent.d.ts +39 -0
- package/node_modules/@comis/core/dist/config/schema-agent.js +14 -0
- package/node_modules/@comis/core/dist/config/schema.d.ts +4 -0
- package/node_modules/@comis/core/dist/config/schema.js +14 -0
- package/node_modules/@comis/core/dist/domain/execution-graph.d.ts +1 -1
- package/node_modules/@comis/core/dist/event-bus/events-agent.d.ts +17 -2
- package/node_modules/@comis/core/dist/exports/config.d.ts +2 -2
- package/node_modules/@comis/core/dist/exports/config.js +1 -1
- package/node_modules/@comis/core/package.json +1 -1
- package/node_modules/@comis/daemon/dist/daemon.d.ts +22 -0
- package/node_modules/@comis/daemon/dist/daemon.js +45 -0
- package/node_modules/@comis/daemon/dist/rpc/agent-handlers.d.ts +5 -2
- package/node_modules/@comis/daemon/dist/rpc/agent-handlers.js +80 -1
- package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.d.ts +67 -0
- package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.js +139 -0
- package/node_modules/@comis/daemon/dist/rpc/model-handlers.d.ts +3 -0
- package/node_modules/@comis/daemon/dist/rpc/model-handlers.js +29 -5
- package/node_modules/@comis/daemon/dist/rpc/probe-provider-auth.d.ts +30 -0
- package/node_modules/@comis/daemon/dist/rpc/probe-provider-auth.js +59 -0
- package/node_modules/@comis/daemon/dist/rpc/provider-handlers.d.ts +37 -0
- package/node_modules/@comis/daemon/dist/rpc/provider-handlers.js +330 -0
- package/node_modules/@comis/daemon/dist/rpc/rpc-dispatch.js +18 -1
- package/node_modules/@comis/daemon/dist/setup-docker-restart-warn.d.ts +4 -0
- package/node_modules/@comis/daemon/dist/setup-docker-restart-warn.js +30 -0
- package/node_modules/@comis/daemon/dist/wiring/setup-agents.d.ts +3 -1
- package/node_modules/@comis/daemon/dist/wiring/setup-agents.js +28 -2
- package/node_modules/@comis/daemon/dist/wiring/setup-cross-session.js +1 -0
- package/node_modules/@comis/daemon/dist/wiring/setup-tools.js +7 -4
- package/node_modules/@comis/daemon/package.json +1 -1
- package/node_modules/@comis/gateway/package.json +1 -1
- package/node_modules/@comis/infra/dist/index.d.ts +1 -0
- package/node_modules/@comis/infra/dist/index.js +2 -0
- package/node_modules/@comis/infra/dist/runtime/is-docker.d.ts +1 -0
- package/node_modules/@comis/infra/dist/runtime/is-docker.js +25 -0
- package/node_modules/@comis/infra/package.json +1 -1
- package/node_modules/@comis/memory/package.json +1 -1
- package/node_modules/@comis/scheduler/package.json +1 -1
- package/node_modules/@comis/shared/package.json +1 -1
- package/node_modules/@comis/skills/dist/bridge/tool-metadata-registry.js +1 -3
- package/node_modules/@comis/skills/dist/builtin/platform/admin-manage-factory.js +24 -1
- package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.d.ts +53 -7
- package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.js +218 -24
- package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.d.ts +4 -1
- package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.js +16 -1
- package/node_modules/@comis/skills/dist/builtin/platform/index.d.ts +1 -1
- package/node_modules/@comis/skills/dist/builtin/platform/index.js +1 -1
- package/node_modules/@comis/skills/dist/builtin/platform/providers-manage-tool.d.ts +56 -0
- package/node_modules/@comis/skills/dist/builtin/platform/providers-manage-tool.js +203 -0
- package/node_modules/@comis/skills/dist/index.d.ts +1 -1
- package/node_modules/@comis/skills/dist/index.js +2 -2
- package/node_modules/@comis/skills/dist/policy/tool-policy.js +0 -1
- package/node_modules/@comis/skills/package.json +1 -1
- package/node_modules/@comis/web/dist/assets/{agent-detail-BG9MGWWj.js → agent-detail-DqL6Artv.js} +270 -270
- package/node_modules/@comis/web/dist/assets/agent-editor-CNM_h94Y.js +2173 -0
- package/node_modules/@comis/web/dist/assets/{agent-list-LHCJ4rw2.js → agent-list-Dbh-xD_F.js} +170 -170
- package/node_modules/@comis/web/dist/assets/{approvals-q9VH_IKr.js → approvals-C-K6hN2U.js} +13 -13
- package/node_modules/@comis/web/dist/assets/billing-view-C1DmtyzK.js +375 -0
- package/node_modules/@comis/web/dist/assets/{channel-detail-CaInesJM.js → channel-detail-CtCH22N1.js} +265 -265
- package/node_modules/@comis/web/dist/assets/channel-list-C7xXn-60.js +323 -0
- package/node_modules/@comis/web/dist/assets/{chat-console-CNmzl0JW.js → chat-console-C51pjFwk.js} +243 -246
- package/node_modules/@comis/web/dist/assets/{config-editor-DX4ITw6y.js → config-editor-BLArYRB7.js} +477 -477
- package/node_modules/@comis/web/dist/assets/{context-dag-browser-BwiaF5tf.js → context-dag-browser-fuyMinNI.js} +105 -105
- package/node_modules/@comis/web/dist/assets/{context-engine-BZ5Am6hA.js → context-engine-Bngf2bH0.js} +136 -136
- package/node_modules/@comis/web/dist/assets/decorate-BvWYovGE.js +38 -0
- package/node_modules/@comis/web/dist/assets/{delivery-view-OfBZof-m.js → delivery-view-C80hucxX.js} +134 -134
- package/node_modules/@comis/web/dist/assets/{diagnostics-view-YFwCxgr2.js → diagnostics-view-Cl4VbHZ6.js} +82 -82
- package/node_modules/@comis/web/dist/assets/directive-BOYXJ-K-.js +1 -0
- package/node_modules/@comis/web/dist/assets/{extract-variables-BM5qyK-s.js → extract-variables-B7-Doo7l.js} +39 -39
- package/node_modules/@comis/web/dist/assets/{ic-array-editor-B7m6x7-S.js → ic-array-editor-BLoEyeLS.js} +29 -29
- package/node_modules/@comis/web/dist/assets/{ic-breadcrumb-CUMpp3BL.js → ic-breadcrumb-DqN6G3gc.js} +16 -16
- package/node_modules/@comis/web/dist/assets/{ic-budget-segment-bar-BtJ6x5mN.js → ic-budget-segment-bar-zLsMzjDO.js} +20 -20
- package/node_modules/@comis/web/dist/assets/ic-chat-message-ByFUoMm6.js +352 -0
- package/node_modules/@comis/web/dist/assets/{ic-confirm-dialog-CCDbB04e.js → ic-confirm-dialog-DGlPbV1T.js} +26 -26
- package/node_modules/@comis/web/dist/assets/{ic-connection-dot-CnT1b8xr.js → ic-connection-dot-C4nDHgY2.js} +13 -13
- package/node_modules/@comis/web/dist/assets/ic-data-table-CKIvr-ag.js +277 -0
- package/node_modules/@comis/web/dist/assets/ic-delivery-row-B3YwjjuM.js +67 -0
- package/node_modules/@comis/web/dist/assets/{ic-detail-panel-BF83r-if.js → ic-detail-panel-DiCe4hLr.js} +27 -27
- package/node_modules/@comis/web/dist/assets/{ic-empty-state-60l2ePhd.js → ic-empty-state-CM3Wbj2f.js} +19 -19
- package/node_modules/@comis/web/dist/assets/ic-graph-canvas-ByRjij68.js +359 -0
- package/node_modules/@comis/web/dist/assets/ic-icon-BGNCCPpZ.js +33 -0
- package/node_modules/@comis/web/dist/assets/{ic-layer-waterfall-COvEYMg5.js → ic-layer-waterfall-WkaFyu-l.js} +18 -18
- package/node_modules/@comis/web/dist/assets/ic-relative-time-B3UAnTqg.js +12 -0
- package/node_modules/@comis/web/dist/assets/{ic-search-input-CSOxY9g7.js → ic-search-input-B02AGw1i.js} +22 -22
- package/node_modules/@comis/web/dist/assets/{ic-select-Ce-Raudx.js → ic-select-BqfZISjw.js} +29 -29
- package/node_modules/@comis/web/dist/assets/ic-tabs-yBjkWKJH.js +95 -0
- package/node_modules/@comis/web/dist/assets/ic-tag-CvMVQFRR.js +33 -0
- package/node_modules/@comis/web/dist/assets/{ic-time-range-picker-CypCT68y.js → ic-time-range-picker-DXbYeBmY.js} +31 -31
- package/node_modules/@comis/web/dist/assets/{ic-tool-call-7MaXSsCW.js → ic-tool-call-Bh5kq-yY.js} +51 -51
- package/node_modules/@comis/web/dist/assets/index-BBkuC-EU.js +2792 -0
- package/node_modules/@comis/web/dist/assets/index-CVEaS9aY.css +2 -0
- package/node_modules/@comis/web/dist/assets/{mcp-management-BNZPnpDn.js → mcp-management-DB-phOo7.js} +209 -209
- package/node_modules/@comis/web/dist/assets/{media-config-BBvTYxOX.js → media-config-CRqZ1ZUH.js} +154 -154
- package/node_modules/@comis/web/dist/assets/{media-test-BkK3RCRK.js → media-test-C9vE20Oy.js} +259 -259
- package/node_modules/@comis/web/dist/assets/{memory-inspector-1hDGCGat.js → memory-inspector-CeqfnxMZ.js} +450 -450
- package/node_modules/@comis/web/dist/assets/{message-center-CXefwsUu.js → message-center-Daup7Mof.js} +290 -290
- package/node_modules/@comis/web/dist/assets/{models-C1qcU_j3.js → models-DLYnEU8E.js} +371 -371
- package/node_modules/@comis/web/dist/assets/observability-types-D0tkwElU.js +1 -0
- package/node_modules/@comis/web/dist/assets/{observe-view-C0VBhX4C.js → observe-view-BTSt_PO5.js} +399 -399
- package/node_modules/@comis/web/dist/assets/pipeline-builder-DknfzyLt.js +1495 -0
- package/node_modules/@comis/web/dist/assets/{pipeline-history-DkfOQ6SW.js → pipeline-history-JnHZdeU_.js} +124 -124
- package/node_modules/@comis/web/dist/assets/{pipeline-history-detail-hyHgD0ai.js → pipeline-history-detail-Dg4knsEb.js} +65 -65
- package/node_modules/@comis/web/dist/assets/{pipeline-list-BPW8hV-q.js → pipeline-list-AEnibjsp.js} +227 -227
- package/node_modules/@comis/web/dist/assets/{pipeline-monitor-Bip16T7e.js → pipeline-monitor-DG7RbIOO.js} +298 -298
- package/node_modules/@comis/web/dist/assets/{scheduler-BGgwKd06.js → scheduler-uL1fYKAT.js} +486 -486
- package/node_modules/@comis/web/dist/assets/{security-D15st4xx.js → security-C3DywRLH.js} +389 -389
- package/node_modules/@comis/web/dist/assets/{session-detail-SGEYNJ0M.js → session-detail-BtqCNWXV.js} +294 -294
- package/node_modules/@comis/web/dist/assets/session-key-parser-Dkqcj2Ss.js +1 -0
- package/node_modules/@comis/web/dist/assets/session-list-CJXWa2XT.js +231 -0
- package/node_modules/@comis/web/dist/assets/{setup-wizard-nT0tz9QP.js → setup-wizard-ywn7oJvu.js} +486 -494
- package/node_modules/@comis/web/dist/assets/{skills-D8yVfSUy.js → skills-DX0KYnWD.js} +329 -329
- package/node_modules/@comis/web/dist/assets/{subagents-HHXMeHYo.js → subagents-B8p5YJEB.js} +74 -74
- package/node_modules/@comis/web/dist/assets/{workspace-manager-BQlr10iH.js → workspace-manager-CgzNIrw1.js} +236 -236
- package/node_modules/@comis/web/dist/index.html +3 -2
- package/node_modules/@comis/web/package.json +1 -1
- package/package.json +15 -15
- package/node_modules/@comis/skills/dist/builtin/platform/agents-list-tool.d.ts +0 -19
- package/node_modules/@comis/skills/dist/builtin/platform/agents-list-tool.js +0 -39
- package/node_modules/@comis/web/dist/assets/agent-editor-C26Q_xCs.js +0 -2173
- package/node_modules/@comis/web/dist/assets/billing-view-CtYvBqTE.js +0 -375
- package/node_modules/@comis/web/dist/assets/channel-list-B8dj3O9a.js +0 -323
- package/node_modules/@comis/web/dist/assets/directive-DoeGSK_T.js +0 -1
- package/node_modules/@comis/web/dist/assets/ic-chat-message-CFyDJd0z.js +0 -352
- package/node_modules/@comis/web/dist/assets/ic-data-table-CKUNTxHw.js +0 -277
- package/node_modules/@comis/web/dist/assets/ic-delivery-row-GP5Fkygs.js +0 -67
- package/node_modules/@comis/web/dist/assets/ic-graph-canvas-C8FuSMe1.js +0 -359
- package/node_modules/@comis/web/dist/assets/ic-icon-xeGTVhVG.js +0 -33
- package/node_modules/@comis/web/dist/assets/ic-relative-time-3FqpjeAI.js +0 -12
- package/node_modules/@comis/web/dist/assets/ic-tabs-B7QtM_v8.js +0 -95
- package/node_modules/@comis/web/dist/assets/ic-tag-CPPUnDLF.js +0 -33
- package/node_modules/@comis/web/dist/assets/index-CEcM1R_C.js +0 -2830
- package/node_modules/@comis/web/dist/assets/index-CIJFuItj.css +0 -1
- package/node_modules/@comis/web/dist/assets/observability-types-D7jUtSde.js +0 -1
- package/node_modules/@comis/web/dist/assets/pipeline-builder-DcUUIrm0.js +0 -1496
- package/node_modules/@comis/web/dist/assets/session-key-parser-DPORMVyU.js +0 -1
- package/node_modules/@comis/web/dist/assets/session-list-6ybUTxbY.js +0 -231
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Thinking-block hash invariant -- diagnostic instrumentation only.
|
|
4
|
+
*
|
|
5
|
+
* Observed problem: Anthropic 400 `messages.N.content.M: thinking/redacted_thinking
|
|
6
|
+
* blocks cannot be modified` errors keep firing in production even after the
|
|
7
|
+
* surrogate-guard, drift-scrubber, and signed-replay-detector layers shipped
|
|
8
|
+
* (260425-rvm), and even after the immutable-section redirect (260425-t40).
|
|
9
|
+
* Trace `c7b91328-9dc5-4618-9ae8-ca207b4b93df` on 2026-04-28 hit a 400 ~2.2s
|
|
10
|
+
* after `turn_end` -- meaning *some other layer* mutates a signed thinking
|
|
11
|
+
* block between the assistant turn and the next replay. We don't know which.
|
|
12
|
+
*
|
|
13
|
+
* This module is the diagnostic. At each `turn_end` with signed thinking
|
|
14
|
+
* blocks, the bridge captures a SHA-256 hash of every thinking block. Before
|
|
15
|
+
* the next assistant-message resend, the bridge recomputes the hashes and
|
|
16
|
+
* asserts they match the captured snapshots. On mismatch, ONE structured
|
|
17
|
+
* ERROR log fires per mutated index with enough context to pinpoint the
|
|
18
|
+
* offending layer (responseId, blockIndex, old/new hash, first-32-chars of
|
|
19
|
+
* old/new text, signature length before/after).
|
|
20
|
+
*
|
|
21
|
+
* Behavior contract (enforced by tests + source-shape grep):
|
|
22
|
+
* - NEVER throws. Every code path returns normally; logger errors are
|
|
23
|
+
* swallowed because we don't want the diagnostic itself to abort agent flow.
|
|
24
|
+
* - NEVER mutates inputs. Pure read; only output is the structured log.
|
|
25
|
+
* - NEVER alters request flow. The mismatch is observable signal only --
|
|
26
|
+
* Anthropic's 400 still surfaces through the existing error path
|
|
27
|
+
* (signed-replay-detector -> executor-prompt-runner). Bug A behavior fix
|
|
28
|
+
* is a separate quick task gated on what this diagnostic reveals.
|
|
29
|
+
*
|
|
30
|
+
* Logging surface follows CLAUDE.md canonical Pino fields:
|
|
31
|
+
* - object-first signature: `error({...fields}, "msg")`
|
|
32
|
+
* - `module: "agent.bridge.hash-invariant"`
|
|
33
|
+
* - `errorKind: "internal"` (classification per AGENTS.md §2.1)
|
|
34
|
+
* - `hint`: actionable next step for the on-call diagnoser
|
|
35
|
+
* - `responseId`, `blockIndex`, `oldHash`, `newHash`,
|
|
36
|
+
* `oldText.firstChars`, `newText.firstChars`, `oldSigLen`, `newSigLen`
|
|
37
|
+
*
|
|
38
|
+
* Privacy / threat note: `oldText.firstChars` and `newText.firstChars` are
|
|
39
|
+
* 32-char prefixes of `block.thinking`. Anthropic redacts thinking text
|
|
40
|
+
* upstream when it would leak credentials, and Comis layers (surrogate guard,
|
|
41
|
+
* drift scrubber) further sanitize before any persistence. The hash itself is
|
|
42
|
+
* one-way and non-credential-bearing. Pino's redaction config is a safety net.
|
|
43
|
+
*
|
|
44
|
+
* @module
|
|
45
|
+
*/
|
|
46
|
+
import { createHash } from "node:crypto";
|
|
47
|
+
import { readFile as fsReadFile } from "node:fs/promises";
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Internals
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
const HINT = "Locate the context-engine layer that touched this block " +
|
|
52
|
+
"(likely between turn_end and the next pi-ai serialize step). " +
|
|
53
|
+
"Compare oldText.firstChars vs newText.firstChars to identify mutation type.";
|
|
54
|
+
const MODULE_FIELD = "agent.bridge.hash-invariant";
|
|
55
|
+
const ERROR_KIND = "internal";
|
|
56
|
+
const TEXT_PREFIX_LEN = 32;
|
|
57
|
+
/** Format the four-field hash payload for one block. */
|
|
58
|
+
function buildHashInput(type, thinking, signature, redacted) {
|
|
59
|
+
const t = typeof type === "string" ? type : "";
|
|
60
|
+
const text = typeof thinking === "string" ? thinking : "";
|
|
61
|
+
const sig = typeof signature === "string" ? signature : "";
|
|
62
|
+
const r = redacted === true ? "1" : "0";
|
|
63
|
+
// Use 0x00 separators so any field's value cannot collide with the
|
|
64
|
+
// delimiter (UTF-16 NUL never appears in Anthropic content blocks).
|
|
65
|
+
return `${t}\x00${text}\x00${sig}\x00${r}`;
|
|
66
|
+
}
|
|
67
|
+
/** Safe shallow read of a record field without throwing on null/undefined. */
|
|
68
|
+
function readField(block, field) {
|
|
69
|
+
if (block === null || typeof block !== "object")
|
|
70
|
+
return undefined;
|
|
71
|
+
// eslint-disable-next-line security/detect-object-injection -- field is a literal constant from caller below
|
|
72
|
+
return block[field];
|
|
73
|
+
}
|
|
74
|
+
/** Best-effort logger.error invocation -- swallows logger errors. */
|
|
75
|
+
function safeLog(deps, payload, msg) {
|
|
76
|
+
try {
|
|
77
|
+
deps.logger.error(payload, msg);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Diagnostic must not abort agent flow even if the logger itself fails.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Public API
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
/**
|
|
87
|
+
* Compute SHA-256 hashes for every `type:"thinking"` block in `content`.
|
|
88
|
+
*
|
|
89
|
+
* Mirrors signature-surrogate-guard's exclusion rule: skips non-thinking
|
|
90
|
+
* blocks AND skips blocks where `redacted === true` (no readable text). The
|
|
91
|
+
* resulting `blockIndex` field counts position WITHIN the thinking-only
|
|
92
|
+
* stream, so callers can compare positionally even when the surrounding mix
|
|
93
|
+
* of text/tool blocks varies between turns.
|
|
94
|
+
*
|
|
95
|
+
* Pure: never mutates input, never throws. Returns an empty array when
|
|
96
|
+
* `content` is empty or contains no thinking blocks.
|
|
97
|
+
*/
|
|
98
|
+
export function computeThinkingBlockHashes(content) {
|
|
99
|
+
if (!Array.isArray(content))
|
|
100
|
+
return [];
|
|
101
|
+
const result = [];
|
|
102
|
+
let thinkingIndex = 0;
|
|
103
|
+
for (const block of content) {
|
|
104
|
+
const type = readField(block, "type");
|
|
105
|
+
if (type !== "thinking")
|
|
106
|
+
continue;
|
|
107
|
+
if (readField(block, "redacted") === true)
|
|
108
|
+
continue;
|
|
109
|
+
const thinking = readField(block, "thinking");
|
|
110
|
+
const signature = readField(block, "thinkingSignature");
|
|
111
|
+
const input = buildHashInput(type, thinking, signature, readField(block, "redacted"));
|
|
112
|
+
const hash = createHash("sha256").update(input).digest("hex");
|
|
113
|
+
const textStr = typeof thinking === "string" ? thinking : "";
|
|
114
|
+
const sigStr = typeof signature === "string" ? signature : "";
|
|
115
|
+
result.push({
|
|
116
|
+
blockIndex: thinkingIndex,
|
|
117
|
+
hash,
|
|
118
|
+
textFirstChars: textStr.slice(0, TEXT_PREFIX_LEN),
|
|
119
|
+
sigLen: sigStr.length,
|
|
120
|
+
});
|
|
121
|
+
thinkingIndex++;
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Compare prior captured hashes against the current shape of `content`.
|
|
127
|
+
*
|
|
128
|
+
* Logs ONE structured ERROR per mismatched index. When `prior` is empty,
|
|
129
|
+
* this is a no-op (no hashes were captured for this responseId, so there's
|
|
130
|
+
* nothing to verify). When `current` has fewer thinking blocks than `prior`,
|
|
131
|
+
* each missing index is reported with `newHash:null`, `newText.firstChars:""`,
|
|
132
|
+
* `newSigLen:0`.
|
|
133
|
+
*
|
|
134
|
+
* Never throws. Never mutates `prior` or `current`.
|
|
135
|
+
*/
|
|
136
|
+
export function assertThinkingBlocksUnchanged(prior, current, responseId, deps) {
|
|
137
|
+
if (!Array.isArray(prior) || prior.length === 0) {
|
|
138
|
+
return { candidatesChecked: 0, mismatchesLogged: 0, anyResponseIdMatched: false };
|
|
139
|
+
}
|
|
140
|
+
const currentHashes = computeThinkingBlockHashes(current);
|
|
141
|
+
const byIndex = new Map();
|
|
142
|
+
for (const h of currentHashes)
|
|
143
|
+
byIndex.set(h.blockIndex, h);
|
|
144
|
+
let mismatchesLogged = 0;
|
|
145
|
+
let anyResponseIdMatched = false;
|
|
146
|
+
for (const old of prior) {
|
|
147
|
+
const now = byIndex.get(old.blockIndex);
|
|
148
|
+
if (!now) {
|
|
149
|
+
mismatchesLogged++;
|
|
150
|
+
safeLog(deps, {
|
|
151
|
+
module: MODULE_FIELD,
|
|
152
|
+
responseId,
|
|
153
|
+
blockIndex: old.blockIndex,
|
|
154
|
+
oldHash: old.hash,
|
|
155
|
+
newHash: null,
|
|
156
|
+
oldText: { firstChars: old.textFirstChars },
|
|
157
|
+
newText: { firstChars: "" },
|
|
158
|
+
oldSigLen: old.sigLen,
|
|
159
|
+
newSigLen: 0,
|
|
160
|
+
errorKind: ERROR_KIND,
|
|
161
|
+
hint: HINT,
|
|
162
|
+
}, "Thinking block mutated between turns");
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
anyResponseIdMatched = true;
|
|
166
|
+
if (now.hash !== old.hash) {
|
|
167
|
+
mismatchesLogged++;
|
|
168
|
+
safeLog(deps, {
|
|
169
|
+
module: MODULE_FIELD,
|
|
170
|
+
responseId,
|
|
171
|
+
blockIndex: old.blockIndex,
|
|
172
|
+
oldHash: old.hash,
|
|
173
|
+
newHash: now.hash,
|
|
174
|
+
oldText: { firstChars: old.textFirstChars },
|
|
175
|
+
newText: { firstChars: now.textFirstChars },
|
|
176
|
+
oldSigLen: old.sigLen,
|
|
177
|
+
newSigLen: now.sigLen,
|
|
178
|
+
errorKind: ERROR_KIND,
|
|
179
|
+
hint: HINT,
|
|
180
|
+
}, "Thinking block mutated between turns");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
candidatesChecked: prior.length,
|
|
185
|
+
mismatchesLogged,
|
|
186
|
+
anyResponseIdMatched,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// 260428-hoy: Canonical thinking-block restoration
|
|
191
|
+
//
|
|
192
|
+
// Heals cross-turn mutation of signed thinking blocks before pi-ai serializes
|
|
193
|
+
// the next API request. Pure / idempotent / never-throws. Runs AFTER
|
|
194
|
+
// `assertThinkingBlocksUnchanged` so the diagnostic ERROR log captures every
|
|
195
|
+
// mutation before the heal overwrites it.
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
const RESTORE_MODULE_FIELD = "agent.bridge.canonical-restore";
|
|
198
|
+
const RESTORE_WARN_HINT = "Canonical restore aborted on malformed input; in-memory messages " +
|
|
199
|
+
"returned unchanged. Inspect prior context-engine layers for shape drift.";
|
|
200
|
+
/** Best-effort logger.info / logger.warn -- swallows logger errors. */
|
|
201
|
+
function safeRestoreLog(deps, level, payload, msg) {
|
|
202
|
+
const logger = deps?.logger;
|
|
203
|
+
if (!logger)
|
|
204
|
+
return;
|
|
205
|
+
try {
|
|
206
|
+
if (level === "info")
|
|
207
|
+
logger.info(payload, msg);
|
|
208
|
+
else
|
|
209
|
+
logger.warn(payload, msg);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// Restore must never abort agent flow even if the logger itself fails.
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Replace mutated thinking blocks with their canonical snapshot, in-memory only.
|
|
217
|
+
*
|
|
218
|
+
* Pure: never mutates input arrays or block objects. Idempotent: when canonical
|
|
219
|
+
* matches in-memory exactly, returns `{ messages: <same ref>, restoredCount: 0,
|
|
220
|
+
* affectedResponseIds: [] }`. On at least one swap, returns a NEW top-level
|
|
221
|
+
* array AND a NEW content array on each affected message.
|
|
222
|
+
*
|
|
223
|
+
* Replaces ONLY blocks where BOTH `current[i].type === "thinking"` AND
|
|
224
|
+
* `canonical[i].type === "thinking"` AND `current[i].redacted !== true` AND
|
|
225
|
+
* `canonical[i].redacted !== true`. Text blocks, tool_use, tool_result,
|
|
226
|
+
* redacted_thinking, and any block where positional types disagree are passed
|
|
227
|
+
* through unchanged.
|
|
228
|
+
*
|
|
229
|
+
* Skips messages where `role !== "assistant"`, where `responseId` is not a
|
|
230
|
+
* string, or where the canonical store has no entry for that responseId.
|
|
231
|
+
*
|
|
232
|
+
* Never throws. On any unexpected error during the walk (e.g. malformed
|
|
233
|
+
* canonical entry whose getter throws), the entire result is `{ messages:
|
|
234
|
+
* <input ref>, restoredCount: 0, affectedResponseIds: [] }` and ONE WARN log
|
|
235
|
+
* fires with `module: RESTORE_MODULE_FIELD, errorKind: "internal"`.
|
|
236
|
+
*/
|
|
237
|
+
export function restoreCanonicalThinkingBlocks(messages, canonicalStore, deps) {
|
|
238
|
+
if (!Array.isArray(messages)) {
|
|
239
|
+
return { messages: [], restoredCount: 0, affectedResponseIds: [] };
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
let result = null; // lazy copy-on-write
|
|
243
|
+
let restoredCount = 0;
|
|
244
|
+
const seenResponseIds = new Set();
|
|
245
|
+
const affectedResponseIds = [];
|
|
246
|
+
for (let i = 0; i < messages.length; i++) {
|
|
247
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
248
|
+
const msg = messages[i];
|
|
249
|
+
const swapMsg = tryRestoreMessage(msg, canonicalStore);
|
|
250
|
+
if (swapMsg.didSwap) {
|
|
251
|
+
if (result === null) {
|
|
252
|
+
// Materialize a copy of the prefix and switch to copy-on-write.
|
|
253
|
+
result = messages.slice(0, i);
|
|
254
|
+
}
|
|
255
|
+
result.push(swapMsg.message);
|
|
256
|
+
restoredCount += swapMsg.swapsInMessage;
|
|
257
|
+
const rid = swapMsg.responseId;
|
|
258
|
+
if (rid !== undefined && !seenResponseIds.has(rid)) {
|
|
259
|
+
seenResponseIds.add(rid);
|
|
260
|
+
affectedResponseIds.push(rid);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else if (result !== null) {
|
|
264
|
+
result.push(msg);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (result === null) {
|
|
268
|
+
// No swap happened -- preserve exact input reference for caller's
|
|
269
|
+
// identity-equality check.
|
|
270
|
+
return {
|
|
271
|
+
messages: messages,
|
|
272
|
+
restoredCount: 0,
|
|
273
|
+
affectedResponseIds: [],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
safeRestoreLog(deps, "info", {
|
|
277
|
+
module: RESTORE_MODULE_FIELD,
|
|
278
|
+
restoredCount,
|
|
279
|
+
affectedResponseIds,
|
|
280
|
+
}, "Restored canonical thinking blocks before resend");
|
|
281
|
+
return { messages: result, restoredCount, affectedResponseIds };
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
// Defensive last-resort: malformed canonical entry or any other thrown
|
|
285
|
+
// error during the walk. Return the in-memory shape unchanged + WARN log.
|
|
286
|
+
safeRestoreLog(deps, "warn", {
|
|
287
|
+
module: RESTORE_MODULE_FIELD,
|
|
288
|
+
errorKind: "internal",
|
|
289
|
+
hint: RESTORE_WARN_HINT,
|
|
290
|
+
}, "Canonical restore aborted on malformed input");
|
|
291
|
+
return {
|
|
292
|
+
messages: messages,
|
|
293
|
+
restoredCount: 0,
|
|
294
|
+
affectedResponseIds: [],
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function tryRestoreMessage(msg, canonicalStore) {
|
|
299
|
+
const noSwap = {
|
|
300
|
+
didSwap: false,
|
|
301
|
+
message: msg,
|
|
302
|
+
swapsInMessage: 0,
|
|
303
|
+
responseId: undefined,
|
|
304
|
+
};
|
|
305
|
+
if (msg === null || typeof msg !== "object")
|
|
306
|
+
return noSwap;
|
|
307
|
+
const role = readField(msg, "role");
|
|
308
|
+
if (role !== "assistant")
|
|
309
|
+
return noSwap;
|
|
310
|
+
const responseId = readField(msg, "responseId");
|
|
311
|
+
if (typeof responseId !== "string")
|
|
312
|
+
return noSwap;
|
|
313
|
+
const canonical = canonicalStore.get(responseId);
|
|
314
|
+
if (!canonical)
|
|
315
|
+
return noSwap;
|
|
316
|
+
if (!Array.isArray(canonical))
|
|
317
|
+
return noSwap;
|
|
318
|
+
const liveContent = readField(msg, "content");
|
|
319
|
+
if (!Array.isArray(liveContent))
|
|
320
|
+
return noSwap;
|
|
321
|
+
// Walk content arrays in parallel; produce a copy-on-write content array.
|
|
322
|
+
let healedContent = null;
|
|
323
|
+
let swapsInMessage = 0;
|
|
324
|
+
const len = liveContent.length;
|
|
325
|
+
for (let j = 0; j < len; j++) {
|
|
326
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
327
|
+
const liveBlock = liveContent[j];
|
|
328
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
329
|
+
const canonicalBlock = j < canonical.length ? canonical[j] : undefined;
|
|
330
|
+
if (!shouldSwapBlock(liveBlock, canonicalBlock)) {
|
|
331
|
+
if (healedContent !== null)
|
|
332
|
+
healedContent.push(liveBlock);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (healedContent === null) {
|
|
336
|
+
healedContent = liveContent.slice(0, j);
|
|
337
|
+
}
|
|
338
|
+
healedContent.push(canonicalBlock);
|
|
339
|
+
swapsInMessage++;
|
|
340
|
+
}
|
|
341
|
+
if (healedContent === null)
|
|
342
|
+
return noSwap;
|
|
343
|
+
// Shallow-copy the message with new content; preserve all other fields.
|
|
344
|
+
const newMsg = { ...msg };
|
|
345
|
+
newMsg.content = healedContent;
|
|
346
|
+
return {
|
|
347
|
+
didSwap: true,
|
|
348
|
+
message: newMsg,
|
|
349
|
+
swapsInMessage,
|
|
350
|
+
responseId,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/** Decide whether to replace `live[j]` with `canonical[j]`. */
|
|
354
|
+
function shouldSwapBlock(liveBlock, canonicalBlock) {
|
|
355
|
+
if (liveBlock === canonicalBlock)
|
|
356
|
+
return false; // identity short-circuit
|
|
357
|
+
const liveType = readField(liveBlock, "type");
|
|
358
|
+
if (liveType !== "thinking")
|
|
359
|
+
return false;
|
|
360
|
+
const canonicalType = readField(canonicalBlock, "type");
|
|
361
|
+
if (canonicalType !== "thinking")
|
|
362
|
+
return false;
|
|
363
|
+
if (readField(liveBlock, "redacted") === true)
|
|
364
|
+
return false;
|
|
365
|
+
if (readField(canonicalBlock, "redacted") === true)
|
|
366
|
+
return false;
|
|
367
|
+
// Compare hashes via the existing four-field tuple. If they match, no swap.
|
|
368
|
+
const liveHash = blockHash(liveBlock);
|
|
369
|
+
const canonicalHash = blockHash(canonicalBlock);
|
|
370
|
+
return liveHash !== canonicalHash;
|
|
371
|
+
}
|
|
372
|
+
/** Compute the same SHA-256 four-field hash used by computeThinkingBlockHashes
|
|
373
|
+
* for a single block. Pure helper -- read fields directly without throwing. */
|
|
374
|
+
function blockHash(block) {
|
|
375
|
+
const type = readField(block, "type");
|
|
376
|
+
const thinking = readField(block, "thinking");
|
|
377
|
+
const signature = readField(block, "thinkingSignature");
|
|
378
|
+
const redacted = readField(block, "redacted");
|
|
379
|
+
const input = buildHashInput(type, thinking, signature, redacted);
|
|
380
|
+
return createHash("sha256").update(input).digest("hex");
|
|
381
|
+
}
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// 260428-iag: Wire-edge diagnostic — diff in-memory thinking blocks against
|
|
384
|
+
// persisted JSONL canonical.
|
|
385
|
+
//
|
|
386
|
+
// Fires from the pi-event-bridge LLM-error path when Anthropic returns 400
|
|
387
|
+
// with a "thinking blocks ... cannot be modified" signature, even after the
|
|
388
|
+
// 260428-hoy canonical-restore layer ran pre-serialize. The persisted JSONL
|
|
389
|
+
// is the only truly immutable record of the assistant message — written
|
|
390
|
+
// byte-for-byte from Anthropic's stream at receipt time. A divergence between
|
|
391
|
+
// in-memory content and persisted canonical at this point implies the
|
|
392
|
+
// mutation occurred AFTER the bridge restoration hook (likely inside pi-ai's
|
|
393
|
+
// `sanitizeSurrogates` during request serialization).
|
|
394
|
+
//
|
|
395
|
+
// Behavior contract (mirrors the rest of this module):
|
|
396
|
+
// - NEVER throws. Every code path returns normally.
|
|
397
|
+
// - Read errors / parse errors / responseId-not-found degrade to ONE WARN log
|
|
398
|
+
// and an empty result. The diagnostic must NEVER abort agent flow.
|
|
399
|
+
// - Caller passes a resolved `jsonlPath`; this helper does not compose paths.
|
|
400
|
+
// ---------------------------------------------------------------------------
|
|
401
|
+
const WIRE_DIFF_MODULE_FIELD = "agent.bridge.wire-diff";
|
|
402
|
+
export const WIRE_DIFF_HINT_FILE_MISSING = "JSONL session file unreadable; wire-edge diff skipped. " +
|
|
403
|
+
"Confirm session path resolution and filesystem permissions.";
|
|
404
|
+
export const WIRE_DIFF_HINT_NOT_FOUND = "responseId not present in persisted JSONL session file; " +
|
|
405
|
+
"wire-edge diff skipped. The assistant message may not have been " +
|
|
406
|
+
"persisted yet, or the responseId was rotated.";
|
|
407
|
+
export const WIRE_DIFF_HINT_INTERNAL = "Wire-edge diff aborted on unexpected internal error; in-memory " +
|
|
408
|
+
"shape passed through unchanged. Inspect prior context-engine layers.";
|
|
409
|
+
/** Best-effort wire-diff log -- swallows logger errors. */
|
|
410
|
+
function safeWireDiffLog(deps, level, payload, msg) {
|
|
411
|
+
const logger = deps?.logger;
|
|
412
|
+
if (!logger)
|
|
413
|
+
return;
|
|
414
|
+
try {
|
|
415
|
+
if (level === "warn")
|
|
416
|
+
logger.warn(payload, msg);
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
// Diagnostic must NEVER abort agent flow even if the logger itself fails.
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Diff in-memory thinking blocks against the persisted JSONL canonical.
|
|
424
|
+
*
|
|
425
|
+
* Reads the persisted JSONL session file, locates the FIRST assistant message
|
|
426
|
+
* matching `responseId`, and compares its content (canonical, written from
|
|
427
|
+
* Anthropic's stream at receipt time) against `inMemoryContent` using the
|
|
428
|
+
* existing `computeThinkingBlockHashes` primitive.
|
|
429
|
+
*
|
|
430
|
+
* Returns an array of `PersistedDiffEntry` -- one per divergent thinking
|
|
431
|
+
* block. When everything matches positionally, returns `[]`. When in-memory
|
|
432
|
+
* has fewer thinking blocks than persisted, each missing index produces an
|
|
433
|
+
* entry with `inMemoryHash: null`, empty `inMemoryText.firstChars`, and
|
|
434
|
+
* `inMemorySigLen: 0`.
|
|
435
|
+
*
|
|
436
|
+
* Behavior contract:
|
|
437
|
+
* - NEVER throws. Read errors, parse errors, or responseId-not-found degrade
|
|
438
|
+
* to ONE WARN log + `[]`.
|
|
439
|
+
* - When TWO assistant messages share the same responseId in the JSONL,
|
|
440
|
+
* uses the FIRST match (matches the bridge's "trust the first persisted
|
|
441
|
+
* state" semantic).
|
|
442
|
+
* - Malformed lines (invalid JSON) are skipped silently; scanning continues.
|
|
443
|
+
*
|
|
444
|
+
* @param inMemoryContent - The in-memory content array of the assistant
|
|
445
|
+
* message (the same shape pi-ai is about to serialize).
|
|
446
|
+
* @param responseId - The responseId of the assistant message to look up.
|
|
447
|
+
* @param jsonlPath - Resolved absolute path to the JSONL session file. Path
|
|
448
|
+
* composition is the caller's responsibility (this helper does no
|
|
449
|
+
* safePath / sessionKey routing).
|
|
450
|
+
* @param deps - Optional logger + readFile injection.
|
|
451
|
+
*/
|
|
452
|
+
export async function diffThinkingBlocksAgainstPersisted(inMemoryContent, responseId, jsonlPath, deps) {
|
|
453
|
+
try {
|
|
454
|
+
// Step 1: Read the persisted JSONL file.
|
|
455
|
+
const reader = deps?.readFile ?? fsReadFile;
|
|
456
|
+
let text;
|
|
457
|
+
try {
|
|
458
|
+
// jsonlPath is resolved upstream via sessionKeyToPath -> safePath, so
|
|
459
|
+
// the file path is traversal-safe by construction. Lint isn't triggered
|
|
460
|
+
// here because `reader` is a polymorphic variable; doc kept for review.
|
|
461
|
+
text = await reader(jsonlPath, "utf-8");
|
|
462
|
+
}
|
|
463
|
+
catch (readErr) {
|
|
464
|
+
safeWireDiffLog(deps, "warn", {
|
|
465
|
+
module: WIRE_DIFF_MODULE_FIELD,
|
|
466
|
+
errorKind: ERROR_KIND,
|
|
467
|
+
hint: WIRE_DIFF_HINT_FILE_MISSING,
|
|
468
|
+
jsonlPath,
|
|
469
|
+
responseId,
|
|
470
|
+
err: readErr instanceof Error ? readErr.message : String(readErr),
|
|
471
|
+
}, "Persisted JSONL not readable; wire-edge diff skipped");
|
|
472
|
+
return [];
|
|
473
|
+
}
|
|
474
|
+
// Step 2: Walk lines, find the FIRST assistant message with matching responseId.
|
|
475
|
+
let persistedContent = null;
|
|
476
|
+
const lines = text.split("\n");
|
|
477
|
+
for (const line of lines) {
|
|
478
|
+
if (line.length === 0)
|
|
479
|
+
continue;
|
|
480
|
+
let parsed;
|
|
481
|
+
try {
|
|
482
|
+
parsed = JSON.parse(line);
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
// Malformed line -- skip silently and continue scanning.
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (parsed === null || typeof parsed !== "object")
|
|
489
|
+
continue;
|
|
490
|
+
const entry = parsed;
|
|
491
|
+
if (entry.type !== "message")
|
|
492
|
+
continue;
|
|
493
|
+
const message = entry.message;
|
|
494
|
+
if (message === null || typeof message !== "object")
|
|
495
|
+
continue;
|
|
496
|
+
const msg = message;
|
|
497
|
+
if (msg.role !== "assistant")
|
|
498
|
+
continue;
|
|
499
|
+
if (msg.responseId !== responseId)
|
|
500
|
+
continue;
|
|
501
|
+
// First match wins.
|
|
502
|
+
persistedContent = Array.isArray(msg.content)
|
|
503
|
+
? msg.content
|
|
504
|
+
: [];
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
if (persistedContent === null) {
|
|
508
|
+
safeWireDiffLog(deps, "warn", {
|
|
509
|
+
module: WIRE_DIFF_MODULE_FIELD,
|
|
510
|
+
errorKind: ERROR_KIND,
|
|
511
|
+
hint: WIRE_DIFF_HINT_NOT_FOUND,
|
|
512
|
+
jsonlPath,
|
|
513
|
+
responseId,
|
|
514
|
+
}, "responseId not found in persisted JSONL; wire-edge diff skipped");
|
|
515
|
+
return [];
|
|
516
|
+
}
|
|
517
|
+
// Step 3: Compute per-side hashes and diff positionally.
|
|
518
|
+
const persistedHashes = computeThinkingBlockHashes(persistedContent);
|
|
519
|
+
const inMemoryHashes = computeThinkingBlockHashes(inMemoryContent ?? []);
|
|
520
|
+
const byIndex = new Map();
|
|
521
|
+
for (const h of inMemoryHashes)
|
|
522
|
+
byIndex.set(h.blockIndex, h);
|
|
523
|
+
const entries = [];
|
|
524
|
+
for (const persisted of persistedHashes) {
|
|
525
|
+
const now = byIndex.get(persisted.blockIndex);
|
|
526
|
+
if (!now) {
|
|
527
|
+
entries.push({
|
|
528
|
+
blockIndex: persisted.blockIndex,
|
|
529
|
+
persistedHash: persisted.hash,
|
|
530
|
+
inMemoryHash: null,
|
|
531
|
+
persistedText: { firstChars: persisted.textFirstChars },
|
|
532
|
+
inMemoryText: { firstChars: "" },
|
|
533
|
+
persistedSigLen: persisted.sigLen,
|
|
534
|
+
inMemorySigLen: 0,
|
|
535
|
+
});
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (now.hash !== persisted.hash) {
|
|
539
|
+
entries.push({
|
|
540
|
+
blockIndex: persisted.blockIndex,
|
|
541
|
+
persistedHash: persisted.hash,
|
|
542
|
+
inMemoryHash: now.hash,
|
|
543
|
+
persistedText: { firstChars: persisted.textFirstChars },
|
|
544
|
+
inMemoryText: { firstChars: now.textFirstChars },
|
|
545
|
+
persistedSigLen: persisted.sigLen,
|
|
546
|
+
inMemorySigLen: now.sigLen,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return entries;
|
|
551
|
+
}
|
|
552
|
+
catch (unexpectedErr) {
|
|
553
|
+
// Defensive last-resort: if anything else throws (parse internals,
|
|
554
|
+
// computeThinkingBlockHashes against malformed input, etc.), degrade
|
|
555
|
+
// to WARN + empty result.
|
|
556
|
+
safeWireDiffLog(deps, "warn", {
|
|
557
|
+
module: WIRE_DIFF_MODULE_FIELD,
|
|
558
|
+
errorKind: ERROR_KIND,
|
|
559
|
+
hint: WIRE_DIFF_HINT_INTERNAL,
|
|
560
|
+
jsonlPath,
|
|
561
|
+
responseId,
|
|
562
|
+
err: unexpectedErr instanceof Error ? unexpectedErr.message : String(unexpectedErr),
|
|
563
|
+
}, "Wire-edge diff aborted on unexpected error");
|
|
564
|
+
return [];
|
|
565
|
+
}
|
|
566
|
+
}
|
|
@@ -196,17 +196,19 @@ export function createContextEngine(config, deps) {
|
|
|
196
196
|
thinkingCleaner = createThinkingBlockCleaner(config.thinkingKeepTurns, (stats) => { callbackState.thinking = stats; }, deps.getThinkingKeepTurnsOverride);
|
|
197
197
|
layers.push(thinkingCleaner);
|
|
198
198
|
}
|
|
199
|
-
// Signature replay scrubber
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
199
|
+
// Signature replay scrubber: drops thinking signatures (and toolCall
|
|
200
|
+
// thoughtSignatures) from every assistant message OLDER than the
|
|
201
|
+
// most recent one. Always-on — no drift-mode gate. The latest
|
|
202
|
+
// assistant message keeps its signatures so Anthropic's
|
|
203
|
+
// extended-thinking continuation can validate the immediate next
|
|
204
|
+
// call's prefix. (Replaces 260428-kvl detection-based approach;
|
|
205
|
+
// see .planning/quick/260428-lm6 for rationale.)
|
|
205
206
|
if (deps.getReplayDriftMode) {
|
|
206
207
|
const getReplayDriftMode = deps.getReplayDriftMode;
|
|
207
208
|
layers.push(createSignatureReplayScrubber({
|
|
208
209
|
getReplayDriftMode,
|
|
209
210
|
onScrubbed: (stats) => { callbackState.signatureReplayScrubber = stats; },
|
|
211
|
+
logger: deps.logger,
|
|
210
212
|
}));
|
|
211
213
|
}
|
|
212
214
|
// Signature surrogate guard (Fix #3): scrub thinkingSignature from blocks
|