comisai 1.0.19 → 1.0.23
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/dist/cli-entry.js +0 -0
- package/node_modules/@comis/agent/dist/context-engine/context-engine.js +43 -2
- package/node_modules/@comis/agent/dist/context-engine/signature-replay-scrubber.d.ts +51 -0
- package/node_modules/@comis/agent/dist/context-engine/signature-replay-scrubber.js +110 -0
- package/node_modules/@comis/agent/dist/context-engine/signature-surrogate-guard.d.ts +54 -0
- package/node_modules/@comis/agent/dist/context-engine/signature-surrogate-guard.js +145 -0
- package/node_modules/@comis/agent/dist/context-engine/types-core.d.ts +17 -0
- package/node_modules/@comis/agent/dist/executor/error-classifier.d.ts +11 -1
- package/node_modules/@comis/agent/dist/executor/error-classifier.js +13 -0
- package/node_modules/@comis/agent/dist/executor/executor-context-engine-setup.d.ts +1 -0
- package/node_modules/@comis/agent/dist/executor/executor-context-engine-setup.js +55 -0
- package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.js +106 -5
- package/node_modules/@comis/agent/dist/executor/executor-tool-assembly.js +1 -0
- package/node_modules/@comis/agent/dist/executor/pi-executor.d.ts +1 -4
- package/node_modules/@comis/agent/dist/executor/pi-executor.js +30 -3
- package/node_modules/@comis/agent/dist/executor/replay-drift-detector.d.ts +85 -0
- package/node_modules/@comis/agent/dist/executor/replay-drift-detector.js +92 -0
- package/node_modules/@comis/agent/dist/executor/signature-block-scrubber.d.ts +34 -0
- package/node_modules/@comis/agent/dist/executor/signature-block-scrubber.js +69 -0
- package/node_modules/@comis/agent/dist/executor/signed-replay-detector.d.ts +39 -0
- package/node_modules/@comis/agent/dist/executor/signed-replay-detector.js +72 -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/cli.js +0 -0
- package/node_modules/@comis/cli/package.json +1 -1
- package/node_modules/@comis/core/dist/config/git-manager.js +10 -4
- package/node_modules/@comis/core/dist/config/index.d.ts +1 -0
- package/node_modules/@comis/core/dist/config/index.js +2 -0
- package/node_modules/@comis/core/dist/config/managed-sections.d.ts +67 -0
- package/node_modules/@comis/core/dist/config/managed-sections.js +124 -0
- package/node_modules/@comis/core/dist/config/schema-agent.d.ts +28 -10
- package/node_modules/@comis/core/dist/config/schema-agent.js +6 -0
- package/node_modules/@comis/core/dist/config/schema-gateway.d.ts +2 -2
- package/node_modules/@comis/core/dist/config/schema.d.ts +65 -64
- package/node_modules/@comis/core/dist/event-bus/events-messaging.d.ts +16 -0
- package/node_modules/@comis/core/dist/exports/config.d.ts +1 -1
- 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/bundled-skills/skill-creator/scripts/init-skill.py +0 -0
- package/node_modules/@comis/daemon/bundled-skills/skill-creator/scripts/validate-skill.py +0 -0
- package/node_modules/@comis/daemon/dist/daemon.js +11 -4
- package/node_modules/@comis/daemon/dist/rpc/config-handlers.js +20 -7
- package/node_modules/@comis/daemon/dist/rpc/session-handlers.js +27 -1
- package/node_modules/@comis/daemon/dist/wiring/setup-gateway.d.ts +22 -0
- package/node_modules/@comis/daemon/dist/wiring/setup-gateway.js +34 -8
- package/node_modules/@comis/daemon/dist/wiring/setup-tools.js +14 -1
- package/node_modules/@comis/daemon/package.json +1 -1
- package/node_modules/@comis/gateway/package.json +1 -1
- package/node_modules/@comis/infra/dist/logging/log-fields.d.ts +2 -2
- 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 +23 -8
- package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.d.ts +1 -1
- package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.js +18 -14
- package/node_modules/@comis/skills/dist/builtin/platform/unified-session-tool.js +1 -1
- package/node_modules/@comis/skills/dist/builtin/sandbox/detect-provider.d.ts +1 -0
- package/node_modules/@comis/skills/dist/builtin/sandbox/detect-provider.js +78 -5
- package/node_modules/@comis/skills/package.json +1 -1
- package/node_modules/@comis/web/package.json +1 -1
- package/package.json +24 -26
- package/node_modules/@comis/agent/dist/provider/response/strip-minimax-xml.d.ts +0 -9
- package/node_modules/@comis/agent/dist/provider/response/strip-minimax-xml.js +0 -17
- package/node_modules/@comis/agent/dist/provider/response/strip-model-tokens.d.ts +0 -13
- package/node_modules/@comis/agent/dist/provider/response/strip-model-tokens.js +0 -19
- package/node_modules/@comis/agent/dist/provider/response/strip-tool-text.d.ts +0 -11
- package/node_modules/@comis/agent/dist/provider/response/strip-tool-text.js +0 -32
- package/node_modules/@comis/agent/dist/safety/follow-through-detector.d.ts +0 -46
- package/node_modules/@comis/agent/dist/safety/follow-through-detector.js +0 -76
- package/node_modules/@comis/agent/dist/safety/post-compaction-safety.d.ts +0 -30
- package/node_modules/@comis/agent/dist/safety/post-compaction-safety.js +0 -51
- package/node_modules/@comis/agent/dist/safety/schema-normalizer.d.ts +0 -37
- package/node_modules/@comis/agent/dist/safety/schema-normalizer.js +0 -137
- package/node_modules/@comis/agent/dist/safety/schema-pruning.d.ts +0 -50
- package/node_modules/@comis/agent/dist/safety/schema-pruning.js +0 -112
- package/node_modules/@comis/agent/dist/safety/tool-image-sanitizer.d.ts +0 -43
- package/node_modules/@comis/agent/dist/safety/tool-image-sanitizer.js +0 -96
- package/node_modules/@comis/agent/dist/safety/tool-sanitizer.d.ts +0 -44
- package/node_modules/@comis/agent/dist/safety/tool-sanitizer.js +0 -94
- package/node_modules/@comis/channels/dist/shared/thinking-tag-filter.d.ts +0 -28
- package/node_modules/@comis/channels/dist/shared/thinking-tag-filter.js +0 -206
- package/node_modules/@comis/cli/dist/wizard/config-writer.d.ts +0 -25
- package/node_modules/@comis/cli/dist/wizard/config-writer.js +0 -144
- package/node_modules/@comis/cli/dist/wizard/flow-types.d.ts +0 -48
- package/node_modules/@comis/cli/dist/wizard/flow-types.js +0 -70
- package/node_modules/@comis/cli/dist/wizard/manual-flow.d.ts +0 -21
- package/node_modules/@comis/cli/dist/wizard/manual-flow.js +0 -345
- package/node_modules/@comis/cli/dist/wizard/quickstart-flow.d.ts +0 -21
- package/node_modules/@comis/cli/dist/wizard/quickstart-flow.js +0 -116
- package/node_modules/@comis/core/dist/config/schema-agent-model.d.ts +0 -135
- package/node_modules/@comis/core/dist/config/schema-agent-model.js +0 -114
- package/node_modules/@comis/core/dist/config/schema-agent-session.d.ts +0 -177
- package/node_modules/@comis/core/dist/config/schema-agent-session.js +0 -116
- package/node_modules/@comis/core/dist/config/schema-context-engine.d.ts +0 -92
- package/node_modules/@comis/core/dist/config/schema-context-engine.js +0 -92
- package/node_modules/@comis/core/dist/config/schema-context-guard.d.ts +0 -34
- package/node_modules/@comis/core/dist/config/schema-context-guard.js +0 -32
- package/node_modules/@comis/core/dist/config/schema-delivery-mirror.d.ts +0 -27
- package/node_modules/@comis/core/dist/config/schema-delivery-mirror.js +0 -26
- package/node_modules/@comis/core/dist/config/schema-delivery-queue.d.ts +0 -31
- package/node_modules/@comis/core/dist/config/schema-delivery-queue.js +0 -30
- package/node_modules/@comis/core/dist/config/schema-delivery-timing.d.ts +0 -41
- package/node_modules/@comis/core/dist/config/schema-delivery-timing.js +0 -31
- package/node_modules/@comis/core/dist/config/schema-monitoring.d.ts +0 -105
- package/node_modules/@comis/core/dist/config/schema-monitoring.js +0 -67
- package/node_modules/@comis/core/dist/ports/media-ports.d.ts +0 -278
- package/node_modules/@comis/core/dist/ports/media-ports.js +0 -1
- package/node_modules/@comis/core/dist/security/input-guard.d.ts +0 -46
- package/node_modules/@comis/core/dist/security/input-guard.js +0 -166
- package/node_modules/@comis/core/dist/security/scoped-secret-manager.d.ts +0 -38
- package/node_modules/@comis/core/dist/security/scoped-secret-manager.js +0 -94
- package/node_modules/@comis/daemon/dist/observability/delivery-context.d.ts +0 -37
- package/node_modules/@comis/daemon/dist/observability/delivery-context.js +0 -1
- package/node_modules/@comis/daemon/dist/observability/log-level-manager.d.ts +0 -23
- package/node_modules/@comis/daemon/dist/observability/log-level-manager.js +0 -34
- package/node_modules/@comis/daemon/dist/observability/log-transport.d.ts +0 -44
- package/node_modules/@comis/daemon/dist/observability/log-transport.js +0 -74
- package/node_modules/@comis/daemon/dist/observability/obs-write-buffer.d.ts +0 -53
- package/node_modules/@comis/daemon/dist/observability/obs-write-buffer.js +0 -68
- package/node_modules/@comis/daemon/dist/observability/types.d.ts +0 -6
- package/node_modules/@comis/daemon/dist/observability/types.js +0 -1
- package/node_modules/@comis/daemon/dist/wiring/seed-bundled-skills.d.ts +0 -41
- package/node_modules/@comis/daemon/dist/wiring/seed-bundled-skills.js +0 -84
- package/node_modules/@comis/daemon/dist/wiring/setup-delivery-mirror.d.ts +0 -24
- package/node_modules/@comis/daemon/dist/wiring/setup-delivery-mirror.js +0 -88
- package/node_modules/@comis/daemon/dist/wiring/setup-delivery-queue.d.ts +0 -31
- package/node_modules/@comis/daemon/dist/wiring/setup-delivery-queue.js +0 -132
- package/node_modules/@comis/daemon/dist/wiring/setup-monitoring.d.ts +0 -38
- package/node_modules/@comis/daemon/dist/wiring/setup-monitoring.js +0 -100
- package/node_modules/@comis/daemon/dist/wiring/setup-rpc-bridge.d.ts +0 -34
- package/node_modules/@comis/daemon/dist/wiring/setup-rpc-bridge.js +0 -52
- package/node_modules/@comis/daemon/dist/wiring/setup-task-extraction.d.ts +0 -41
- package/node_modules/@comis/daemon/dist/wiring/setup-task-extraction.js +0 -86
- package/node_modules/@comis/memory/dist/embedding-cache.d.ts +0 -36
- package/node_modules/@comis/memory/dist/embedding-cache.js +0 -94
- package/node_modules/@comis/skills/dist/bridge/tool-output-schemas.d.ts +0 -17
- package/node_modules/@comis/skills/dist/bridge/tool-output-schemas.js +0 -125
- package/node_modules/@comis/skills/dist/bridge/tool-parallelism-metadata.d.ts +0 -14
- package/node_modules/@comis/skills/dist/bridge/tool-parallelism-metadata.js +0 -92
- package/node_modules/@comis/skills/dist/bridge/tool-result-caps.d.ts +0 -14
- package/node_modules/@comis/skills/dist/bridge/tool-result-caps.js +0 -36
- package/node_modules/@comis/skills/dist/bridge/tool-search-hints.d.ts +0 -15
- package/node_modules/@comis/skills/dist/bridge/tool-search-hints.js +0 -68
- package/node_modules/@comis/skills/dist/bridge/tool-validators.d.ts +0 -11
- package/node_modules/@comis/skills/dist/bridge/tool-validators.js +0 -105
- package/node_modules/@comis/skills/dist/builtin/file/find-sort-wrapper.d.ts +0 -22
- package/node_modules/@comis/skills/dist/builtin/file/find-sort-wrapper.js +0 -95
- package/node_modules/@comis/skills/dist/builtin/file/grep-output-mode-wrapper.d.ts +0 -24
- package/node_modules/@comis/skills/dist/builtin/file/grep-output-mode-wrapper.js +0 -167
- package/node_modules/@comis/skills/dist/builtin/task-plan-tool.d.ts +0 -25
- package/node_modules/@comis/skills/dist/builtin/task-plan-tool.js +0 -67
- package/node_modules/@comis/skills/dist/integrations/mcp-tool-bridge.d.ts +0 -75
- package/node_modules/@comis/skills/dist/integrations/mcp-tool-bridge.js +0 -235
package/dist/cli-entry.js
CHANGED
|
File without changes
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
import { LAYER_CIRCUIT_BREAKER_THRESHOLD, CHARS_PER_TOKEN_RATIO, DEFAULT_COMPACTION_PREFIX_ANCHOR_TURNS } from "./constants.js";
|
|
19
19
|
import { computeTokenBudget } from "./token-budget.js";
|
|
20
20
|
import { createThinkingBlockCleaner } from "./thinking-block-cleaner.js";
|
|
21
|
+
import { createSignatureReplayScrubber } from "./signature-replay-scrubber.js";
|
|
22
|
+
import { createSignatureSurrogateGuard } from "./signature-surrogate-guard.js";
|
|
21
23
|
import { createReasoningTagStripper } from "./reasoning-tag-stripper.js";
|
|
22
24
|
import { createHistoryWindowLayer } from "./history-window.js";
|
|
23
25
|
import { createObservationMaskerLayer } from "./observation-masker.js";
|
|
@@ -93,8 +95,8 @@ async function runLayer(layer, messages, budget, breaker, logger) {
|
|
|
93
95
|
const result = await layer.apply(messages, budget);
|
|
94
96
|
breaker.recordSuccess(layer.name);
|
|
95
97
|
const durationMs = Date.now() - start;
|
|
96
|
-
// Only log layers that actually modify messages
|
|
97
|
-
if (messagesIn !== result.length
|
|
98
|
+
// Only log layers that actually modify messages
|
|
99
|
+
if (messagesIn !== result.length) {
|
|
98
100
|
logger.debug({ layerName: layer.name, messagesIn, messagesOut: result.length, durationMs }, "Context engine layer applied");
|
|
99
101
|
}
|
|
100
102
|
return {
|
|
@@ -132,6 +134,8 @@ function getCallbackSnapshot(state) {
|
|
|
132
134
|
return {
|
|
133
135
|
masker: state.masker,
|
|
134
136
|
thinking: state.thinking,
|
|
137
|
+
signatureReplayScrubber: state.signatureReplayScrubber,
|
|
138
|
+
signatureSurrogateGuard: state.signatureSurrogateGuard,
|
|
135
139
|
reasoningTags: state.reasoningTags,
|
|
136
140
|
compaction: state.compaction,
|
|
137
141
|
rehydration: state.rehydration,
|
|
@@ -175,6 +179,8 @@ export function createContextEngine(config, deps) {
|
|
|
175
179
|
const callbackState = {
|
|
176
180
|
masker: null,
|
|
177
181
|
thinking: null,
|
|
182
|
+
signatureReplayScrubber: null,
|
|
183
|
+
signatureSurrogateGuard: null,
|
|
178
184
|
reasoningTags: null,
|
|
179
185
|
compaction: null,
|
|
180
186
|
rehydration: null,
|
|
@@ -190,6 +196,27 @@ export function createContextEngine(config, deps) {
|
|
|
190
196
|
thinkingCleaner = createThinkingBlockCleaner(config.thinkingKeepTurns, (stats) => { callbackState.thinking = stats; }, deps.getThinkingKeepTurnsOverride);
|
|
191
197
|
layers.push(thinkingCleaner);
|
|
192
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`.
|
|
205
|
+
if (deps.getReplayDriftMode) {
|
|
206
|
+
const getReplayDriftMode = deps.getReplayDriftMode;
|
|
207
|
+
layers.push(createSignatureReplayScrubber({
|
|
208
|
+
getReplayDriftMode,
|
|
209
|
+
onScrubbed: (stats) => { callbackState.signatureReplayScrubber = stats; },
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
// Signature surrogate guard (Fix #3): scrub thinkingSignature from blocks
|
|
213
|
+
// whose text contains unpaired UTF-16 surrogates so pi-ai's
|
|
214
|
+
// sanitizeSurrogates() does not produce a sanitized-text-with-original-
|
|
215
|
+
// signature mismatch on replay. Always active — cost is one walk over
|
|
216
|
+
// thinking blocks with two regex tests per block, no I/O.
|
|
217
|
+
layers.push(createSignatureSurrogateGuard({
|
|
218
|
+
onGuarded: (stats) => { callbackState.signatureSurrogateGuard = stats; },
|
|
219
|
+
}));
|
|
193
220
|
// Reasoning tag stripper: always active (not gated by model.reasoning) because
|
|
194
221
|
// inline tags come from OTHER providers' responses persisted in session history --
|
|
195
222
|
// the current model's capabilities are irrelevant.
|
|
@@ -285,6 +312,8 @@ export function createContextEngine(config, deps) {
|
|
|
285
312
|
// Reset callback state for this invocation
|
|
286
313
|
callbackState.masker = null;
|
|
287
314
|
callbackState.thinking = null;
|
|
315
|
+
callbackState.signatureReplayScrubber = null;
|
|
316
|
+
callbackState.signatureSurrogateGuard = null;
|
|
288
317
|
callbackState.reasoningTags = null;
|
|
289
318
|
callbackState.compaction = null;
|
|
290
319
|
callbackState.rehydration = null;
|
|
@@ -537,6 +566,18 @@ export function createContextEngine(config, deps) {
|
|
|
537
566
|
thinkingFenceIndex: snap.thinking.cacheFenceIndex,
|
|
538
567
|
}),
|
|
539
568
|
...(snap.reasoningTags && snap.reasoningTags.tagsStripped > 0 ? { reasoningTagsStripped: snap.reasoningTags.tagsStripped } : {}),
|
|
569
|
+
...(snap.signatureReplayScrubber && snap.signatureReplayScrubber.dropped > 0
|
|
570
|
+
? { signatureReplayScrubberDropped: snap.signatureReplayScrubber.dropped }
|
|
571
|
+
: {}),
|
|
572
|
+
...(snap.signatureReplayScrubber && snap.signatureReplayScrubber.signaturesStripped > 0
|
|
573
|
+
? { signatureReplayScrubberSignaturesStripped: snap.signatureReplayScrubber.signaturesStripped }
|
|
574
|
+
: {}),
|
|
575
|
+
...(snap.signatureReplayScrubber?.reason !== undefined
|
|
576
|
+
? { signatureReplayScrubberReason: snap.signatureReplayScrubber.reason }
|
|
577
|
+
: {}),
|
|
578
|
+
...(snap.signatureSurrogateGuard && snap.signatureSurrogateGuard.signaturesStripped > 0
|
|
579
|
+
? { signatureSurrogateGuardSignaturesStripped: snap.signatureSurrogateGuard.signaturesStripped }
|
|
580
|
+
: {}),
|
|
540
581
|
budgetUtilization: Math.round(metrics.budgetUtilization * 100) / 100,
|
|
541
582
|
evictionCategories: metrics.evictionCategories,
|
|
542
583
|
rereadCount: metrics.rereadCount,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signature replay scrubber context engine layer.
|
|
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.
|
|
10
|
+
*
|
|
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.
|
|
15
|
+
*
|
|
16
|
+
* 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).
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
import type { ContextLayer } from "./types.js";
|
|
25
|
+
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
|
+
/** Dependencies for `createSignatureReplayScrubber`. */
|
|
36
|
+
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;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Create the signature-replay-scrubber pipeline layer.
|
|
46
|
+
*
|
|
47
|
+
* Layer ordering: runs AFTER `thinking-block-cleaner` and BEFORE
|
|
48
|
+
* `signature-surrogate-guard` (and well before `reasoning-tag-stripper`).
|
|
49
|
+
*/
|
|
50
|
+
export declare function createSignatureReplayScrubber(deps: SignatureReplayScrubberDeps): ContextLayer;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Signature replay scrubber context engine layer.
|
|
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.
|
|
11
|
+
*
|
|
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.
|
|
16
|
+
*
|
|
17
|
+
* 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).
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Create the signature-replay-scrubber pipeline layer.
|
|
27
|
+
*
|
|
28
|
+
* Layer ordering: runs AFTER `thinking-block-cleaner` and BEFORE
|
|
29
|
+
* `signature-surrogate-guard` (and well before `reasoning-tag-stripper`).
|
|
30
|
+
*/
|
|
31
|
+
export function createSignatureReplayScrubber(deps) {
|
|
32
|
+
return {
|
|
33
|
+
name: "signature-replay-scrubber",
|
|
34
|
+
async apply(messages, budget) {
|
|
35
|
+
const drift = deps.getReplayDriftMode();
|
|
36
|
+
if (!drift || !drift.drop) {
|
|
37
|
+
// Gate closed → no-op, return same reference (zero allocation).
|
|
38
|
+
return messages;
|
|
39
|
+
}
|
|
40
|
+
if (messages.length === 0)
|
|
41
|
+
return messages;
|
|
42
|
+
let anyChanged = false;
|
|
43
|
+
let dropped = 0;
|
|
44
|
+
let signaturesStripped = 0;
|
|
45
|
+
const result = new Array(messages.length);
|
|
46
|
+
for (let i = 0; i < messages.length; i++) {
|
|
47
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
48
|
+
const original = messages[i];
|
|
49
|
+
// Cache fence: messages at or below the fence must not be modified.
|
|
50
|
+
if (i <= budget.cacheFenceIndex) {
|
|
51
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
52
|
+
result[i] = original;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const msg = original;
|
|
56
|
+
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
|
|
57
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
58
|
+
result[i] = original;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const content = msg.content;
|
|
62
|
+
let messageChanged = false;
|
|
63
|
+
const newContent = [];
|
|
64
|
+
for (let j = 0; j < content.length; j++) {
|
|
65
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
66
|
+
const block = content[j];
|
|
67
|
+
if (!block || typeof block !== "object") {
|
|
68
|
+
newContent.push(block);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const b = block;
|
|
72
|
+
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;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if ((b.type === "toolCall" || b.type === "tool_call") && b.thoughtSignature !== undefined) {
|
|
81
|
+
// Shallow-copy and drop only the thoughtSignature property.
|
|
82
|
+
const copy = { ...b };
|
|
83
|
+
delete copy.thoughtSignature;
|
|
84
|
+
newContent.push(copy);
|
|
85
|
+
signaturesStripped++;
|
|
86
|
+
messageChanged = true;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
newContent.push(block);
|
|
90
|
+
}
|
|
91
|
+
if (messageChanged) {
|
|
92
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
93
|
+
result[i] = { ...msg, content: newContent };
|
|
94
|
+
anyChanged = true;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
98
|
+
result[i] = original;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
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 });
|
|
104
|
+
// Zero-allocation early return when nothing was actually changed.
|
|
105
|
+
if (!anyChanged)
|
|
106
|
+
return messages;
|
|
107
|
+
return result;
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signature surrogate guard context engine layer.
|
|
3
|
+
*
|
|
4
|
+
* Defends against pi-ai's `sanitizeSurrogates(block.thinking)` mutating the
|
|
5
|
+
* thinking text while preserving the original signature. When that happens,
|
|
6
|
+
* Anthropic's signature validator rejects the assistant turn on replay with
|
|
7
|
+
* `400 invalid_request_error: ... cannot be modified` because the bytes the
|
|
8
|
+
* signature was computed over no longer match the bytes being sent.
|
|
9
|
+
*
|
|
10
|
+
* Reference: pi-ai upstream behavior in
|
|
11
|
+
* `node_modules/.pnpm/@mariozechner+pi-ai@0.67.68_HASH/node_modules/@mariozechner/pi-ai/dist/providers/anthropic.js`
|
|
12
|
+
* around line 661 calls `sanitizeSurrogates(block.thinking)` while preserving
|
|
13
|
+
* the original `signature`. A separate upstream PR is recommended (out of
|
|
14
|
+
* scope for this commit); Comis is protected by this guard regardless of
|
|
15
|
+
* upstream.
|
|
16
|
+
*
|
|
17
|
+
* Strategy: scan every type:"thinking" block on every assistant message
|
|
18
|
+
* for unpaired UTF-16 surrogates in `block.thinking`. When found AND the
|
|
19
|
+
* block carries a non-empty `thinkingSignature`, strip the signature so
|
|
20
|
+
* pi-ai's downstream serialization falls back to converting the block to
|
|
21
|
+
* plain text rather than sending sanitized-text + original-signature
|
|
22
|
+
* mismatch. Skips `redacted: true` blocks (no readable text to taint).
|
|
23
|
+
*
|
|
24
|
+
* Cache fence respected exactly like `thinking-block-cleaner` and
|
|
25
|
+
* `signature-replay-scrubber`.
|
|
26
|
+
*
|
|
27
|
+
* Immutability: never mutates input; shallow-copies the block and the
|
|
28
|
+
* containing message only when scrubbing is needed. When no scrub fires,
|
|
29
|
+
* returns the input array reference unchanged (zero allocation).
|
|
30
|
+
*
|
|
31
|
+
* @module
|
|
32
|
+
*/
|
|
33
|
+
import type { ContextLayer } from "./types.js";
|
|
34
|
+
/** Stats reported via the `onGuarded` callback. */
|
|
35
|
+
interface GuardedStats {
|
|
36
|
+
/** Number of thinkingSignatures stripped due to unpaired surrogates. */
|
|
37
|
+
signaturesStripped: number;
|
|
38
|
+
}
|
|
39
|
+
/** Optional dependencies for `createSignatureSurrogateGuard`. */
|
|
40
|
+
export interface SignatureSurrogateGuardDeps {
|
|
41
|
+
/** Optional callback invoked exactly once at the end of `apply()` with
|
|
42
|
+
* the count of signatures stripped. */
|
|
43
|
+
onGuarded?: (stats: GuardedStats) => void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Create the signature-surrogate-guard pipeline layer.
|
|
47
|
+
*
|
|
48
|
+
* Layer ordering: runs AFTER `signature-replay-scrubber` (Fix #2) and
|
|
49
|
+
* BEFORE `reasoning-tag-stripper`. Always added unconditionally — the cost
|
|
50
|
+
* is one walk over thinking blocks with two regex tests per block, which
|
|
51
|
+
* is negligible compared to a single 400-rejection round trip.
|
|
52
|
+
*/
|
|
53
|
+
export declare function createSignatureSurrogateGuard(deps?: SignatureSurrogateGuardDeps): ContextLayer;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Signature surrogate guard context engine layer.
|
|
4
|
+
*
|
|
5
|
+
* Defends against pi-ai's `sanitizeSurrogates(block.thinking)` mutating the
|
|
6
|
+
* thinking text while preserving the original signature. When that happens,
|
|
7
|
+
* Anthropic's signature validator rejects the assistant turn on replay with
|
|
8
|
+
* `400 invalid_request_error: ... cannot be modified` because the bytes the
|
|
9
|
+
* signature was computed over no longer match the bytes being sent.
|
|
10
|
+
*
|
|
11
|
+
* Reference: pi-ai upstream behavior in
|
|
12
|
+
* `node_modules/.pnpm/@mariozechner+pi-ai@0.67.68_HASH/node_modules/@mariozechner/pi-ai/dist/providers/anthropic.js`
|
|
13
|
+
* around line 661 calls `sanitizeSurrogates(block.thinking)` while preserving
|
|
14
|
+
* the original `signature`. A separate upstream PR is recommended (out of
|
|
15
|
+
* scope for this commit); Comis is protected by this guard regardless of
|
|
16
|
+
* upstream.
|
|
17
|
+
*
|
|
18
|
+
* Strategy: scan every type:"thinking" block on every assistant message
|
|
19
|
+
* for unpaired UTF-16 surrogates in `block.thinking`. When found AND the
|
|
20
|
+
* block carries a non-empty `thinkingSignature`, strip the signature so
|
|
21
|
+
* pi-ai's downstream serialization falls back to converting the block to
|
|
22
|
+
* plain text rather than sending sanitized-text + original-signature
|
|
23
|
+
* mismatch. Skips `redacted: true` blocks (no readable text to taint).
|
|
24
|
+
*
|
|
25
|
+
* Cache fence respected exactly like `thinking-block-cleaner` and
|
|
26
|
+
* `signature-replay-scrubber`.
|
|
27
|
+
*
|
|
28
|
+
* Immutability: never mutates input; shallow-copies the block and the
|
|
29
|
+
* containing message only when scrubbing is needed. When no scrub fires,
|
|
30
|
+
* returns the input array reference unchanged (zero allocation).
|
|
31
|
+
*
|
|
32
|
+
* @module
|
|
33
|
+
*/
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Surrogate detection regexes (mirror pi-ai sanitizeSurrogates internals)
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Reference: pi-ai sanitizeSurrogates internals, anthropic.js:661 area.
|
|
38
|
+
// High surrogate not followed by low surrogate.
|
|
39
|
+
const UNPAIRED_HIGH_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])/;
|
|
40
|
+
// Low surrogate not preceded by high surrogate.
|
|
41
|
+
const UNPAIRED_LOW_SURROGATE = /(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
42
|
+
/**
|
|
43
|
+
* Create the signature-surrogate-guard pipeline layer.
|
|
44
|
+
*
|
|
45
|
+
* Layer ordering: runs AFTER `signature-replay-scrubber` (Fix #2) and
|
|
46
|
+
* BEFORE `reasoning-tag-stripper`. Always added unconditionally — the cost
|
|
47
|
+
* is one walk over thinking blocks with two regex tests per block, which
|
|
48
|
+
* is negligible compared to a single 400-rejection round trip.
|
|
49
|
+
*/
|
|
50
|
+
export function createSignatureSurrogateGuard(deps) {
|
|
51
|
+
return {
|
|
52
|
+
name: "signature-surrogate-guard",
|
|
53
|
+
async apply(messages, budget) {
|
|
54
|
+
if (messages.length === 0) {
|
|
55
|
+
deps?.onGuarded?.({ signaturesStripped: 0 });
|
|
56
|
+
return messages;
|
|
57
|
+
}
|
|
58
|
+
let anyChanged = false;
|
|
59
|
+
let signaturesStripped = 0;
|
|
60
|
+
const result = new Array(messages.length);
|
|
61
|
+
for (let i = 0; i < messages.length; i++) {
|
|
62
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
63
|
+
const original = messages[i];
|
|
64
|
+
// Cache fence: messages at or below the fence must not be modified.
|
|
65
|
+
if (i <= budget.cacheFenceIndex) {
|
|
66
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
67
|
+
result[i] = original;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const msg = original;
|
|
71
|
+
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
|
|
72
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
73
|
+
result[i] = original;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const content = msg.content;
|
|
77
|
+
let messageChanged = false;
|
|
78
|
+
const newContent = new Array(content.length);
|
|
79
|
+
for (let j = 0; j < content.length; j++) {
|
|
80
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
81
|
+
const block = content[j];
|
|
82
|
+
if (!block || typeof block !== "object") {
|
|
83
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
84
|
+
newContent[j] = block;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const b = block;
|
|
88
|
+
// Only inspect type:"thinking" blocks.
|
|
89
|
+
if (b.type !== "thinking") {
|
|
90
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
91
|
+
newContent[j] = block;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// Skip redacted blocks: they have no readable text to taint.
|
|
95
|
+
if (b.redacted === true) {
|
|
96
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
97
|
+
newContent[j] = block;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
// Skip when there's no signature to strip in the first place.
|
|
101
|
+
if (!b.thinkingSignature) {
|
|
102
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
103
|
+
newContent[j] = block;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const text = b.thinking;
|
|
107
|
+
if (typeof text !== "string") {
|
|
108
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
109
|
+
newContent[j] = block;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (UNPAIRED_HIGH_SURROGATE.test(text) || UNPAIRED_LOW_SURROGATE.test(text)) {
|
|
113
|
+
// Strip the signature: shallow-copy the block and set
|
|
114
|
+
// thinkingSignature to "" so pi-ai's downstream serialization
|
|
115
|
+
// falls back to converting the block to plain text rather than
|
|
116
|
+
// sending sanitized-text + original-signature mismatch.
|
|
117
|
+
const copy = { ...b, thinkingSignature: "" };
|
|
118
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
119
|
+
newContent[j] = copy;
|
|
120
|
+
signaturesStripped++;
|
|
121
|
+
messageChanged = true;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
125
|
+
newContent[j] = block;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (messageChanged) {
|
|
129
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
130
|
+
result[i] = { ...msg, content: newContent };
|
|
131
|
+
anyChanged = true;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index
|
|
135
|
+
result[i] = original;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
deps?.onGuarded?.({ signaturesStripped });
|
|
139
|
+
// Zero-allocation early return when nothing was scrubbed.
|
|
140
|
+
if (!anyChanged)
|
|
141
|
+
return messages;
|
|
142
|
+
return result;
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -92,6 +92,16 @@ export interface ContextEngineDeps {
|
|
|
92
92
|
contextWindow: number;
|
|
93
93
|
/** Maximum output tokens for the model. */
|
|
94
94
|
maxTokens: number;
|
|
95
|
+
/** Optional model identifier (e.g. "claude-opus-4-7"). Used by replay
|
|
96
|
+
* drift detection downstream of this getter. */
|
|
97
|
+
id?: string;
|
|
98
|
+
/** Optional provider name (e.g. "anthropic"). Used by replay drift
|
|
99
|
+
* detection downstream of this getter. */
|
|
100
|
+
provider?: string;
|
|
101
|
+
/** Optional API family tag (e.g. "anthropic.messages",
|
|
102
|
+
* "google.generative_ai.responses"). Used by replay drift detection
|
|
103
|
+
* downstream of this getter. */
|
|
104
|
+
api?: string;
|
|
95
105
|
};
|
|
96
106
|
/** Channel type for history window per-channel overrides (e.g., "dm", "group"). */
|
|
97
107
|
channelType?: string;
|
|
@@ -134,6 +144,13 @@ export interface ContextEngineDeps {
|
|
|
134
144
|
* static value is used. Used by idle thinking clear to strip all thinking
|
|
135
145
|
* blocks when the cache is cold (>1h idle). */
|
|
136
146
|
getThinkingKeepTurnsOverride?: () => number | undefined;
|
|
147
|
+
/** Optional getter for the per-execute() memoized replay drift decision.
|
|
148
|
+
* When the getter returns a DriftCheck with `drop: true`, the
|
|
149
|
+
* signature-replay-scrubber layer activates for this pipeline run. When
|
|
150
|
+
* undefined or `drop: false`, the layer no-ops. The drift result is
|
|
151
|
+
* memoized at the executor layer so all pipeline runs within a single
|
|
152
|
+
* execute() see a consistent decision. */
|
|
153
|
+
getReplayDriftMode?: () => import("../executor/replay-drift-detector.js").DriftCheck | undefined;
|
|
137
154
|
}
|
|
138
155
|
/**
|
|
139
156
|
* API-grounded token count anchor from the last LLM response.
|
|
@@ -8,7 +8,17 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module
|
|
10
10
|
*/
|
|
11
|
-
export type ErrorCategory = "credit_exhausted" | "rate_limited" | "auth_invalid" | "overloaded" | "context_too_long" | "content_filtered"
|
|
11
|
+
export type ErrorCategory = "credit_exhausted" | "rate_limited" | "auth_invalid" | "overloaded" | "context_too_long" | "content_filtered"
|
|
12
|
+
/**
|
|
13
|
+
* Provider-agnostic signed-replay rejection: the model rejected stored
|
|
14
|
+
* signed thinking / reasoning state on the latest assistant turn during
|
|
15
|
+
* replay (Anthropic `cannot be modified`, Gemini `thought_signature
|
|
16
|
+
* mismatch`, OpenAI Responses `reasoning_item not found`, OpenAI
|
|
17
|
+
* Completions `reasoning_id expired`, Mistral `encrypted_content
|
|
18
|
+
* verification failed`, etc.). Self-healable: the runner scrubs the
|
|
19
|
+
* stored signed state in place and re-enters the model retry chain.
|
|
20
|
+
*/
|
|
21
|
+
| "client_request_signed_replay" | "client_request" | "prompt_timeout"
|
|
12
22
|
/**
|
|
13
23
|
* Model produced an empty response (no text, no tool call). Almost always
|
|
14
24
|
* caused by a malformed toolResult poisoning the next turn. Retryable once
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module
|
|
11
11
|
*/
|
|
12
|
+
import { isSignedReplayError } from "./signed-replay-detector.js";
|
|
12
13
|
const ERROR_PATTERNS = [
|
|
13
14
|
// Billing / credits
|
|
14
15
|
{
|
|
@@ -45,6 +46,18 @@ const ERROR_PATTERNS = [
|
|
|
45
46
|
userMessage: "The conversation has grown too long. Please start a new conversation.",
|
|
46
47
|
retryable: false,
|
|
47
48
|
},
|
|
49
|
+
// Provider-agnostic signed-replay rejection: must be tested BEFORE the
|
|
50
|
+
// plain client_request pattern because every signed-replay error string
|
|
51
|
+
// also matches `invalid_request_error` / `cannot be modified`. First match
|
|
52
|
+
// wins, so this more-specific subcategory has to be checked first.
|
|
53
|
+
// Retryable=true because the runner scrubs signed thinking state in place
|
|
54
|
+
// and re-enters the model retry chain (see executor-prompt-runner.ts).
|
|
55
|
+
{
|
|
56
|
+
test: { test: (s) => isSignedReplayError(s) },
|
|
57
|
+
category: "client_request_signed_replay",
|
|
58
|
+
userMessage: "Your request couldn't be processed due to a formatting issue. The AI agent will try again automatically.",
|
|
59
|
+
retryable: true,
|
|
60
|
+
},
|
|
48
61
|
// Client-side validation (Anthropic 400 invalid_request_error, 422, malformed)
|
|
49
62
|
// Placed BEFORE content_filtered so /refus|blocked/ in that rule cannot steal
|
|
50
63
|
// matches. Placed AFTER billing/auth/rate/overloaded/context so those specific
|
|
@@ -17,6 +17,7 @@ import { createContextEngine } from "../context-engine/index.js";
|
|
|
17
17
|
import { CHARS_PER_TOKEN_RATIO } from "../context-engine/constants.js";
|
|
18
18
|
import { resolveOperationModel, resolveProviderFamily } from "../model/operation-model-resolver.js";
|
|
19
19
|
import { getBreakpointIndex, getBreakpointIndexMapSize, getSessionLatches, } from "./executor-session-state.js";
|
|
20
|
+
import { shouldDropSignedFields } from "./replay-drift-detector.js";
|
|
20
21
|
import { readFileSync } from "node:fs";
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
23
|
// Main function
|
|
@@ -41,6 +42,45 @@ export function setupContextEngine(params) {
|
|
|
41
42
|
const agentId = deps.agentId;
|
|
42
43
|
// contextEngineOverrides removed from ExecutionOverrides -- compaction model resolved via operationModels chain
|
|
43
44
|
const contextEngineConfig = config.contextEngine ?? ContextEngineConfigSchema.parse({});
|
|
45
|
+
// --- Replay drift memo (Fix #2) -----------------------------------------
|
|
46
|
+
// Memoized per-execute() so all pipeline runs in a single execute() see a
|
|
47
|
+
// consistent decision (cleaner + scrubber must agree). The closure reads
|
|
48
|
+
// the latest model identity each time (handles cycleModel mid-execute).
|
|
49
|
+
let memoizedDrift;
|
|
50
|
+
const computeDriftIfNeeded = () => {
|
|
51
|
+
if (memoizedDrift !== undefined)
|
|
52
|
+
return memoizedDrift;
|
|
53
|
+
try {
|
|
54
|
+
const model = session.agent.state.model;
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SessionManager interop
|
|
56
|
+
const fileEntries = (sm?.fileEntries ?? []);
|
|
57
|
+
const idleMs = contextEngineConfig.replayDriftIdleMs ?? 30 * 60_000;
|
|
58
|
+
// Derive currentApi from model.api when present; otherwise fall back to
|
|
59
|
+
// the provider family (resolveProviderFamily strips -bedrock / -vertex).
|
|
60
|
+
const currentApi = model?.api ?? resolveProviderFamily(config.provider);
|
|
61
|
+
memoizedDrift = shouldDropSignedFields({
|
|
62
|
+
// Cast: shouldDropSignedFields tolerates malformed entries internally.
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
fileEntries: fileEntries,
|
|
65
|
+
currentModel: {
|
|
66
|
+
id: model?.id,
|
|
67
|
+
provider: model?.provider ?? config.provider,
|
|
68
|
+
api: currentApi,
|
|
69
|
+
},
|
|
70
|
+
idleMs,
|
|
71
|
+
});
|
|
72
|
+
return memoizedDrift;
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
deps.logger.warn({
|
|
76
|
+
err,
|
|
77
|
+
hint: "Replay drift detection failed; defaulting to no scrub",
|
|
78
|
+
errorKind: "internal",
|
|
79
|
+
}, "Replay drift detection failed");
|
|
80
|
+
memoizedDrift = { drop: false };
|
|
81
|
+
return memoizedDrift;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
44
84
|
const contextEngine = createContextEngine(contextEngineConfig, {
|
|
45
85
|
logger: deps.logger,
|
|
46
86
|
eventBus: deps.eventBus,
|
|
@@ -53,6 +93,11 @@ export function setupContextEngine(params) {
|
|
|
53
93
|
reasoning: model?.reasoning ?? false,
|
|
54
94
|
contextWindow: model?.contextWindow ?? 128_000,
|
|
55
95
|
maxTokens: model?.maxTokens ?? 8192,
|
|
96
|
+
id: model?.id,
|
|
97
|
+
provider: model?.provider,
|
|
98
|
+
// model.api is optional pi-ai metadata. Cast for the optional access
|
|
99
|
+
// since the structural type does not require it.
|
|
100
|
+
api: model?.api,
|
|
56
101
|
};
|
|
57
102
|
},
|
|
58
103
|
channelType: msg.channelType,
|
|
@@ -70,8 +115,18 @@ export function setupContextEngine(params) {
|
|
|
70
115
|
const latches = getSessionLatches(formattedKey);
|
|
71
116
|
if (latches?.idleThinkingClear.get())
|
|
72
117
|
return 0; // Strip all thinking when idle
|
|
118
|
+
// When replay drift fires, also clamp keepTurns=0 so the cleaner agrees
|
|
119
|
+
// with the new signature-replay-scrubber. Defense in depth: the scrubber
|
|
120
|
+
// drops everything beyond the cache fence, but a future refactor that
|
|
121
|
+
// narrows the scrubber's scope must not leave the cleaner inconsistent.
|
|
122
|
+
const drift = computeDriftIfNeeded();
|
|
123
|
+
if (drift?.drop)
|
|
124
|
+
return 0;
|
|
73
125
|
return undefined; // Use default keepTurns
|
|
74
126
|
},
|
|
127
|
+
// Replay drift mode getter (Fix #2): activates the
|
|
128
|
+
// signature-replay-scrubber pipeline layer when drift is detected.
|
|
129
|
+
getReplayDriftMode: () => computeDriftIfNeeded(),
|
|
75
130
|
// LLM compaction deps
|
|
76
131
|
getCompactionDeps: () => ({
|
|
77
132
|
logger: deps.logger,
|