comisai 1.0.25 → 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 (145) 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/config/immutable-keys.d.ts +2 -2
  50. package/node_modules/@comis/core/dist/config/immutable-keys.js +8 -3
  51. package/node_modules/@comis/core/dist/config/managed-sections.d.ts +43 -4
  52. package/node_modules/@comis/core/dist/config/managed-sections.js +100 -6
  53. package/node_modules/@comis/core/dist/config/schema-agent.d.ts +39 -0
  54. package/node_modules/@comis/core/dist/config/schema-agent.js +14 -0
  55. package/node_modules/@comis/core/dist/config/schema.d.ts +4 -0
  56. package/node_modules/@comis/core/dist/config/schema.js +14 -0
  57. package/node_modules/@comis/core/dist/domain/execution-graph.d.ts +1 -1
  58. package/node_modules/@comis/core/dist/event-bus/events-agent.d.ts +17 -2
  59. package/node_modules/@comis/core/dist/exports/config.d.ts +2 -2
  60. package/node_modules/@comis/core/dist/exports/config.js +1 -1
  61. package/node_modules/@comis/core/package.json +1 -1
  62. package/node_modules/@comis/daemon/dist/daemon.d.ts +22 -0
  63. package/node_modules/@comis/daemon/dist/daemon.js +42 -0
  64. package/node_modules/@comis/daemon/dist/rpc/agent-handlers.d.ts +5 -2
  65. package/node_modules/@comis/daemon/dist/rpc/agent-handlers.js +80 -1
  66. package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.d.ts +67 -0
  67. package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.js +139 -0
  68. package/node_modules/@comis/daemon/dist/rpc/model-handlers.d.ts +3 -0
  69. package/node_modules/@comis/daemon/dist/rpc/model-handlers.js +29 -5
  70. package/node_modules/@comis/daemon/dist/rpc/probe-provider-auth.d.ts +30 -0
  71. package/node_modules/@comis/daemon/dist/rpc/probe-provider-auth.js +59 -0
  72. package/node_modules/@comis/daemon/dist/rpc/provider-handlers.d.ts +37 -0
  73. package/node_modules/@comis/daemon/dist/rpc/provider-handlers.js +330 -0
  74. package/node_modules/@comis/daemon/dist/rpc/rpc-dispatch.js +18 -1
  75. package/node_modules/@comis/daemon/dist/setup-docker-restart-warn.d.ts +4 -0
  76. package/node_modules/@comis/daemon/dist/setup-docker-restart-warn.js +30 -0
  77. package/node_modules/@comis/daemon/dist/wiring/setup-agents.d.ts +3 -1
  78. package/node_modules/@comis/daemon/dist/wiring/setup-agents.js +28 -2
  79. package/node_modules/@comis/daemon/dist/wiring/setup-cross-session.js +1 -0
  80. package/node_modules/@comis/daemon/dist/wiring/setup-tools.js +7 -4
  81. package/node_modules/@comis/daemon/package.json +1 -1
  82. package/node_modules/@comis/gateway/package.json +1 -1
  83. package/node_modules/@comis/infra/dist/index.d.ts +1 -0
  84. package/node_modules/@comis/infra/dist/index.js +2 -0
  85. package/node_modules/@comis/infra/dist/runtime/is-docker.d.ts +1 -0
  86. package/node_modules/@comis/infra/dist/runtime/is-docker.js +25 -0
  87. package/node_modules/@comis/infra/package.json +1 -1
  88. package/node_modules/@comis/memory/package.json +1 -1
  89. package/node_modules/@comis/scheduler/package.json +1 -1
  90. package/node_modules/@comis/shared/package.json +1 -1
  91. package/node_modules/@comis/skills/dist/bridge/tool-metadata-registry.js +1 -3
  92. package/node_modules/@comis/skills/dist/builtin/platform/admin-manage-factory.js +24 -1
  93. package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.d.ts +53 -7
  94. package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.js +218 -24
  95. package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.d.ts +4 -1
  96. package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.js +16 -1
  97. package/node_modules/@comis/skills/dist/builtin/platform/index.d.ts +1 -1
  98. package/node_modules/@comis/skills/dist/builtin/platform/index.js +1 -1
  99. package/node_modules/@comis/skills/dist/builtin/platform/providers-manage-tool.d.ts +56 -0
  100. package/node_modules/@comis/skills/dist/builtin/platform/providers-manage-tool.js +203 -0
  101. package/node_modules/@comis/skills/dist/index.d.ts +1 -1
  102. package/node_modules/@comis/skills/dist/index.js +2 -2
  103. package/node_modules/@comis/skills/dist/policy/tool-policy.js +0 -1
  104. package/node_modules/@comis/skills/package.json +1 -1
  105. package/node_modules/@comis/web/dist/assets/{agent-detail-ru-AhppM.js → agent-detail-DqL6Artv.js} +1 -1
  106. package/node_modules/@comis/web/dist/assets/{agent-editor-hjwRuFVp.js → agent-editor-CNM_h94Y.js} +1 -1
  107. package/node_modules/@comis/web/dist/assets/{agent-list-6Uotjatr.js → agent-list-Dbh-xD_F.js} +1 -1
  108. package/node_modules/@comis/web/dist/assets/{billing-view-CxysXH0p.js → billing-view-C1DmtyzK.js} +1 -1
  109. package/node_modules/@comis/web/dist/assets/{channel-detail-BBCKtmne.js → channel-detail-CtCH22N1.js} +1 -1
  110. package/node_modules/@comis/web/dist/assets/{channel-list-FkfeOLBQ.js → channel-list-C7xXn-60.js} +1 -1
  111. package/node_modules/@comis/web/dist/assets/{chat-console-BumBaIgO.js → chat-console-C51pjFwk.js} +1 -1
  112. package/node_modules/@comis/web/dist/assets/{config-editor-C9BSwHGy.js → config-editor-BLArYRB7.js} +1 -1
  113. package/node_modules/@comis/web/dist/assets/{context-dag-browser-BHm00mJD.js → context-dag-browser-fuyMinNI.js} +1 -1
  114. package/node_modules/@comis/web/dist/assets/{context-engine-BENY3pWE.js → context-engine-Bngf2bH0.js} +1 -1
  115. package/node_modules/@comis/web/dist/assets/{delivery-view-BCnkPsAp.js → delivery-view-C80hucxX.js} +1 -1
  116. package/node_modules/@comis/web/dist/assets/{diagnostics-view-C_jQFG2H.js → diagnostics-view-Cl4VbHZ6.js} +1 -1
  117. package/node_modules/@comis/web/dist/assets/{ic-chat-message-FdQcZsSQ.js → ic-chat-message-ByFUoMm6.js} +1 -1
  118. package/node_modules/@comis/web/dist/assets/{ic-connection-dot-BgYiK2N4.js → ic-connection-dot-C4nDHgY2.js} +1 -1
  119. package/node_modules/@comis/web/dist/assets/{ic-tool-call-DMPHsLyx.js → ic-tool-call-Bh5kq-yY.js} +1 -1
  120. package/node_modules/@comis/web/dist/assets/{index-FLPhHz8p.js → index-BBkuC-EU.js} +2 -2
  121. package/node_modules/@comis/web/dist/assets/{mcp-management-5jyScQis.js → mcp-management-DB-phOo7.js} +1 -1
  122. package/node_modules/@comis/web/dist/assets/{media-config-J9oT9PPs.js → media-config-CRqZ1ZUH.js} +1 -1
  123. package/node_modules/@comis/web/dist/assets/{media-test-DGTCtM8-.js → media-test-C9vE20Oy.js} +1 -1
  124. package/node_modules/@comis/web/dist/assets/{memory-inspector-D5Re9ptG.js → memory-inspector-CeqfnxMZ.js} +1 -1
  125. package/node_modules/@comis/web/dist/assets/{message-center-cRLK6ZmG.js → message-center-Daup7Mof.js} +1 -1
  126. package/node_modules/@comis/web/dist/assets/{models-D5vu07MR.js → models-DLYnEU8E.js} +1 -1
  127. package/node_modules/@comis/web/dist/assets/{observe-view-CalNNEmd.js → observe-view-BTSt_PO5.js} +1 -1
  128. package/node_modules/@comis/web/dist/assets/{pipeline-builder-DUYDGwZf.js → pipeline-builder-DknfzyLt.js} +1 -1
  129. package/node_modules/@comis/web/dist/assets/{pipeline-history-BAO8brOe.js → pipeline-history-JnHZdeU_.js} +1 -1
  130. package/node_modules/@comis/web/dist/assets/{pipeline-history-detail-DectIoQt.js → pipeline-history-detail-Dg4knsEb.js} +1 -1
  131. package/node_modules/@comis/web/dist/assets/{pipeline-list-BHlaBKww.js → pipeline-list-AEnibjsp.js} +1 -1
  132. package/node_modules/@comis/web/dist/assets/{pipeline-monitor-BhtpNEHf.js → pipeline-monitor-DG7RbIOO.js} +1 -1
  133. package/node_modules/@comis/web/dist/assets/{scheduler-VafN_8xi.js → scheduler-uL1fYKAT.js} +1 -1
  134. package/node_modules/@comis/web/dist/assets/{security-QQXMRTlo.js → security-C3DywRLH.js} +1 -1
  135. package/node_modules/@comis/web/dist/assets/{session-detail-BpZ_8Yih.js → session-detail-BtqCNWXV.js} +1 -1
  136. package/node_modules/@comis/web/dist/assets/{session-list-DfCm8Cec.js → session-list-CJXWa2XT.js} +1 -1
  137. package/node_modules/@comis/web/dist/assets/{setup-wizard-C-z477CG.js → setup-wizard-ywn7oJvu.js} +1 -1
  138. package/node_modules/@comis/web/dist/assets/{skills-BCOGPf6s.js → skills-DX0KYnWD.js} +1 -1
  139. package/node_modules/@comis/web/dist/assets/{subagents-l-auUraL.js → subagents-B8p5YJEB.js} +1 -1
  140. package/node_modules/@comis/web/dist/assets/{workspace-manager-DlvBixiq.js → workspace-manager-CgzNIrw1.js} +1 -1
  141. package/node_modules/@comis/web/dist/index.html +1 -1
  142. package/node_modules/@comis/web/package.json +1 -1
  143. package/package.json +13 -13
  144. package/node_modules/@comis/skills/dist/builtin/platform/agents-list-tool.d.ts +0 -19
  145. package/node_modules/@comis/skills/dist/builtin/platform/agents-list-tool.js +0 -39
