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
|
@@ -22,6 +22,7 @@ import { wrapInEnvelope } from "../envelope/message-envelope.js";
|
|
|
22
22
|
import { runWithModelRetry } from "./model-retry.js";
|
|
23
23
|
import { withPromptTimeout, PromptTimeoutError } from "./prompt-timeout.js";
|
|
24
24
|
import { classifyError, classifyPromptTimeout } from "./error-classifier.js";
|
|
25
|
+
import { scrubSignedReplayStateInPlace } from "./signature-block-scrubber.js";
|
|
25
26
|
import { createOverflowRecoveryWrapper } from "./overflow-recovery.js";
|
|
26
27
|
import { isContextOverflowError } from "../safety/context-truncation-recovery.js";
|
|
27
28
|
import { scanWithOutputGuard, recoverEmptyFinalResponse, extractExecutionPlan, generateCompletenessNudge, } from "./executor-response-filter.js";
|
|
@@ -252,13 +253,113 @@ export async function runPrompt(params) {
|
|
|
252
253
|
}
|
|
253
254
|
}
|
|
254
255
|
if (!silent02Recovered && !silentRetryAttempted) {
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
256
|
+
// Classify the bridge's recorded LLM error to pick the correct path:
|
|
257
|
+
// - "client_request_signed_replay": scrub signed thinking state and
|
|
258
|
+
// re-enter runWithModelRetry once (provider-agnostic self-heal,
|
|
259
|
+
// covers Anthropic, Bedrock-Claude, Gemini, OpenAI Responses,
|
|
260
|
+
// OpenAI Completions reasoning, Mistral).
|
|
261
|
+
// - "client_request": deterministic; declare terminal failure
|
|
262
|
+
// verbatim with the original error wording.
|
|
263
|
+
// - default: fall through to the existing strip-empty-turn +
|
|
264
|
+
// re-enter retry path below.
|
|
259
265
|
const llmErrSource = earlyBridgeResult.lastLlmErrorMessage ?? "";
|
|
260
266
|
const earlyClassification = classifyError(new Error(llmErrSource));
|
|
261
|
-
if (earlyClassification.category === "
|
|
267
|
+
if (earlyClassification.category === "client_request_signed_replay") {
|
|
268
|
+
// Provider-agnostic signed-replay self-heal. Scrub stored signed
|
|
269
|
+
// thinking / reasoning state in place, then re-enter the full
|
|
270
|
+
// model retry pipeline once. Mirrors the silent-retry shape but
|
|
271
|
+
// with a 1s settle (vs 3s for transient overload) since the
|
|
272
|
+
// failure cause is deterministic state on disk, not a transient
|
|
273
|
+
// provider condition.
|
|
274
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
275
|
+
const msgs = session.messages ?? [];
|
|
276
|
+
const { blocksRemoved, thoughtSignaturesStripped } = scrubSignedReplayStateInPlace(msgs);
|
|
277
|
+
deps.logger.info({
|
|
278
|
+
blocksRemoved,
|
|
279
|
+
thoughtSignaturesStripped,
|
|
280
|
+
providerError: llmErrSource,
|
|
281
|
+
hint: "Signed-replay rejection detected; scrubbing thinking state and retrying once",
|
|
282
|
+
errorKind: "transient",
|
|
283
|
+
}, "Signed-replay self-heal: scrubbing and retrying");
|
|
284
|
+
// Brief settle before retry. Distinct from the 3s silent-retry
|
|
285
|
+
// delay because signed-replay is a deterministic state error,
|
|
286
|
+
// not a transient provider condition.
|
|
287
|
+
await new Promise(r => setTimeout(r, 1_000));
|
|
288
|
+
const retryResult = await runWithModelRetry({
|
|
289
|
+
session,
|
|
290
|
+
messageText,
|
|
291
|
+
promptImages,
|
|
292
|
+
config: { provider: config.provider, model: config.model },
|
|
293
|
+
resolvedModel: resolvedModel ? `${resolvedModel.provider}:${resolvedModel.id}` : undefined,
|
|
294
|
+
timeoutConfig: {
|
|
295
|
+
promptTimeoutMs: effectiveTimeout.promptTimeoutMs,
|
|
296
|
+
retryPromptTimeoutMs: effectiveTimeout.retryPromptTimeoutMs,
|
|
297
|
+
},
|
|
298
|
+
deps: {
|
|
299
|
+
eventBus: deps.eventBus,
|
|
300
|
+
logger: deps.logger,
|
|
301
|
+
authRotation: deps.authRotation,
|
|
302
|
+
fallbackModels: deps.fallbackModels,
|
|
303
|
+
modelRegistry: deps.modelRegistry,
|
|
304
|
+
agentId,
|
|
305
|
+
sessionKey: formatSessionKey(sessionKey),
|
|
306
|
+
providerHealth: deps.providerHealth,
|
|
307
|
+
onResetTimer: (fn) => { onResetTimer(fn); },
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
promptSucceeded = retryResult.succeeded;
|
|
311
|
+
promptError = retryResult.error;
|
|
312
|
+
// Re-check for empty response after retry; mirror the
|
|
313
|
+
// silent-retry post-check semantics so the recovery event
|
|
314
|
+
// reports a faithful succeeded flag.
|
|
315
|
+
let recovered = promptSucceeded;
|
|
316
|
+
if (promptSucceeded) {
|
|
317
|
+
const retryText = session.getLastAssistantText?.() ?? "";
|
|
318
|
+
if (retryText === "") {
|
|
319
|
+
const retryBridgeResult = bridge.getResult();
|
|
320
|
+
if ((retryBridgeResult.llmCalls ?? 0) > 0 && !retryBridgeResult.textEmitted) {
|
|
321
|
+
recovered = false;
|
|
322
|
+
promptSucceeded = false;
|
|
323
|
+
const llmDetail = retryBridgeResult.lastLlmErrorMessage
|
|
324
|
+
? ` — ${retryBridgeResult.lastLlmErrorMessage}`
|
|
325
|
+
: "";
|
|
326
|
+
promptError = new Error(`Signed-replay self-heal failed: ${retryBridgeResult.llmCalls} LLM call(s) produced empty response after retry (finishReason: ${retryBridgeResult.finishReason ?? "unknown"})${llmDetail}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
deps.eventBus.emit("execution:signed_replay_recovered", {
|
|
331
|
+
agentId: agentId ?? "default",
|
|
332
|
+
sessionKey: formatSessionKey(sessionKey),
|
|
333
|
+
blocksRemoved,
|
|
334
|
+
thoughtSignaturesStripped,
|
|
335
|
+
succeeded: recovered,
|
|
336
|
+
timestamp: Date.now(),
|
|
337
|
+
});
|
|
338
|
+
if (recovered) {
|
|
339
|
+
deps.logger.info({
|
|
340
|
+
blocksRemoved,
|
|
341
|
+
thoughtSignaturesStripped,
|
|
342
|
+
recovered: true,
|
|
343
|
+
}, "Signed-replay self-heal succeeded");
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
deps.logger.warn({
|
|
347
|
+
blocksRemoved,
|
|
348
|
+
thoughtSignaturesStripped,
|
|
349
|
+
hint: "Signed-replay self-heal retry also failed; declaring terminal failure",
|
|
350
|
+
errorKind: "dependency",
|
|
351
|
+
}, "Signed-replay self-heal retry failed");
|
|
352
|
+
}
|
|
353
|
+
// Close the gate so this branch cannot be re-entered within the
|
|
354
|
+
// same runPrompt invocation.
|
|
355
|
+
// eslint-disable-next-line no-useless-assignment
|
|
356
|
+
silentRetryAttempted = true;
|
|
357
|
+
}
|
|
358
|
+
else if (earlyClassification.category === "client_request") {
|
|
359
|
+
// Plain client_request: deterministic failure (e.g. unprocessable_entity,
|
|
360
|
+
// bare "cannot be modified" without signature noun). Retrying would
|
|
361
|
+
// reproduce the same failure. Short-circuit before the strip+retry
|
|
362
|
+
// block to avoid wasting tokens.
|
|
262
363
|
deps.logger.warn({
|
|
263
364
|
llmCalls: earlyBridgeResult.llmCalls,
|
|
264
365
|
finishReason: earlyBridgeResult.finishReason,
|
|
@@ -186,6 +186,7 @@ export async function assembleTools(params) {
|
|
|
186
186
|
// -------------------------------------------------------------------
|
|
187
187
|
const resourceLoaderOptions = {
|
|
188
188
|
cwd: deps.workspaceDir,
|
|
189
|
+
agentDir: deps.agentDir,
|
|
189
190
|
settingsManager,
|
|
190
191
|
noExtensions: true,
|
|
191
192
|
additionalSkillPaths: config.skills?.discoveryPaths ?? [],
|
|
@@ -50,10 +50,7 @@ import type { AgentExecutor } from "./types.js";
|
|
|
50
50
|
*
|
|
51
51
|
* Extracted as a named function for independent unit testing.
|
|
52
52
|
*/
|
|
53
|
-
export declare function createBeforeToolCallGuard(stepCounter: StepCounter, budgetGuard: BudgetGuard, circuitBreaker: CircuitBreaker, toolRetryBreaker?: ToolRetryBreaker, messageSendLimiter?: MessageSendLimiter): (context: unknown, _signal?: AbortSignal) => Promise<
|
|
54
|
-
block: boolean;
|
|
55
|
-
reason: string;
|
|
56
|
-
} | undefined>;
|
|
53
|
+
export declare function createBeforeToolCallGuard(stepCounter: StepCounter, budgetGuard: BudgetGuard, circuitBreaker: CircuitBreaker, toolRetryBreaker?: ToolRetryBreaker, messageSendLimiter?: MessageSendLimiter): (context: unknown, _signal?: AbortSignal) => Promise<import("../safety/message-send-limiter.js").MessageSendVerdict | undefined>;
|
|
57
54
|
/**
|
|
58
55
|
* Merge SDK session stats into execution result for token totals (R-13).
|
|
59
56
|
*
|
|
@@ -449,6 +449,24 @@ export function createPiExecutor(config, deps) {
|
|
|
449
449
|
}
|
|
450
450
|
const resourceLoader = new DefaultResourceLoader(resourceLoaderOptions);
|
|
451
451
|
await resourceLoader.reload();
|
|
452
|
+
// The SDK's `tools` is an allowlist of tool *names* (not definitions).
|
|
453
|
+
// An empty array is treated as a non-empty allowlist that allows zero
|
|
454
|
+
// tools, including all customTools — which is why the agent ran
|
|
455
|
+
// tool-less from every entry point (chat API, SSE, Telegram, etc.):
|
|
456
|
+
// every Comis tool was filtered out of the SDK's tool registry, the
|
|
457
|
+
// Anthropic API request went out with `tools: []`, and the model
|
|
458
|
+
// emitted `<tool_call>...</tool_call>` markup as plaintext that
|
|
459
|
+
// Comis's loop never parsed back.
|
|
460
|
+
//
|
|
461
|
+
// Pass our customTool names as the explicit allowlist so:
|
|
462
|
+
// 1. All customTools land in the SDK's tool registry (their names
|
|
463
|
+
// pass `isAllowedTool`).
|
|
464
|
+
// 2. SDK built-ins like `bash` that conflict with Comis's policy
|
|
465
|
+
// controls are filtered out (Comis uses `exec` instead, with
|
|
466
|
+
// its own sandbox/audit hooks).
|
|
467
|
+
// 3. Where names overlap (read/edit/write), Comis's customTools
|
|
468
|
+
// override the SDK built-ins via Map.set() in the registry
|
|
469
|
+
// build (`agent-session.js:1810-1813` in pi-coding-agent@0.68.0).
|
|
452
470
|
const sessionOptions = {
|
|
453
471
|
cwd: deps.workspaceDir,
|
|
454
472
|
authStorage: deps.authStorage,
|
|
@@ -457,7 +475,7 @@ export function createPiExecutor(config, deps) {
|
|
|
457
475
|
sessionManager: sm,
|
|
458
476
|
settingsManager,
|
|
459
477
|
resourceLoader,
|
|
460
|
-
tools:
|
|
478
|
+
tools: mergedCustomTools.map((t) => t.name),
|
|
461
479
|
customTools: mergedCustomTools,
|
|
462
480
|
};
|
|
463
481
|
const { session, modelFallbackMessage } = await createAgentSession(sessionOptions);
|
|
@@ -654,11 +672,20 @@ export function createPiExecutor(config, deps) {
|
|
|
654
672
|
const postActiveNames = session.getActiveToolNames?.() ?? [];
|
|
655
673
|
if (postActiveNames.length < mergedToolNames.length) {
|
|
656
674
|
const rejected = mergedToolNames.filter(n => !postActiveNames.includes(n));
|
|
675
|
+
const allRejected = postActiveNames.length === 0 && rejected.length === mergedToolNames.length;
|
|
657
676
|
deps.logger.warn({
|
|
658
677
|
rejected,
|
|
659
|
-
|
|
678
|
+
rejectedCount: rejected.length,
|
|
679
|
+
registeredCount: mergedToolNames.length,
|
|
680
|
+
postActiveCount: postActiveNames.length,
|
|
681
|
+
allRejected,
|
|
682
|
+
hint: allRejected
|
|
683
|
+
? "SDK has 0 active tools after setActiveToolsByName -- not a name collision (empty active list, every Comis tool dropped). Indicates the SDK ResourceLoader / agent.tools handoff is broken; the LLM will receive no structured tool definitions and may emit `<tool_call>` markup as plaintext instead of using tool_use content blocks."
|
|
684
|
+
: "SDK filtered some Comis tools; likely name collisions with SDK built-ins (e.g. SDK reserves `bash`, `read_file`, etc.). Rename or omit the listed tools to avoid the conflict.",
|
|
660
685
|
errorKind: "validation",
|
|
661
|
-
},
|
|
686
|
+
}, allRejected
|
|
687
|
+
? "SDK rejected ALL tool registrations -- agent will run with no tools"
|
|
688
|
+
: "SDK rejected some tool registrations");
|
|
662
689
|
}
|
|
663
690
|
}
|
|
664
691
|
catch (toolMgmtError) {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-agnostic replay-drift detector.
|
|
3
|
+
*
|
|
4
|
+
* Reads session file entries (jsonl-backed; survives daemon restarts) and
|
|
5
|
+
* decides whether the current pipeline run should scrub stored signed
|
|
6
|
+
* thinking / reasoning state pre-send to avoid provider replay-rejection.
|
|
7
|
+
*
|
|
8
|
+
* Drift conditions (first-match-wins ordering):
|
|
9
|
+
* 1. idle gap > threshold
|
|
10
|
+
* 2. model id change vs the last assistant turn
|
|
11
|
+
* 3. provider change
|
|
12
|
+
* 4. api change (e.g. anthropic.messages -> google.generative_ai.responses)
|
|
13
|
+
*
|
|
14
|
+
* Pure function — no async, no I/O. Defensive: tolerates malformed entries,
|
|
15
|
+
* missing timestamps, and the empty-history case.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
/** Subset of a session file entry the detector reads. */
|
|
20
|
+
interface DriftFileEntry {
|
|
21
|
+
/** Unix-ms timestamp of the entry. */
|
|
22
|
+
timestamp?: number;
|
|
23
|
+
/** Entry kind; only `"message"` carries a role/content we care about. */
|
|
24
|
+
type?: string;
|
|
25
|
+
/** Message body for message-type entries. */
|
|
26
|
+
message?: {
|
|
27
|
+
role?: string;
|
|
28
|
+
/** Optional metadata persisted on assistant turns; pi-ai records the
|
|
29
|
+
* stream metadata here including model identity. */
|
|
30
|
+
metadata?: {
|
|
31
|
+
model?: {
|
|
32
|
+
id?: string;
|
|
33
|
+
provider?: string;
|
|
34
|
+
api?: string;
|
|
35
|
+
};
|
|
36
|
+
provider?: string;
|
|
37
|
+
api?: string;
|
|
38
|
+
};
|
|
39
|
+
content?: unknown;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Input to `shouldDropSignedFields`. */
|
|
43
|
+
export interface DriftCheckInput {
|
|
44
|
+
/** Session manager file entries, jsonl-backed. */
|
|
45
|
+
fileEntries: ReadonlyArray<DriftFileEntry>;
|
|
46
|
+
/** Current model identity from `session.agent.state.model` plus
|
|
47
|
+
* `config.provider`. All fields optional; missing fields disable the
|
|
48
|
+
* corresponding drift check rather than tripping it. */
|
|
49
|
+
currentModel: {
|
|
50
|
+
id?: string;
|
|
51
|
+
provider?: string;
|
|
52
|
+
api?: string;
|
|
53
|
+
};
|
|
54
|
+
/** Idle threshold from config (already resolved with default applied). */
|
|
55
|
+
idleMs: number;
|
|
56
|
+
/** Now() injection for testability. Default: `Date.now()`. */
|
|
57
|
+
now?: number;
|
|
58
|
+
}
|
|
59
|
+
/** Result of `shouldDropSignedFields`. */
|
|
60
|
+
export interface DriftCheck {
|
|
61
|
+
/** True when the caller should scrub signed thinking state pre-send. */
|
|
62
|
+
drop: boolean;
|
|
63
|
+
/** Human-readable reason; populated only when drop===true. */
|
|
64
|
+
reason?: "idle" | "model_change" | "provider_change" | "api_change";
|
|
65
|
+
/** Diagnostic detail for logger / event payload. */
|
|
66
|
+
detail?: {
|
|
67
|
+
idleGapMs?: number;
|
|
68
|
+
previousModel?: string;
|
|
69
|
+
previousProvider?: string;
|
|
70
|
+
previousApi?: string;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Decide whether signed thinking state should be scrubbed pre-send.
|
|
75
|
+
*
|
|
76
|
+
* Walks `fileEntries` from the END to find the most recent assistant message
|
|
77
|
+
* with content. Compares its timestamp + recorded model identity against the
|
|
78
|
+
* current model identity and the configured idle threshold.
|
|
79
|
+
*
|
|
80
|
+
* Ordering: idle takes precedence over model_change, which takes precedence
|
|
81
|
+
* over provider_change, which takes precedence over api_change. The order is
|
|
82
|
+
* tested explicitly so future refactors cannot silently change it.
|
|
83
|
+
*/
|
|
84
|
+
export declare function shouldDropSignedFields(input: DriftCheckInput): DriftCheck;
|
|
85
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Provider-agnostic replay-drift detector.
|
|
4
|
+
*
|
|
5
|
+
* Reads session file entries (jsonl-backed; survives daemon restarts) and
|
|
6
|
+
* decides whether the current pipeline run should scrub stored signed
|
|
7
|
+
* thinking / reasoning state pre-send to avoid provider replay-rejection.
|
|
8
|
+
*
|
|
9
|
+
* Drift conditions (first-match-wins ordering):
|
|
10
|
+
* 1. idle gap > threshold
|
|
11
|
+
* 2. model id change vs the last assistant turn
|
|
12
|
+
* 3. provider change
|
|
13
|
+
* 4. api change (e.g. anthropic.messages -> google.generative_ai.responses)
|
|
14
|
+
*
|
|
15
|
+
* Pure function — no async, no I/O. Defensive: tolerates malformed entries,
|
|
16
|
+
* missing timestamps, and the empty-history case.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Implementation
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
/**
|
|
24
|
+
* Decide whether signed thinking state should be scrubbed pre-send.
|
|
25
|
+
*
|
|
26
|
+
* Walks `fileEntries` from the END to find the most recent assistant message
|
|
27
|
+
* with content. Compares its timestamp + recorded model identity against the
|
|
28
|
+
* current model identity and the configured idle threshold.
|
|
29
|
+
*
|
|
30
|
+
* Ordering: idle takes precedence over model_change, which takes precedence
|
|
31
|
+
* over provider_change, which takes precedence over api_change. The order is
|
|
32
|
+
* tested explicitly so future refactors cannot silently change it.
|
|
33
|
+
*/
|
|
34
|
+
export function shouldDropSignedFields(input) {
|
|
35
|
+
const { fileEntries, currentModel, idleMs } = input;
|
|
36
|
+
const now = input.now ?? Date.now();
|
|
37
|
+
if (!Array.isArray(fileEntries) || fileEntries.length === 0) {
|
|
38
|
+
return { drop: false };
|
|
39
|
+
}
|
|
40
|
+
// Walk from the end to find the most recent assistant message.
|
|
41
|
+
let lastAssistant = null;
|
|
42
|
+
for (let i = fileEntries.length - 1; i >= 0; i--) {
|
|
43
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index, caller-supplied array
|
|
44
|
+
const entry = fileEntries[i];
|
|
45
|
+
if (!entry || typeof entry !== "object")
|
|
46
|
+
continue;
|
|
47
|
+
if (entry.type !== "message")
|
|
48
|
+
continue;
|
|
49
|
+
if (entry.message?.role !== "assistant")
|
|
50
|
+
continue;
|
|
51
|
+
lastAssistant = entry;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
if (lastAssistant === null) {
|
|
55
|
+
// No prior assistant entry → nothing to drift from.
|
|
56
|
+
return { drop: false };
|
|
57
|
+
}
|
|
58
|
+
// Idle check: highest precedence.
|
|
59
|
+
const lastTs = typeof lastAssistant.timestamp === "number" ? lastAssistant.timestamp : undefined;
|
|
60
|
+
if (lastTs !== undefined) {
|
|
61
|
+
const idleGapMs = now - lastTs;
|
|
62
|
+
if (idleGapMs > idleMs) {
|
|
63
|
+
return { drop: true, reason: "idle", detail: { idleGapMs } };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Identity comparisons. pi-ai persists stream metadata on assistant turns
|
|
67
|
+
// under message.metadata.model; some legacy paths put it directly on
|
|
68
|
+
// message.metadata.{provider,api}. Probe both shapes.
|
|
69
|
+
const meta = lastAssistant.message?.metadata;
|
|
70
|
+
const previousModel = meta?.model?.id;
|
|
71
|
+
const previousProvider = meta?.model?.provider ?? meta?.provider;
|
|
72
|
+
const previousApi = meta?.model?.api ?? meta?.api;
|
|
73
|
+
// Model id change.
|
|
74
|
+
if (previousModel !== undefined &&
|
|
75
|
+
currentModel.id !== undefined &&
|
|
76
|
+
previousModel !== currentModel.id) {
|
|
77
|
+
return { drop: true, reason: "model_change", detail: { previousModel } };
|
|
78
|
+
}
|
|
79
|
+
// Provider change.
|
|
80
|
+
if (previousProvider !== undefined &&
|
|
81
|
+
currentModel.provider !== undefined &&
|
|
82
|
+
previousProvider !== currentModel.provider) {
|
|
83
|
+
return { drop: true, reason: "provider_change", detail: { previousProvider } };
|
|
84
|
+
}
|
|
85
|
+
// API change.
|
|
86
|
+
if (previousApi !== undefined &&
|
|
87
|
+
currentModel.api !== undefined &&
|
|
88
|
+
previousApi !== currentModel.api) {
|
|
89
|
+
return { drop: true, reason: "api_change", detail: { previousApi } };
|
|
90
|
+
}
|
|
91
|
+
return { drop: false };
|
|
92
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signed-replay state scrubber for the reject-and-retry self-heal path.
|
|
3
|
+
*
|
|
4
|
+
* Mutates the in-memory session message array in place so the next API call
|
|
5
|
+
* does not replay the rejected signed thinking / reasoning state. Distinct
|
|
6
|
+
* from the steady-state thinking-block-cleaner pipeline layer:
|
|
7
|
+
* - This scrubber is more aggressive: drops every `type:"thinking"` block
|
|
8
|
+
* regardless of `redacted` flag, and strips `thoughtSignature` from every
|
|
9
|
+
* `type:"toolCall"` block.
|
|
10
|
+
* - It runs only on the rejection path in `executor-prompt-runner.ts`, after
|
|
11
|
+
* `isSignedReplayError` has classified the error.
|
|
12
|
+
*
|
|
13
|
+
* Defensive: tolerates undefined / non-array `messages`, individual messages
|
|
14
|
+
* lacking `content`, non-array content, blocks lacking `type`. Returns the
|
|
15
|
+
* counts so the caller can emit the recovery event with accurate numbers.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Walk every assistant message in `messages` and:
|
|
21
|
+
* - Filter out every block where `block.type === "thinking"` (signed and
|
|
22
|
+
* redacted alike).
|
|
23
|
+
* - For every remaining block where `block.type === "toolCall"` or
|
|
24
|
+
* `block.type === "tool_call"` (pi-ai normalizes inconsistently across
|
|
25
|
+
* providers), delete `thoughtSignature` if present.
|
|
26
|
+
*
|
|
27
|
+
* Mutates the input array in place. Returns the number of blocks removed and
|
|
28
|
+
* the number of thoughtSignatures stripped so the caller can include them
|
|
29
|
+
* in the recovery event payload.
|
|
30
|
+
*/
|
|
31
|
+
export declare function scrubSignedReplayStateInPlace(messages: unknown[]): {
|
|
32
|
+
blocksRemoved: number;
|
|
33
|
+
thoughtSignaturesStripped: number;
|
|
34
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Signed-replay state scrubber for the reject-and-retry self-heal path.
|
|
4
|
+
*
|
|
5
|
+
* Mutates the in-memory session message array in place so the next API call
|
|
6
|
+
* does not replay the rejected signed thinking / reasoning state. Distinct
|
|
7
|
+
* from the steady-state thinking-block-cleaner pipeline layer:
|
|
8
|
+
* - This scrubber is more aggressive: drops every `type:"thinking"` block
|
|
9
|
+
* regardless of `redacted` flag, and strips `thoughtSignature` from every
|
|
10
|
+
* `type:"toolCall"` block.
|
|
11
|
+
* - It runs only on the rejection path in `executor-prompt-runner.ts`, after
|
|
12
|
+
* `isSignedReplayError` has classified the error.
|
|
13
|
+
*
|
|
14
|
+
* Defensive: tolerates undefined / non-array `messages`, individual messages
|
|
15
|
+
* lacking `content`, non-array content, blocks lacking `type`. Returns the
|
|
16
|
+
* counts so the caller can emit the recovery event with accurate numbers.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Public API
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
/**
|
|
24
|
+
* Walk every assistant message in `messages` and:
|
|
25
|
+
* - Filter out every block where `block.type === "thinking"` (signed and
|
|
26
|
+
* redacted alike).
|
|
27
|
+
* - For every remaining block where `block.type === "toolCall"` or
|
|
28
|
+
* `block.type === "tool_call"` (pi-ai normalizes inconsistently across
|
|
29
|
+
* providers), delete `thoughtSignature` if present.
|
|
30
|
+
*
|
|
31
|
+
* Mutates the input array in place. Returns the number of blocks removed and
|
|
32
|
+
* the number of thoughtSignatures stripped so the caller can include them
|
|
33
|
+
* in the recovery event payload.
|
|
34
|
+
*/
|
|
35
|
+
export function scrubSignedReplayStateInPlace(messages) {
|
|
36
|
+
let blocksRemoved = 0;
|
|
37
|
+
let thoughtSignaturesStripped = 0;
|
|
38
|
+
if (!Array.isArray(messages)) {
|
|
39
|
+
return { blocksRemoved, thoughtSignaturesStripped };
|
|
40
|
+
}
|
|
41
|
+
for (const msg of messages) {
|
|
42
|
+
if (!msg || typeof msg !== "object")
|
|
43
|
+
continue;
|
|
44
|
+
const m = msg;
|
|
45
|
+
if (m.role !== "assistant")
|
|
46
|
+
continue;
|
|
47
|
+
if (!Array.isArray(m.content))
|
|
48
|
+
continue;
|
|
49
|
+
const content = m.content;
|
|
50
|
+
// Walk in reverse so splicing does not shift indices we still need to visit.
|
|
51
|
+
for (let i = content.length - 1; i >= 0; i--) {
|
|
52
|
+
// eslint-disable-next-line security/detect-object-injection -- numeric index over caller's array
|
|
53
|
+
const block = content[i];
|
|
54
|
+
if (!block || typeof block !== "object")
|
|
55
|
+
continue;
|
|
56
|
+
const b = block;
|
|
57
|
+
if (b.type === "thinking") {
|
|
58
|
+
content.splice(i, 1);
|
|
59
|
+
blocksRemoved++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if ((b.type === "toolCall" || b.type === "tool_call") && b.thoughtSignature !== undefined) {
|
|
63
|
+
delete b.thoughtSignature;
|
|
64
|
+
thoughtSignaturesStripped++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { blocksRemoved, thoughtSignaturesStripped };
|
|
69
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-agnostic signed-replay error detector.
|
|
3
|
+
*
|
|
4
|
+
* Detects provider rejections of stored signed thinking / reasoning state on
|
|
5
|
+
* the latest assistant message during replay. Triggered by a real production
|
|
6
|
+
* incident on `srv1593437` 2026-04-24T20:07:34Z (trace
|
|
7
|
+
* `93ba66cf-4283-4ed4-92bd-73d00b4eeb76`, request_id
|
|
8
|
+
* `req_011CaPCYYKfJRpuG3w2y5s52`) where Anthropic returned
|
|
9
|
+
* `400 invalid_request_error: messages.5.content.17: 'thinking' or
|
|
10
|
+
* 'redacted_thinking' blocks in the latest assistant message cannot be
|
|
11
|
+
* modified` after a 74-min idle gap with multiple daemon restarts.
|
|
12
|
+
*
|
|
13
|
+
* Provider coverage:
|
|
14
|
+
* - Anthropic: `messages.N.content.M ... thinking|redacted_thinking ... cannot be modified`
|
|
15
|
+
* (also via the JSON-path fast-path).
|
|
16
|
+
* - Bedrock-Claude: same wire shape as Anthropic over Bedrock.
|
|
17
|
+
* - Google Gemini / Vertex / Gemini-CLI: `thought_signature` mismatch /
|
|
18
|
+
* verification failed / not found.
|
|
19
|
+
* - OpenAI Responses (o-series): `reasoning_item` not found / invalid /
|
|
20
|
+
* expired / mismatch.
|
|
21
|
+
* - OpenAI Completions reasoning: `reasoning_id` not found / expired.
|
|
22
|
+
* - Mistral: `encrypted_content` mismatch / verification failed / tampered.
|
|
23
|
+
*
|
|
24
|
+
* Pure function — no I/O, no logger. The classifier in `error-classifier.ts`
|
|
25
|
+
* uses this as a `RegExp | { test(s: string): boolean }`-shaped pattern. The
|
|
26
|
+
* runner in `executor-prompt-runner.ts` uses the resulting category to drive
|
|
27
|
+
* the scrub-and-retry self-heal path.
|
|
28
|
+
*
|
|
29
|
+
* @module
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Returns `true` when the given error message indicates a provider has
|
|
33
|
+
* rejected stored signed thinking / reasoning state on the latest assistant
|
|
34
|
+
* turn during replay, across the seven targeted providers.
|
|
35
|
+
*
|
|
36
|
+
* Match logic: either (signature noun + rejection verb both fire) OR the
|
|
37
|
+
* Anthropic JSON-path fast-path fires.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isSignedReplayError(message: string): boolean;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Provider-agnostic signed-replay error detector.
|
|
4
|
+
*
|
|
5
|
+
* Detects provider rejections of stored signed thinking / reasoning state on
|
|
6
|
+
* the latest assistant message during replay. Triggered by a real production
|
|
7
|
+
* incident on `srv1593437` 2026-04-24T20:07:34Z (trace
|
|
8
|
+
* `93ba66cf-4283-4ed4-92bd-73d00b4eeb76`, request_id
|
|
9
|
+
* `req_011CaPCYYKfJRpuG3w2y5s52`) where Anthropic returned
|
|
10
|
+
* `400 invalid_request_error: messages.5.content.17: 'thinking' or
|
|
11
|
+
* 'redacted_thinking' blocks in the latest assistant message cannot be
|
|
12
|
+
* modified` after a 74-min idle gap with multiple daemon restarts.
|
|
13
|
+
*
|
|
14
|
+
* Provider coverage:
|
|
15
|
+
* - Anthropic: `messages.N.content.M ... thinking|redacted_thinking ... cannot be modified`
|
|
16
|
+
* (also via the JSON-path fast-path).
|
|
17
|
+
* - Bedrock-Claude: same wire shape as Anthropic over Bedrock.
|
|
18
|
+
* - Google Gemini / Vertex / Gemini-CLI: `thought_signature` mismatch /
|
|
19
|
+
* verification failed / not found.
|
|
20
|
+
* - OpenAI Responses (o-series): `reasoning_item` not found / invalid /
|
|
21
|
+
* expired / mismatch.
|
|
22
|
+
* - OpenAI Completions reasoning: `reasoning_id` not found / expired.
|
|
23
|
+
* - Mistral: `encrypted_content` mismatch / verification failed / tampered.
|
|
24
|
+
*
|
|
25
|
+
* Pure function — no I/O, no logger. The classifier in `error-classifier.ts`
|
|
26
|
+
* uses this as a `RegExp | { test(s: string): boolean }`-shaped pattern. The
|
|
27
|
+
* runner in `executor-prompt-runner.ts` uses the resulting category to drive
|
|
28
|
+
* the scrub-and-retry self-heal path.
|
|
29
|
+
*
|
|
30
|
+
* @module
|
|
31
|
+
*/
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Patterns (case-insensitive)
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
/**
|
|
36
|
+
* Signature-noun regex: matches the provider-specific name for "the signed
|
|
37
|
+
* piece of state attached to the assistant turn". Covers all seven targeted
|
|
38
|
+
* providers without leaking unrelated false positives.
|
|
39
|
+
*/
|
|
40
|
+
const SIGNATURE_NOUN = /thinking|redacted_thinking|reasoning_item|encrypted_content|thought_signature|reasoning_id/i;
|
|
41
|
+
/**
|
|
42
|
+
* Rejection-verb regex: matches any of the verbs providers use to reject
|
|
43
|
+
* tampered / stale / mismatched signed state. Pairing this with a signature
|
|
44
|
+
* noun avoids matching unrelated `invalid` / `not found` errors (e.g. model
|
|
45
|
+
* not found).
|
|
46
|
+
*/
|
|
47
|
+
const REJECTION_VERB = /cannot be modified|not found|invalid|mismatch|verification failed|expired|tampered|stale/i;
|
|
48
|
+
/**
|
|
49
|
+
* Anthropic JSON-path fast-path: matches the canonical Anthropic 400 error
|
|
50
|
+
* shape `messages.N.content.M: ...thinking|redacted_thinking...`. This shape
|
|
51
|
+
* always indicates signed-replay rejection regardless of which verb appears
|
|
52
|
+
* in the surrounding text.
|
|
53
|
+
*/
|
|
54
|
+
const ANTHROPIC_JSON_PATH = /messages\.\d+\.content\.\d+:.*(?:thinking|redacted_thinking)/i;
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Public API
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
/**
|
|
59
|
+
* Returns `true` when the given error message indicates a provider has
|
|
60
|
+
* rejected stored signed thinking / reasoning state on the latest assistant
|
|
61
|
+
* turn during replay, across the seven targeted providers.
|
|
62
|
+
*
|
|
63
|
+
* Match logic: either (signature noun + rejection verb both fire) OR the
|
|
64
|
+
* Anthropic JSON-path fast-path fires.
|
|
65
|
+
*/
|
|
66
|
+
export function isSignedReplayError(message) {
|
|
67
|
+
if (!message)
|
|
68
|
+
return false;
|
|
69
|
+
if (ANTHROPIC_JSON_PATH.test(message))
|
|
70
|
+
return true;
|
|
71
|
+
return SIGNATURE_NOUN.test(message) && REJECTION_VERB.test(message);
|
|
72
|
+
}
|
|
File without changes
|