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.
Files changed (192) hide show
  1. package/node_modules/@comis/agent/dist/bootstrap/sections/tool-descriptions.js +130 -10
  2. package/node_modules/@comis/agent/dist/bootstrap/sections/tooling-sections.d.ts +1 -1
  3. package/node_modules/@comis/agent/dist/bootstrap/sections/tooling-sections.js +9 -2
  4. package/node_modules/@comis/agent/dist/bridge/bridge-metrics.d.ts +8 -0
  5. package/node_modules/@comis/agent/dist/bridge/bridge-metrics.js +2 -0
  6. package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.d.ts +29 -0
  7. package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.js +242 -2
  8. package/node_modules/@comis/agent/dist/bridge/thinking-block-hash-invariant.d.ts +210 -0
  9. package/node_modules/@comis/agent/dist/bridge/thinking-block-hash-invariant.js +566 -0
  10. package/node_modules/@comis/agent/dist/context-engine/context-engine.js +8 -6
  11. package/node_modules/@comis/agent/dist/context-engine/signature-replay-scrubber.d.ts +51 -30
  12. package/node_modules/@comis/agent/dist/context-engine/signature-replay-scrubber.js +109 -36
  13. package/node_modules/@comis/agent/dist/executor/executor-context-engine-setup.js +5 -1
  14. package/node_modules/@comis/agent/dist/executor/executor-post-execution.js +22 -20
  15. package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.d.ts +2 -0
  16. package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.js +111 -15
  17. package/node_modules/@comis/agent/dist/executor/executor-response-filter.d.ts +20 -17
  18. package/node_modules/@comis/agent/dist/executor/executor-response-filter.js +132 -52
  19. package/node_modules/@comis/agent/dist/executor/executor-tool-assembly.js +16 -3
  20. package/node_modules/@comis/agent/dist/executor/model-retry.d.ts +14 -0
  21. package/node_modules/@comis/agent/dist/executor/model-retry.js +72 -1
  22. package/node_modules/@comis/agent/dist/executor/pi-executor.d.ts +3 -0
  23. package/node_modules/@comis/agent/dist/executor/pi-executor.js +68 -9
  24. package/node_modules/@comis/agent/dist/executor/post-batch-continuation.d.ts +82 -0
  25. package/node_modules/@comis/agent/dist/executor/post-batch-continuation.js +200 -0
  26. package/node_modules/@comis/agent/dist/executor/stream-wrappers/request-body-injector.js +1 -9
  27. package/node_modules/@comis/agent/dist/executor/tool-deferral.d.ts +37 -2
  28. package/node_modules/@comis/agent/dist/executor/tool-deferral.js +45 -3
  29. package/node_modules/@comis/agent/dist/executor/tool-parallelism.js +0 -1
  30. package/node_modules/@comis/agent/dist/executor/types.d.ts +11 -2
  31. package/node_modules/@comis/agent/dist/index.d.ts +3 -1
  32. package/node_modules/@comis/agent/dist/index.js +2 -0
  33. package/node_modules/@comis/agent/dist/model/last-known-model.d.ts +36 -0
  34. package/node_modules/@comis/agent/dist/model/last-known-model.js +49 -0
  35. package/node_modules/@comis/agent/dist/model/model-registry-adapter.d.ts +16 -4
  36. package/node_modules/@comis/agent/dist/model/model-registry-adapter.js +65 -21
  37. package/node_modules/@comis/agent/dist/planner/types.d.ts +0 -2
  38. package/node_modules/@comis/agent/dist/session/comis-session-manager.d.ts +10 -0
  39. package/node_modules/@comis/agent/dist/session/comis-session-manager.js +5 -0
  40. package/node_modules/@comis/agent/dist/spawn/pi-mono-adapters.js +7 -0
  41. package/node_modules/@comis/agent/package.json +1 -1
  42. package/node_modules/@comis/channels/package.json +1 -1
  43. package/node_modules/@comis/cli/dist/client/rpc-client.js +6 -1
  44. package/node_modules/@comis/cli/dist/commands/doctor.js +5 -3
  45. package/node_modules/@comis/cli/dist/commands/health.js +5 -2
  46. package/node_modules/@comis/cli/dist/wizard/json-output.js +7 -3
  47. package/node_modules/@comis/cli/dist/wizard/steps/11-daemon-start.js +130 -0
  48. package/node_modules/@comis/cli/package.json +1 -1
  49. package/node_modules/@comis/core/dist/bootstrap.js +5 -0
  50. package/node_modules/@comis/core/dist/config/env-layer.d.ts +31 -0
  51. package/node_modules/@comis/core/dist/config/env-layer.js +41 -0
  52. package/node_modules/@comis/core/dist/config/immutable-keys.d.ts +2 -2
  53. package/node_modules/@comis/core/dist/config/immutable-keys.js +8 -3
  54. package/node_modules/@comis/core/dist/config/layered.d.ts +9 -0
  55. package/node_modules/@comis/core/dist/config/layered.js +11 -0
  56. package/node_modules/@comis/core/dist/config/managed-sections.d.ts +43 -4
  57. package/node_modules/@comis/core/dist/config/managed-sections.js +100 -6
  58. package/node_modules/@comis/core/dist/config/schema-agent.d.ts +39 -0
  59. package/node_modules/@comis/core/dist/config/schema-agent.js +14 -0
  60. package/node_modules/@comis/core/dist/config/schema.d.ts +4 -0
  61. package/node_modules/@comis/core/dist/config/schema.js +14 -0
  62. package/node_modules/@comis/core/dist/domain/execution-graph.d.ts +1 -1
  63. package/node_modules/@comis/core/dist/event-bus/events-agent.d.ts +17 -2
  64. package/node_modules/@comis/core/dist/exports/config.d.ts +2 -2
  65. package/node_modules/@comis/core/dist/exports/config.js +1 -1
  66. package/node_modules/@comis/core/package.json +1 -1
  67. package/node_modules/@comis/daemon/dist/daemon.d.ts +22 -0
  68. package/node_modules/@comis/daemon/dist/daemon.js +45 -0
  69. package/node_modules/@comis/daemon/dist/rpc/agent-handlers.d.ts +5 -2
  70. package/node_modules/@comis/daemon/dist/rpc/agent-handlers.js +80 -1
  71. package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.d.ts +67 -0
  72. package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.js +139 -0
  73. package/node_modules/@comis/daemon/dist/rpc/model-handlers.d.ts +3 -0
  74. package/node_modules/@comis/daemon/dist/rpc/model-handlers.js +29 -5
  75. package/node_modules/@comis/daemon/dist/rpc/probe-provider-auth.d.ts +30 -0
  76. package/node_modules/@comis/daemon/dist/rpc/probe-provider-auth.js +59 -0
  77. package/node_modules/@comis/daemon/dist/rpc/provider-handlers.d.ts +37 -0
  78. package/node_modules/@comis/daemon/dist/rpc/provider-handlers.js +330 -0
  79. package/node_modules/@comis/daemon/dist/rpc/rpc-dispatch.js +18 -1
  80. package/node_modules/@comis/daemon/dist/setup-docker-restart-warn.d.ts +4 -0
  81. package/node_modules/@comis/daemon/dist/setup-docker-restart-warn.js +30 -0
  82. package/node_modules/@comis/daemon/dist/wiring/setup-agents.d.ts +3 -1
  83. package/node_modules/@comis/daemon/dist/wiring/setup-agents.js +28 -2
  84. package/node_modules/@comis/daemon/dist/wiring/setup-cross-session.js +1 -0
  85. package/node_modules/@comis/daemon/dist/wiring/setup-tools.js +7 -4
  86. package/node_modules/@comis/daemon/package.json +1 -1
  87. package/node_modules/@comis/gateway/package.json +1 -1
  88. package/node_modules/@comis/infra/dist/index.d.ts +1 -0
  89. package/node_modules/@comis/infra/dist/index.js +2 -0
  90. package/node_modules/@comis/infra/dist/runtime/is-docker.d.ts +1 -0
  91. package/node_modules/@comis/infra/dist/runtime/is-docker.js +25 -0
  92. package/node_modules/@comis/infra/package.json +1 -1
  93. package/node_modules/@comis/memory/package.json +1 -1
  94. package/node_modules/@comis/scheduler/package.json +1 -1
  95. package/node_modules/@comis/shared/package.json +1 -1
  96. package/node_modules/@comis/skills/dist/bridge/tool-metadata-registry.js +1 -3
  97. package/node_modules/@comis/skills/dist/builtin/platform/admin-manage-factory.js +24 -1
  98. package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.d.ts +53 -7
  99. package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.js +218 -24
  100. package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.d.ts +4 -1
  101. package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.js +16 -1
  102. package/node_modules/@comis/skills/dist/builtin/platform/index.d.ts +1 -1
  103. package/node_modules/@comis/skills/dist/builtin/platform/index.js +1 -1
  104. package/node_modules/@comis/skills/dist/builtin/platform/providers-manage-tool.d.ts +56 -0
  105. package/node_modules/@comis/skills/dist/builtin/platform/providers-manage-tool.js +203 -0
  106. package/node_modules/@comis/skills/dist/index.d.ts +1 -1
  107. package/node_modules/@comis/skills/dist/index.js +2 -2
  108. package/node_modules/@comis/skills/dist/policy/tool-policy.js +0 -1
  109. package/node_modules/@comis/skills/package.json +1 -1
  110. package/node_modules/@comis/web/dist/assets/{agent-detail-BG9MGWWj.js → agent-detail-DqL6Artv.js} +270 -270
  111. package/node_modules/@comis/web/dist/assets/agent-editor-CNM_h94Y.js +2173 -0
  112. package/node_modules/@comis/web/dist/assets/{agent-list-LHCJ4rw2.js → agent-list-Dbh-xD_F.js} +170 -170
  113. package/node_modules/@comis/web/dist/assets/{approvals-q9VH_IKr.js → approvals-C-K6hN2U.js} +13 -13
  114. package/node_modules/@comis/web/dist/assets/billing-view-C1DmtyzK.js +375 -0
  115. package/node_modules/@comis/web/dist/assets/{channel-detail-CaInesJM.js → channel-detail-CtCH22N1.js} +265 -265
  116. package/node_modules/@comis/web/dist/assets/channel-list-C7xXn-60.js +323 -0
  117. package/node_modules/@comis/web/dist/assets/{chat-console-CNmzl0JW.js → chat-console-C51pjFwk.js} +243 -246
  118. package/node_modules/@comis/web/dist/assets/{config-editor-DX4ITw6y.js → config-editor-BLArYRB7.js} +477 -477
  119. package/node_modules/@comis/web/dist/assets/{context-dag-browser-BwiaF5tf.js → context-dag-browser-fuyMinNI.js} +105 -105
  120. package/node_modules/@comis/web/dist/assets/{context-engine-BZ5Am6hA.js → context-engine-Bngf2bH0.js} +136 -136
  121. package/node_modules/@comis/web/dist/assets/decorate-BvWYovGE.js +38 -0
  122. package/node_modules/@comis/web/dist/assets/{delivery-view-OfBZof-m.js → delivery-view-C80hucxX.js} +134 -134
  123. package/node_modules/@comis/web/dist/assets/{diagnostics-view-YFwCxgr2.js → diagnostics-view-Cl4VbHZ6.js} +82 -82
  124. package/node_modules/@comis/web/dist/assets/directive-BOYXJ-K-.js +1 -0
  125. package/node_modules/@comis/web/dist/assets/{extract-variables-BM5qyK-s.js → extract-variables-B7-Doo7l.js} +39 -39
  126. package/node_modules/@comis/web/dist/assets/{ic-array-editor-B7m6x7-S.js → ic-array-editor-BLoEyeLS.js} +29 -29
  127. package/node_modules/@comis/web/dist/assets/{ic-breadcrumb-CUMpp3BL.js → ic-breadcrumb-DqN6G3gc.js} +16 -16
  128. package/node_modules/@comis/web/dist/assets/{ic-budget-segment-bar-BtJ6x5mN.js → ic-budget-segment-bar-zLsMzjDO.js} +20 -20
  129. package/node_modules/@comis/web/dist/assets/ic-chat-message-ByFUoMm6.js +352 -0
  130. package/node_modules/@comis/web/dist/assets/{ic-confirm-dialog-CCDbB04e.js → ic-confirm-dialog-DGlPbV1T.js} +26 -26
  131. package/node_modules/@comis/web/dist/assets/{ic-connection-dot-CnT1b8xr.js → ic-connection-dot-C4nDHgY2.js} +13 -13
  132. package/node_modules/@comis/web/dist/assets/ic-data-table-CKIvr-ag.js +277 -0
  133. package/node_modules/@comis/web/dist/assets/ic-delivery-row-B3YwjjuM.js +67 -0
  134. package/node_modules/@comis/web/dist/assets/{ic-detail-panel-BF83r-if.js → ic-detail-panel-DiCe4hLr.js} +27 -27
  135. package/node_modules/@comis/web/dist/assets/{ic-empty-state-60l2ePhd.js → ic-empty-state-CM3Wbj2f.js} +19 -19
  136. package/node_modules/@comis/web/dist/assets/ic-graph-canvas-ByRjij68.js +359 -0
  137. package/node_modules/@comis/web/dist/assets/ic-icon-BGNCCPpZ.js +33 -0
  138. package/node_modules/@comis/web/dist/assets/{ic-layer-waterfall-COvEYMg5.js → ic-layer-waterfall-WkaFyu-l.js} +18 -18
  139. package/node_modules/@comis/web/dist/assets/ic-relative-time-B3UAnTqg.js +12 -0
  140. package/node_modules/@comis/web/dist/assets/{ic-search-input-CSOxY9g7.js → ic-search-input-B02AGw1i.js} +22 -22
  141. package/node_modules/@comis/web/dist/assets/{ic-select-Ce-Raudx.js → ic-select-BqfZISjw.js} +29 -29
  142. package/node_modules/@comis/web/dist/assets/ic-tabs-yBjkWKJH.js +95 -0
  143. package/node_modules/@comis/web/dist/assets/ic-tag-CvMVQFRR.js +33 -0
  144. package/node_modules/@comis/web/dist/assets/{ic-time-range-picker-CypCT68y.js → ic-time-range-picker-DXbYeBmY.js} +31 -31
  145. package/node_modules/@comis/web/dist/assets/{ic-tool-call-7MaXSsCW.js → ic-tool-call-Bh5kq-yY.js} +51 -51
  146. package/node_modules/@comis/web/dist/assets/index-BBkuC-EU.js +2792 -0
  147. package/node_modules/@comis/web/dist/assets/index-CVEaS9aY.css +2 -0
  148. package/node_modules/@comis/web/dist/assets/{mcp-management-BNZPnpDn.js → mcp-management-DB-phOo7.js} +209 -209
  149. package/node_modules/@comis/web/dist/assets/{media-config-BBvTYxOX.js → media-config-CRqZ1ZUH.js} +154 -154
  150. package/node_modules/@comis/web/dist/assets/{media-test-BkK3RCRK.js → media-test-C9vE20Oy.js} +259 -259
  151. package/node_modules/@comis/web/dist/assets/{memory-inspector-1hDGCGat.js → memory-inspector-CeqfnxMZ.js} +450 -450
  152. package/node_modules/@comis/web/dist/assets/{message-center-CXefwsUu.js → message-center-Daup7Mof.js} +290 -290
  153. package/node_modules/@comis/web/dist/assets/{models-C1qcU_j3.js → models-DLYnEU8E.js} +371 -371
  154. package/node_modules/@comis/web/dist/assets/observability-types-D0tkwElU.js +1 -0
  155. package/node_modules/@comis/web/dist/assets/{observe-view-C0VBhX4C.js → observe-view-BTSt_PO5.js} +399 -399
  156. package/node_modules/@comis/web/dist/assets/pipeline-builder-DknfzyLt.js +1495 -0
  157. package/node_modules/@comis/web/dist/assets/{pipeline-history-DkfOQ6SW.js → pipeline-history-JnHZdeU_.js} +124 -124
  158. package/node_modules/@comis/web/dist/assets/{pipeline-history-detail-hyHgD0ai.js → pipeline-history-detail-Dg4knsEb.js} +65 -65
  159. package/node_modules/@comis/web/dist/assets/{pipeline-list-BPW8hV-q.js → pipeline-list-AEnibjsp.js} +227 -227
  160. package/node_modules/@comis/web/dist/assets/{pipeline-monitor-Bip16T7e.js → pipeline-monitor-DG7RbIOO.js} +298 -298
  161. package/node_modules/@comis/web/dist/assets/{scheduler-BGgwKd06.js → scheduler-uL1fYKAT.js} +486 -486
  162. package/node_modules/@comis/web/dist/assets/{security-D15st4xx.js → security-C3DywRLH.js} +389 -389
  163. package/node_modules/@comis/web/dist/assets/{session-detail-SGEYNJ0M.js → session-detail-BtqCNWXV.js} +294 -294
  164. package/node_modules/@comis/web/dist/assets/session-key-parser-Dkqcj2Ss.js +1 -0
  165. package/node_modules/@comis/web/dist/assets/session-list-CJXWa2XT.js +231 -0
  166. package/node_modules/@comis/web/dist/assets/{setup-wizard-nT0tz9QP.js → setup-wizard-ywn7oJvu.js} +486 -494
  167. package/node_modules/@comis/web/dist/assets/{skills-D8yVfSUy.js → skills-DX0KYnWD.js} +329 -329
  168. package/node_modules/@comis/web/dist/assets/{subagents-HHXMeHYo.js → subagents-B8p5YJEB.js} +74 -74
  169. package/node_modules/@comis/web/dist/assets/{workspace-manager-BQlr10iH.js → workspace-manager-CgzNIrw1.js} +236 -236
  170. package/node_modules/@comis/web/dist/index.html +3 -2
  171. package/node_modules/@comis/web/package.json +1 -1
  172. package/package.json +15 -15
  173. package/node_modules/@comis/skills/dist/builtin/platform/agents-list-tool.d.ts +0 -19
  174. package/node_modules/@comis/skills/dist/builtin/platform/agents-list-tool.js +0 -39
  175. package/node_modules/@comis/web/dist/assets/agent-editor-C26Q_xCs.js +0 -2173
  176. package/node_modules/@comis/web/dist/assets/billing-view-CtYvBqTE.js +0 -375
  177. package/node_modules/@comis/web/dist/assets/channel-list-B8dj3O9a.js +0 -323
  178. package/node_modules/@comis/web/dist/assets/directive-DoeGSK_T.js +0 -1
  179. package/node_modules/@comis/web/dist/assets/ic-chat-message-CFyDJd0z.js +0 -352
  180. package/node_modules/@comis/web/dist/assets/ic-data-table-CKUNTxHw.js +0 -277
  181. package/node_modules/@comis/web/dist/assets/ic-delivery-row-GP5Fkygs.js +0 -67
  182. package/node_modules/@comis/web/dist/assets/ic-graph-canvas-C8FuSMe1.js +0 -359
  183. package/node_modules/@comis/web/dist/assets/ic-icon-xeGTVhVG.js +0 -33
  184. package/node_modules/@comis/web/dist/assets/ic-relative-time-3FqpjeAI.js +0 -12
  185. package/node_modules/@comis/web/dist/assets/ic-tabs-B7QtM_v8.js +0 -95
  186. package/node_modules/@comis/web/dist/assets/ic-tag-CPPUnDLF.js +0 -33
  187. package/node_modules/@comis/web/dist/assets/index-CEcM1R_C.js +0 -2830
  188. package/node_modules/@comis/web/dist/assets/index-CIJFuItj.css +0 -1
  189. package/node_modules/@comis/web/dist/assets/observability-types-D7jUtSde.js +0 -1
  190. package/node_modules/@comis/web/dist/assets/pipeline-builder-DcUUIrm0.js +0 -1496
  191. package/node_modules/@comis/web/dist/assets/session-key-parser-DPORMVyU.js +0 -1
  192. 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 (Fix #2): activates only when the executor
200
- // memoized a `{ drop: true }` drift decision for this execute() call.
201
- // NOT gated on `model.reasoning` because Gemini's `thoughtSignature` lives
202
- // on toolCall blocks even when the model itself isn't flagged as reasoning.
203
- // The layer no-ops when the gate is closed, so unconditional addition is
204
- // cheap; opt-in is via the executor providing `getReplayDriftMode`.
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