@@ -1,45 +1,67 @@
1
1
  /**
2
2
  * Signature replay scrubber context engine layer.
3
3
  *
4
- * Activates only when `getReplayDriftMode()` returns `{ drop: true }`. When
5
- * active, drops every `type:"thinking"` block (signed and redacted alike)
6
- * and strips `thoughtSignature` from every `type:"toolCall"` /
7
- * `type:"tool_call"` block across the whole history. Respects
8
- * `budget.cacheFenceIndex` exactly like `thinking-block-cleaner`: messages
9
- * at or below the fence are passed through unchanged.
4
+ * Always-on policy: strips signed `thinking` blocks entirely from EVERY
5
+ * assistant message (latest included) and strips `thoughtSignature` from
6
+ * `toolCall` / `tool_call` blocks in the same messages. `redacted_thinking`
7
+ * blocks are never modified, anywhere.
10
8
  *
11
- * Provider coverage rationale: the scrubber is NOT gated on `model.reasoning`
12
- * because Gemini's `thoughtSignature` lives on toolCall blocks even when the
13
- * model itself is not flagged as reasoning. Cost of running a no-op loop is
14
- * negligible vs the savings of preventing a 400-rejection round trip.
9
+ * Rationale: Anthropic's signed-thinking validation operates on the full
10
+ * (system + tools + history) prefix. After 8 quick tasks of progressively
11
+ * narrower drift detection (gj6 kvl) we proved targeted detection is
12
+ * intractable; trace 679c8927 had stable tools (49138 bytes across 4 turns)
13
+ * but the system prompt grew +1824 bytes and the 400 fired anyway.
14
+ *
15
+ * 260428-lm6 introduced an unconditional drop that preserved the LATEST
16
+ * assistant message's signatures, on the theory that the immediate-next
17
+ * continuation could still validate them. 260428-nzp's repro proved that
18
+ * carve-out doesn't work: cross-turn signature validation covers the whole
19
+ * request body (system + tools + history) and comis's dynamic context
20
+ * guarantees the surrounding context changes turn-to-turn. So the latest's
21
+ * signatures get invalidated too. Drop them all.
22
+ *
23
+ * Provider coverage: NOT gated on `model.reasoning` because Gemini's
24
+ * `thoughtSignature` lives on toolCall blocks even when the model itself
25
+ * is not flagged as reasoning. Cost is one walk over assistant messages,
26
+ * no I/O.
15
27
  *
16
28
  * Immutability: never mutates input messages or arrays. Returns new arrays
17
- * and shallow-copied messages only when changes are needed. When drift is
18
- * detected but the history happens to contain no thinking blocks or signed
19
- * toolCalls (e.g. fresh session), returns the original `messages` reference
20
- * (zero allocation).
29
+ * and shallow-copied messages only when changes are needed. When the
30
+ * history contains zero touchable signed state (e.g., fresh session, or
31
+ * single-assistant turn) returns the original `messages` reference (zero
32
+ * allocation).
21
33
  *
22
34
  * @module
23
35
  */
36
+ import type { ComisLogger } from "@comis/infra";
24
37
  import type { ContextLayer } from "./types.js";
25
38
  import type { DriftCheck } from "../executor/replay-drift-detector.js";
26
- /** Stats reported via the `onScrubbed` callback. */
27
- interface ScrubbedStats {
28
- /** Number of thinking blocks dropped across the whole history. */
29
- dropped: number;
30
- /** Number of thoughtSignatures stripped from toolCall blocks. */
31
- signaturesStripped: number;
32
- /** Drift reason that triggered the scrub (forwarded for observability). */
33
- reason?: string;
34
- }
35
39
  /** Dependencies for `createSignatureReplayScrubber`. */
36
40
  export interface SignatureReplayScrubberDeps {
37
- /** Getter for the per-execute() memoized replay drift decision. The
38
- * layer no-ops when this returns undefined or `{ drop: false }`. */
39
- getReplayDriftMode: () => DriftCheck | undefined;
40
- /** Optional callback invoked exactly once at the end of `apply()` with
41
- * the scrub counts and drift reason. */
42
- onScrubbed?: (stats: ScrubbedStats) => void;
41
+ /** Kept on the deps shape for back-compat with existing wiring; unused
42
+ * by this layer. The thinking cleaner's keepTurns override in
43
+ * executor-context-engine-setup.ts still consults the same closure for
44
+ * unrelated reasons, so leaving the field plumbed avoids a chain of
45
+ * unrelated edits in callers. */
46
+ getReplayDriftMode?: () => DriftCheck | undefined;
47
+ /** Optional callback invoked at the end of `apply()` with the scrub
48
+ * counts. Fields preserve the legacy `dropped` / `signaturesStripped`
49
+ * names so the context-engine snapshot consumer keeps working without
50
+ * churn; the new explicit counter names are also included. */
51
+ onScrubbed?: (stats: {
52
+ scrubbedAssistantMessages: number;
53
+ blocksAffected: number;
54
+ toolCallsAffected: number;
55
+ latestAssistantIdx: number;
56
+ /** Alias of blocksAffected (legacy field name preserved). */
57
+ dropped: number;
58
+ /** Alias of toolCallsAffected (legacy field name preserved). */
59
+ signaturesStripped: number;
60
+ /** Legacy; always undefined now (no drift reason in the always-on path). */
61
+ reason?: string;
62
+ }) => void;
63
+ /** Required: per-execute INFO log emission. */
64
+ logger: ComisLogger;
43
65
  }
44
66
  /**
45
67
  * Create the signature-replay-scrubber pipeline layer.
@@ -48,4 +70,3 @@ export interface SignatureReplayScrubberDeps {
48
70
  * `signature-surrogate-guard` (and well before `reasoning-tag-stripper`).
49
71
  */
50
72
  export declare function createSignatureReplayScrubber(deps: SignatureReplayScrubberDeps): ContextLayer;
51
- export {};
@@ -2,23 +2,35 @@
2
2
  /**
3
3
  * Signature replay scrubber context engine layer.
4
4
  *
5
- * Activates only when `getReplayDriftMode()` returns `{ drop: true }`. When
6
- * active, drops every `type:"thinking"` block (signed and redacted alike)
7
- * and strips `thoughtSignature` from every `type:"toolCall"` /
8
- * `type:"tool_call"` block across the whole history. Respects
9
- * `budget.cacheFenceIndex` exactly like `thinking-block-cleaner`: messages
10
- * at or below the fence are passed through unchanged.
5
+ * Always-on policy: strips signed `thinking` blocks entirely from EVERY
6
+ * assistant message (latest included) and strips `thoughtSignature` from
7
+ * `toolCall` / `tool_call` blocks in the same messages. `redacted_thinking`
8
+ * blocks are never modified, anywhere.
11
9
  *
12
- * Provider coverage rationale: the scrubber is NOT gated on `model.reasoning`
13
- * because Gemini's `thoughtSignature` lives on toolCall blocks even when the
14
- * model itself is not flagged as reasoning. Cost of running a no-op loop is
15
- * negligible vs the savings of preventing a 400-rejection round trip.
10
+ * Rationale: Anthropic's signed-thinking validation operates on the full
11
+ * (system + tools + history) prefix. After 8 quick tasks of progressively
12
+ * narrower drift detection (gj6 kvl) we proved targeted detection is
13
+ * intractable; trace 679c8927 had stable tools (49138 bytes across 4 turns)
14
+ * but the system prompt grew +1824 bytes and the 400 fired anyway.
15
+ *
16
+ * 260428-lm6 introduced an unconditional drop that preserved the LATEST
17
+ * assistant message's signatures, on the theory that the immediate-next
18
+ * continuation could still validate them. 260428-nzp's repro proved that
19
+ * carve-out doesn't work: cross-turn signature validation covers the whole
20
+ * request body (system + tools + history) and comis's dynamic context
21
+ * guarantees the surrounding context changes turn-to-turn. So the latest's
22
+ * signatures get invalidated too. Drop them all.
23
+ *
24
+ * Provider coverage: NOT gated on `model.reasoning` because Gemini's
25
+ * `thoughtSignature` lives on toolCall blocks even when the model itself
26
+ * is not flagged as reasoning. Cost is one walk over assistant messages,
27
+ * no I/O.
16
28
  *
17
29
  * Immutability: never mutates input messages or arrays. Returns new arrays
18
- * and shallow-copied messages only when changes are needed. When drift is
19
- * detected but the history happens to contain no thinking blocks or signed
20
- * toolCalls (e.g. fresh session), returns the original `messages` reference
21
- * (zero allocation).
30
+ * and shallow-copied messages only when changes are needed. When the
31
+ * history contains zero touchable signed state (e.g., fresh session, or
32
+ * single-assistant turn) returns the original `messages` reference (zero
33
+ * allocation).
22
34
  *
23
35
  * @module
24
36
  */
@@ -32,16 +44,22 @@ export function createSignatureReplayScrubber(deps) {
32
44
  return {
33
45
  name: "signature-replay-scrubber",
34
46
  async apply(messages, budget) {
35
- const drift = deps.getReplayDriftMode();
36
- if (!drift || !drift.drop) {
37
- // Gate closed → no-op, return same reference (zero allocation).
47
+ if (messages.length === 0)
38
48
  return messages;
49
+ // Find the latest assistant message index. If none, no scrub.
50
+ let latestIdx = -1;
51
+ for (let i = 0; i < messages.length; i++) {
52
+ // eslint-disable-next-line security/detect-object-injection -- numeric index
53
+ const m = messages[i];
54
+ if (m && m.role === "assistant")
55
+ latestIdx = i;
39
56
  }
40
- if (messages.length === 0)
57
+ if (latestIdx < 0)
41
58
  return messages;
59
+ let scrubbedAssistantMessages = 0;
60
+ let blocksAffected = 0;
61
+ let toolCallsAffected = 0;
42
62
  let anyChanged = false;
43
- let dropped = 0;
44
- let signaturesStripped = 0;
45
63
  const result = new Array(messages.length);
46
64
  for (let i = 0; i < messages.length; i++) {
47
65
  // eslint-disable-next-line security/detect-object-injection -- numeric index
@@ -58,39 +76,73 @@ export function createSignatureReplayScrubber(deps) {
58
76
  result[i] = original;
59
77
  continue;
60
78
  }
79
+ // Assistant message past the fence — walk content blocks. Latest
80
+ // included: cross-turn signature validation invalidates it too.
61
81
  const content = msg.content;
62
82
  let messageChanged = false;
63
- const newContent = [];
83
+ const newContent = new Array(content.length);
64
84
  for (let j = 0; j < content.length; j++) {
65
85
  // eslint-disable-next-line security/detect-object-injection -- numeric index
66
86
  const block = content[j];
67
87
  if (!block || typeof block !== "object") {
68
- newContent.push(block);
88
+ // eslint-disable-next-line security/detect-object-injection -- numeric index
89
+ newContent[j] = block;
69
90
  continue;
70
91
  }
71
92
  const b = block;
72
93
  if (b.type === "thinking") {
73
- // Drop signed AND redacted alike — drift mode invalidates the
74
- // entire prefix, so retaining redacted thinking just keeps a
75
- // surface that the next replay can still reject.
76
- dropped++;
77
- messageChanged = true;
94
+ // redacted_thinking: never modified.
95
+ if (b.redacted === true) {
96
+ // eslint-disable-next-line security/detect-object-injection -- numeric index
97
+ newContent[j] = block;
98
+ continue;
99
+ }
100
+ // Signed thinking: strip the block entirely. Clearing the
101
+ // signature to "" was previously attempted but Anthropic only
102
+ // tolerates it while the prompt cache covers the prefix. On
103
+ // cache eviction the full request is re-validated and a modified
104
+ // thinkingSignature triggers a 400 ("thinking blocks cannot be
105
+ // modified"). Stripping the block avoids this: Anthropic accepts
106
+ // conversations where thinking blocks are absent from historical
107
+ // turns. Reasoning-token continuity is lost, but that is
108
+ // strictly better than a hard 400 that kills the session.
109
+ if (typeof b.thinkingSignature === "string" && b.thinkingSignature.length > 0) {
110
+ // Mark as null — filtered out below.
111
+ // eslint-disable-next-line security/detect-object-injection -- numeric index
112
+ newContent[j] = null;
113
+ blocksAffected++;
114
+ messageChanged = true;
115
+ continue;
116
+ }
117
+ // eslint-disable-next-line security/detect-object-injection -- numeric index
118
+ newContent[j] = block;
78
119
  continue;
79
120
  }
80
- if ((b.type === "toolCall" || b.type === "tool_call") && b.thoughtSignature !== undefined) {
81
- // Shallow-copy and drop only the thoughtSignature property.
121
+ if ((b.type === "toolCall" || b.type === "tool_call") &&
122
+ b.thoughtSignature !== undefined &&
123
+ b.thoughtSignature !== null &&
124
+ !(typeof b.thoughtSignature === "string" && b.thoughtSignature.length === 0)) {
82
125
  const copy = { ...b };
83
126
  delete copy.thoughtSignature;
84
- newContent.push(copy);
85
- signaturesStripped++;
127
+ // eslint-disable-next-line security/detect-object-injection -- numeric index
128
+ newContent[j] = copy;
129
+ toolCallsAffected++;
86
130
  messageChanged = true;
87
131
  continue;
88
132
  }
89
- newContent.push(block);
133
+ // eslint-disable-next-line security/detect-object-injection -- numeric index
134
+ newContent[j] = block;
90
135
  }
91
136
  if (messageChanged) {
137
+ const filtered = newContent.filter((b) => b !== null);
138
+ // Safety: if stripping thinking blocks emptied the content, keep
139
+ // a minimal text block so the message structure stays valid.
140
+ const safeContent = filtered.length > 0
141
+ ? filtered
142
+ : [{ type: "text", text: "" }];
92
143
  // eslint-disable-next-line security/detect-object-injection -- numeric index
93
- result[i] = { ...msg, content: newContent };
144
+ result[i] = { ...msg, content: safeContent };
145
+ scrubbedAssistantMessages++;
94
146
  anyChanged = true;
95
147
  }
96
148
  else {
@@ -98,9 +150,30 @@ export function createSignatureReplayScrubber(deps) {
98
150
  result[i] = original;
99
151
  }
100
152
  }
101
- // Always notify so observers see the drift reason even on zero-touch
102
- // histories (drift fired but no signed state in the conversation yet).
103
- deps.onScrubbed?.({ dropped, signaturesStripped, reason: drift.reason });
153
+ // Always invoke onScrubbed so the context-engine snapshot stays
154
+ // consistent on zero-touch turns (e.g., a single assistant message
155
+ // history). Legacy aliases preserved for the existing snapshot
156
+ // consumer at context-engine.ts ~lines 718–725.
157
+ deps.onScrubbed?.({
158
+ scrubbedAssistantMessages,
159
+ blocksAffected,
160
+ toolCallsAffected,
161
+ latestAssistantIdx: latestIdx,
162
+ dropped: blocksAffected,
163
+ signaturesStripped: toolCallsAffected,
164
+ reason: undefined,
165
+ });
166
+ // Emit INFO once per execute() when at least one assistant message
167
+ // was actually scrubbed. Pino object-first; no string interp.
168
+ if (scrubbedAssistantMessages > 0) {
169
+ deps.logger.info({
170
+ module: "agent.context-engine.signature-replay-scrub",
171
+ scrubbedAssistantMessages,
172
+ blocksAffected,
173
+ toolCallsAffected,
174
+ latestAssistantIdx: latestIdx,
175
+ }, "Dropped thinking signatures from all assistant messages (cross-turn replay)");
176
+ }
104
177
  // Zero-allocation early return when nothing was actually changed.
105
178
  if (!anyChanged)
106
179
  return messages;
@@ -46,6 +46,9 @@ export function setupContextEngine(params) {
46
46
  // Memoized per-execute() so all pipeline runs in a single execute() see a
47
47
  // consistent decision (cleaner + scrubber must agree). The closure reads
48
48
  // the latest model identity each time (handles cycleModel mid-execute).
49
+ // Returns the identity/idle drift only — the kvl tool-defs dimension was
50
+ // removed in 260428-lm6 in favor of the unconditional latest-message
51
+ // preserving scrub in signature-replay-scrubber.
49
52
  let memoizedDrift;
50
53
  const computeDriftIfNeeded = () => {
51
54
  if (memoizedDrift !== undefined)
@@ -58,7 +61,7 @@ export function setupContextEngine(params) {
58
61
  // Derive currentApi from model.api when present; otherwise fall back to
59
62
  // the provider family (resolveProviderFamily strips -bedrock / -vertex).
60
63
  const currentApi = model?.api ?? resolveProviderFamily(config.provider);
61
- memoizedDrift = shouldDropSignedFields({
64
+ const existingDrift = shouldDropSignedFields({
62
65
  // Cast: shouldDropSignedFields tolerates malformed entries internally.
63
66
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
64
67
  fileEntries: fileEntries,
@@ -69,6 +72,7 @@ export function setupContextEngine(params) {
69
72
  },
70
73
  idleMs,
71
74
  });
75
+ memoizedDrift = existingDrift;
72
76
  return memoizedDrift;
73
77
  }
74
78
  catch (err) {
@@ -193,30 +193,28 @@ export async function postExecution(params) {
193
193
  });
194
194
  }
195
195
  }
196
- // SEP: Attach planner metrics to result
196
+ // SEP: Attach planner metrics to result (observability-only post-L4).
197
+ // Uses actual tool-call count instead of prose-extracted step count to
198
+ // avoid over-counting (the LLM's numbered plan often has 2-3× more
199
+ // items than logical steps — e.g., "11 steps" for a 4-tool task).
197
200
  if (executionPlanRef.current?.active) {
198
201
  const plan = executionPlanRef.current;
202
+ const toolCalls = result.stepsExecuted ?? 0;
199
203
  result.plannerMetrics = {
200
- stepsPlanned: plan.steps.length,
201
- stepsCompleted: plan.completedCount,
202
- stepsSkipped: plan.steps.filter(s => s.status === "skipped").length,
203
- nudgeTriggered: plan.nudged,
204
+ stepsPlanned: toolCalls,
205
+ stepsCompleted: toolCalls,
206
+ stepsSkipped: 0,
204
207
  planExtractionTurn: 1,
205
208
  };
206
- // Emit plan_completed if all steps resolved
207
- const allResolved = plan.steps.every(s => s.status === "done" || s.status === "skipped");
208
- if (allResolved) {
209
- deps.eventBus.emit("sep:plan_completed", {
210
- agentId: agentId ?? "default",
211
- sessionKey: formattedKey,
212
- stepsPlanned: plan.steps.length,
213
- stepsCompleted: plan.completedCount,
214
- stepsSkipped: plan.steps.filter(s => s.status === "skipped").length,
215
- nudgeTriggered: plan.nudged,
216
- durationMs: Date.now() - plan.createdAtMs,
217
- timestamp: Date.now(),
218
- });
219
- }
209
+ deps.eventBus.emit("sep:plan_completed", {
210
+ agentId: agentId ?? "default",
211
+ sessionKey: formattedKey,
212
+ stepsPlanned: toolCalls,
213
+ stepsCompleted: toolCalls,
214
+ stepsSkipped: 0,
215
+ durationMs: Date.now() - plan.createdAtMs,
216
+ timestamp: Date.now(),
217
+ });
220
218
  }
221
219
  // Record timestamp after successful execution for TTL guard.
222
220
  // Uses the stream-setup captured retention (same ref the wrapper chain captured)
@@ -292,7 +290,11 @@ export async function postExecution(params) {
292
290
  ...(result.plannerMetrics && {
293
291
  sepStepsPlanned: result.plannerMetrics.stepsPlanned,
294
292
  sepStepsCompleted: result.plannerMetrics.stepsCompleted,
295
- sepNudgeTriggered: result.plannerMetrics.nudgeTriggered,
293
+ }),
294
+ ...(result.continuationMetrics && {
295
+ postBatchContinuationFired: result.continuationMetrics.fired,
296
+ postBatchContinuationAttempts: result.continuationMetrics.attempts,
297
+ postBatchContinuationOutcome: result.continuationMetrics.outcome,
296
298
  }),
297
299
  // 1.5 + 3.2: Thinking token tracking (conditional -- only when thinking tokens detected)
298
300
  ...(bridgeResult.thinkingTokens != null && bridgeResult.thinkingTokens > 0 && {
@@ -24,6 +24,7 @@ import type { ExecutionResult, ExecutionOverrides } from "./types.js";
24
24
  import type { ExecutionPlan } from "../planner/types.js";
25
25
  import type { AuthRotationAdapter } from "../model/auth-rotation-adapter.js";
26
26
  import type { ProviderHealthMonitor } from "../safety/provider-health-monitor.js";
27
+ import type { LastKnownModelTracker } from "../model/last-known-model.js";
27
28
  import type { EnvelopeConfig } from "@comis/core";
28
29
  /** Bridge interface used by the prompt runner (minimal getResult). */
29
30
  export interface PromptRunnerBridge {
@@ -89,6 +90,7 @@ export interface RunPromptParams {
89
90
  fallbackModels?: string[];
90
91
  modelRegistry: ModelRegistry;
91
92
  providerHealth?: ProviderHealthMonitor;
93
+ lastKnownModel?: LastKnownModelTracker;
92
94
  envelopeConfig?: EnvelopeConfig;
93
95
  outputGuard?: OutputGuardPort;
94
96
  canaryToken?: string;
@@ -19,13 +19,15 @@ import { fromPromise } from "@comis/shared";
19
19
  import { parseUserTokenBudget } from "../commands/budget-command.js";
20
20
  import { createTurnBudgetTracker } from "../budget/turn-budget-tracker.js";
21
21
  import { wrapInEnvelope } from "../envelope/message-envelope.js";
22
- import { runWithModelRetry } from "./model-retry.js";
22
+ import { runWithModelRetry, isAuthError } from "./model-retry.js";
23
+ import { normalizeModelId } from "../provider/model-id-normalize.js";
23
24
  import { withPromptTimeout, PromptTimeoutError } from "./prompt-timeout.js";
24
25
  import { classifyError, classifyPromptTimeout } from "./error-classifier.js";
25
26
  import { scrubSignedReplayStateInPlace } from "./signature-block-scrubber.js";
26
27
  import { createOverflowRecoveryWrapper } from "./overflow-recovery.js";
27
28
  import { isContextOverflowError } from "../safety/context-truncation-recovery.js";
28
- import { scanWithOutputGuard, recoverEmptyFinalResponse, extractExecutionPlan, generateCompletenessNudge, } from "./executor-response-filter.js";
29
+ import { scanWithOutputGuard, recoverEmptyFinalResponse, extractExecutionPlan, } from "./executor-response-filter.js";
30
+ import { runPostBatchContinuation } from "./post-batch-continuation.js";
29
31
  import { getVisibleAssistantText } from "./phase-filter.js";
30
32
  import { CHARS_PER_TOKEN_RATIO } from "../context-engine/constants.js";
31
33
  import { resolveModelPricing } from "../model/model-catalog.js";
@@ -179,11 +181,16 @@ export async function runPrompt(params) {
179
181
  agentId,
180
182
  sessionKey: formatSessionKey(sessionKey),
181
183
  providerHealth: deps.providerHealth,
184
+ lastKnownModel: deps.lastKnownModel,
182
185
  onResetTimer: (fn) => { onResetTimer(fn); },
183
186
  },
184
187
  });
185
188
  promptSucceeded = retryResult.succeeded;
186
189
  promptError = retryResult.error;
190
+ // Record successful model for last-known-working tracker
191
+ if (retryResult.succeeded && retryResult.effectiveModel) {
192
+ deps.lastKnownModel?.recordSuccess(agentId ?? "default", retryResult.effectiveModel.provider, retryResult.effectiveModel.model);
193
+ }
187
194
  }
188
195
  // Detect zero-LLM-call stuck session.
189
196
  // When session.prompt() succeeds but the agent loop made zero LLM calls
@@ -304,6 +311,7 @@ export async function runPrompt(params) {
304
311
  agentId,
305
312
  sessionKey: formatSessionKey(sessionKey),
306
313
  providerHealth: deps.providerHealth,
314
+ lastKnownModel: deps.lastKnownModel,
307
315
  onResetTimer: (fn) => { onResetTimer(fn); },
308
316
  },
309
317
  });
@@ -429,6 +437,7 @@ export async function runPrompt(params) {
429
437
  agentId,
430
438
  sessionKey: formatSessionKey(sessionKey),
431
439
  providerHealth: deps.providerHealth,
440
+ lastKnownModel: deps.lastKnownModel,
432
441
  onResetTimer: (fn) => { onResetTimer(fn); },
433
442
  },
434
443
  });
@@ -451,6 +460,62 @@ export async function runPrompt(params) {
451
460
  ? ` — ${retryBridgeResult.lastLlmErrorMessage}`
452
461
  : "";
453
462
  promptError = new Error(`Silent LLM failure: ${retryBridgeResult.llmCalls} LLM call(s) produced empty response after retry (finishReason: ${retryBridgeResult.finishReason ?? "unknown"})${llmDetail}`);
463
+ // LKW silent-failure fallback: some providers return 403 as
464
+ // an empty response (SDK doesn't throw), so model-retry's LKW
465
+ // block never fires. Detect auth errors here and try the LKW
466
+ // model as a final attempt before giving up.
467
+ const silentAuthErr = retryBridgeResult.lastLlmErrorMessage ?? "";
468
+ if (isAuthError(new Error(silentAuthErr)) && deps.lastKnownModel) {
469
+ const lkw = deps.lastKnownModel.getLastKnown(agentId ?? "") ??
470
+ deps.lastKnownModel.getAnyKnown(config.provider);
471
+ if (lkw && (lkw.provider !== config.provider || lkw.model !== config.model)) {
472
+ deps.logger.info({ lkwProvider: lkw.provider, lkwModel: lkw.model, silentAuthErr }, "Silent auth failure — attempting last-known-working model");
473
+ try {
474
+ const normalizedLkw = normalizeModelId(lkw.provider, lkw.model);
475
+ const lkwModelObj = deps.modelRegistry.find(lkw.provider, normalizedLkw.modelId);
476
+ if (lkwModelObj) {
477
+ await session.setModel(lkwModelObj);
478
+ }
479
+ // Strip trailing empty assistant turns before the LKW attempt
480
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
481
+ const lkwMsgs = session.messages ?? [];
482
+ for (let li = lkwMsgs.length - 1; li >= 0; li--) {
483
+ const lm = lkwMsgs[li]; // eslint-disable-line security/detect-object-injection
484
+ if (lm?.role !== "assistant")
485
+ break;
486
+ const lBlocks = Array.isArray(lm.content) ? lm.content : [];
487
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK interop boundary
488
+ const lHasText = lBlocks.some((b) => b.type === "text" && typeof b.text === "string" && b.text.trim() !== "");
489
+ if (!lHasText)
490
+ lkwMsgs.splice(li, 1);
491
+ else
492
+ break;
493
+ }
494
+ await withPromptTimeout(session.prompt(messageText, { expandPromptTemplates: false, images: promptImages }), effectiveTimeout.retryPromptTimeoutMs, () => session.abort());
495
+ const lkwText = getVisibleAssistantText(session);
496
+ if (lkwText !== "") {
497
+ promptSucceeded = true;
498
+ promptError = undefined;
499
+ deps.lastKnownModel.recordSuccess(agentId ?? "default", lkw.provider, lkw.model);
500
+ deps.logger.info({ lkwProvider: lkw.provider, lkwModel: lkw.model }, "LKW silent-failure fallback succeeded");
501
+ }
502
+ else {
503
+ deps.logger.warn({
504
+ lkwProvider: lkw.provider, lkwModel: lkw.model,
505
+ hint: "LKW model also produced empty response",
506
+ errorKind: "dependency",
507
+ }, "LKW silent-failure fallback produced empty response");
508
+ }
509
+ }
510
+ catch (lkwErr) {
511
+ deps.logger.warn({
512
+ err: lkwErr, lkwProvider: lkw.provider, lkwModel: lkw.model,
513
+ hint: "LKW model threw during silent-failure fallback",
514
+ errorKind: "dependency",
515
+ }, "LKW silent-failure fallback failed");
516
+ }
517
+ }
518
+ }
454
519
  }
455
520
  }
456
521
  }
@@ -632,20 +697,45 @@ export async function runPrompt(params) {
632
697
  if (sepEnabled && !executionPlanRef.current && extractedResponse && toolCallCount === 0) {
633
698
  deps.logger.debug({ agentId }, "SEP extraction skipped: no tool calls in execution (likely conversational response)");
634
699
  }
635
- // SEP: Completeness nudge (extracted to executor-response-filter.ts)
636
- if (executionPlanRef.current?.active && !executionPlanRef.current.nudged) {
637
- const nudgeText = generateCompletenessNudge({
638
- plan: executionPlanRef.current,
639
- verificationNudge: config.sep?.verificationNudge !== false,
700
+ // L4: Post-batch continuation (replaces the deleted SEP one-shot nudge).
701
+ // Detects empty final assistant turn after a successful tool batch within
702
+ // the current execution window and fires a directive followUp with multi-
703
+ // shot retry. Falls through to L3 synthesis (recoverEmptyFinalResponse) on
704
+ // exhaustion. SEP plan extraction + step counting remain intact for
705
+ // observability — see pi-event-bridge.ts:949-1024.
706
+ {
707
+ const continuationConfig = config.contextEngine?.postBatchContinuation
708
+ ?? { enabled: true, maxRetries: 2 };
709
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
710
+ const sessionMessages = session.messages ?? [];
711
+ const continuationResult = await runPostBatchContinuation({
712
+ session,
713
+ messages: sessionMessages,
714
+ config: continuationConfig,
715
+ logger: deps.logger,
716
+ agentId,
717
+ getVisibleAssistantText,
640
718
  });
641
- if (nudgeText) {
642
- executionPlanRef.current.nudged = true;
643
- deps.logger.info({ agentId, remainingSteps: executionPlanRef.current.steps.filter(s => s.status === "pending" || s.status === "in_progress").length }, "SEP completeness nudge triggered");
644
- await session.followUp(nudgeText);
645
- const nudgeResponse = getVisibleAssistantText(session);
646
- if (nudgeResponse) {
647
- result.response = nudgeResponse;
719
+ if (continuationResult.ok) {
720
+ const v = continuationResult.value;
721
+ if (v.recovered && v.response) {
722
+ result.response = v.response;
648
723
  }
724
+ // Stash outcome metrics for executor-post-execution.ts to emit in the
725
+ // Execution complete log.
726
+ result.continuationMetrics = {
727
+ fired: v.outcome !== "no_match" && v.outcome !== "disabled",
728
+ attempts: v.attempts,
729
+ outcome: v.outcome,
730
+ };
731
+ }
732
+ else {
733
+ deps.logger.warn({
734
+ err: continuationResult.error.cause,
735
+ hint: "Post-batch continuation followUp failed; preserving response collected so far",
736
+ errorKind: "internal",
737
+ }, "Post-batch continuation error");
738
+ result.continuationMetrics = { fired: false, attempts: 0, outcome: "still_empty" };
649
739
  }
650
740
  }
651
741
  // Budget-driven continuation loop
@@ -764,7 +854,13 @@ export async function runPrompt(params) {
764
854
  const classified = promptError instanceof PromptTimeoutError
765
855
  ? classifyPromptTimeout(promptError.timeoutMs)
766
856
  : classifyError(promptError);
767
- result.response = classified.userMessage;
857
+ // Enrich auth_invalid messages with the failing provider name
858
+ if (classified.category === "auth_invalid") {
859
+ result.response = `The AI service could not authenticate with the "${config.provider}" provider. Please check the API key or notify the system administrator.`;
860
+ }
861
+ else {
862
+ result.response = classified.userMessage;
863
+ }
768
864
  result.errorContext = {
769
865
  errorType: promptError instanceof PromptTimeoutError ? "PromptTimeout" : "PromptFailure",
770
866
  retryable: classified.retryable,