comisai 1.0.19 → 1.0.22

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 (147) hide show
  1. package/dist/cli-entry.js +0 -0
  2. package/node_modules/@comis/agent/dist/context-engine/context-engine.js +43 -2
  3. package/node_modules/@comis/agent/dist/context-engine/signature-replay-scrubber.d.ts +51 -0
  4. package/node_modules/@comis/agent/dist/context-engine/signature-replay-scrubber.js +110 -0
  5. package/node_modules/@comis/agent/dist/context-engine/signature-surrogate-guard.d.ts +54 -0
  6. package/node_modules/@comis/agent/dist/context-engine/signature-surrogate-guard.js +145 -0
  7. package/node_modules/@comis/agent/dist/context-engine/types-core.d.ts +17 -0
  8. package/node_modules/@comis/agent/dist/executor/error-classifier.d.ts +11 -1
  9. package/node_modules/@comis/agent/dist/executor/error-classifier.js +13 -0
  10. package/node_modules/@comis/agent/dist/executor/executor-context-engine-setup.d.ts +1 -0
  11. package/node_modules/@comis/agent/dist/executor/executor-context-engine-setup.js +55 -0
  12. package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.js +106 -5
  13. package/node_modules/@comis/agent/dist/executor/executor-tool-assembly.js +1 -0
  14. package/node_modules/@comis/agent/dist/executor/pi-executor.d.ts +1 -4
  15. package/node_modules/@comis/agent/dist/executor/replay-drift-detector.d.ts +85 -0
  16. package/node_modules/@comis/agent/dist/executor/replay-drift-detector.js +92 -0
  17. package/node_modules/@comis/agent/dist/executor/signature-block-scrubber.d.ts +34 -0
  18. package/node_modules/@comis/agent/dist/executor/signature-block-scrubber.js +69 -0
  19. package/node_modules/@comis/agent/dist/executor/signed-replay-detector.d.ts +39 -0
  20. package/node_modules/@comis/agent/dist/executor/signed-replay-detector.js +72 -0
  21. package/node_modules/@comis/agent/package.json +1 -1
  22. package/node_modules/@comis/channels/package.json +1 -1
  23. package/node_modules/@comis/cli/dist/cli.js +0 -0
  24. package/node_modules/@comis/cli/package.json +1 -1
  25. package/node_modules/@comis/core/dist/config/git-manager.js +10 -4
  26. package/node_modules/@comis/core/dist/config/index.d.ts +1 -0
  27. package/node_modules/@comis/core/dist/config/index.js +2 -0
  28. package/node_modules/@comis/core/dist/config/managed-sections.d.ts +67 -0
  29. package/node_modules/@comis/core/dist/config/managed-sections.js +124 -0
  30. package/node_modules/@comis/core/dist/config/schema-agent.d.ts +28 -10
  31. package/node_modules/@comis/core/dist/config/schema-agent.js +6 -0
  32. package/node_modules/@comis/core/dist/config/schema-gateway.d.ts +2 -2
  33. package/node_modules/@comis/core/dist/config/schema.d.ts +65 -64
  34. package/node_modules/@comis/core/dist/event-bus/events-messaging.d.ts +16 -0
  35. package/node_modules/@comis/core/dist/exports/config.d.ts +1 -1
  36. package/node_modules/@comis/core/dist/exports/config.js +1 -1
  37. package/node_modules/@comis/core/package.json +1 -1
  38. package/node_modules/@comis/daemon/bundled-skills/skill-creator/scripts/init-skill.py +0 -0
  39. package/node_modules/@comis/daemon/bundled-skills/skill-creator/scripts/validate-skill.py +0 -0
  40. package/node_modules/@comis/daemon/dist/daemon.js +0 -0
  41. package/node_modules/@comis/daemon/dist/rpc/config-handlers.js +20 -7
  42. package/node_modules/@comis/daemon/dist/rpc/session-handlers.js +27 -1
  43. package/node_modules/@comis/daemon/package.json +1 -1
  44. package/node_modules/@comis/gateway/package.json +1 -1
  45. package/node_modules/@comis/infra/package.json +1 -1
  46. package/node_modules/@comis/memory/package.json +1 -1
  47. package/node_modules/@comis/scheduler/package.json +1 -1
  48. package/node_modules/@comis/shared/package.json +1 -1
  49. package/node_modules/@comis/skills/dist/bridge/tool-metadata-registry.js +23 -8
  50. package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.d.ts +1 -1
  51. package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.js +18 -14
  52. package/node_modules/@comis/skills/dist/builtin/platform/unified-session-tool.js +1 -1
  53. package/node_modules/@comis/skills/package.json +1 -1
  54. package/node_modules/@comis/web/package.json +1 -1
  55. package/package.json +24 -26
  56. package/node_modules/@comis/agent/dist/provider/response/strip-minimax-xml.d.ts +0 -9
  57. package/node_modules/@comis/agent/dist/provider/response/strip-minimax-xml.js +0 -17
  58. package/node_modules/@comis/agent/dist/provider/response/strip-model-tokens.d.ts +0 -13
  59. package/node_modules/@comis/agent/dist/provider/response/strip-model-tokens.js +0 -19
  60. package/node_modules/@comis/agent/dist/provider/response/strip-tool-text.d.ts +0 -11
  61. package/node_modules/@comis/agent/dist/provider/response/strip-tool-text.js +0 -32
  62. package/node_modules/@comis/agent/dist/safety/follow-through-detector.d.ts +0 -46
  63. package/node_modules/@comis/agent/dist/safety/follow-through-detector.js +0 -76
  64. package/node_modules/@comis/agent/dist/safety/post-compaction-safety.d.ts +0 -30
  65. package/node_modules/@comis/agent/dist/safety/post-compaction-safety.js +0 -51
  66. package/node_modules/@comis/agent/dist/safety/schema-normalizer.d.ts +0 -37
  67. package/node_modules/@comis/agent/dist/safety/schema-normalizer.js +0 -137
  68. package/node_modules/@comis/agent/dist/safety/schema-pruning.d.ts +0 -50
  69. package/node_modules/@comis/agent/dist/safety/schema-pruning.js +0 -112
  70. package/node_modules/@comis/agent/dist/safety/tool-image-sanitizer.d.ts +0 -43
  71. package/node_modules/@comis/agent/dist/safety/tool-image-sanitizer.js +0 -96
  72. package/node_modules/@comis/agent/dist/safety/tool-sanitizer.d.ts +0 -44
  73. package/node_modules/@comis/agent/dist/safety/tool-sanitizer.js +0 -94
  74. package/node_modules/@comis/channels/dist/shared/thinking-tag-filter.d.ts +0 -28
  75. package/node_modules/@comis/channels/dist/shared/thinking-tag-filter.js +0 -206
  76. package/node_modules/@comis/cli/dist/wizard/config-writer.d.ts +0 -25
  77. package/node_modules/@comis/cli/dist/wizard/config-writer.js +0 -144
  78. package/node_modules/@comis/cli/dist/wizard/flow-types.d.ts +0 -48
  79. package/node_modules/@comis/cli/dist/wizard/flow-types.js +0 -70
  80. package/node_modules/@comis/cli/dist/wizard/manual-flow.d.ts +0 -21
  81. package/node_modules/@comis/cli/dist/wizard/manual-flow.js +0 -345
  82. package/node_modules/@comis/cli/dist/wizard/quickstart-flow.d.ts +0 -21
  83. package/node_modules/@comis/cli/dist/wizard/quickstart-flow.js +0 -116
  84. package/node_modules/@comis/core/dist/config/schema-agent-model.d.ts +0 -135
  85. package/node_modules/@comis/core/dist/config/schema-agent-model.js +0 -114
  86. package/node_modules/@comis/core/dist/config/schema-agent-session.d.ts +0 -177
  87. package/node_modules/@comis/core/dist/config/schema-agent-session.js +0 -116
  88. package/node_modules/@comis/core/dist/config/schema-context-engine.d.ts +0 -92
  89. package/node_modules/@comis/core/dist/config/schema-context-engine.js +0 -92
  90. package/node_modules/@comis/core/dist/config/schema-context-guard.d.ts +0 -34
  91. package/node_modules/@comis/core/dist/config/schema-context-guard.js +0 -32
  92. package/node_modules/@comis/core/dist/config/schema-delivery-mirror.d.ts +0 -27
  93. package/node_modules/@comis/core/dist/config/schema-delivery-mirror.js +0 -26
  94. package/node_modules/@comis/core/dist/config/schema-delivery-queue.d.ts +0 -31
  95. package/node_modules/@comis/core/dist/config/schema-delivery-queue.js +0 -30
  96. package/node_modules/@comis/core/dist/config/schema-delivery-timing.d.ts +0 -41
  97. package/node_modules/@comis/core/dist/config/schema-delivery-timing.js +0 -31
  98. package/node_modules/@comis/core/dist/config/schema-monitoring.d.ts +0 -105
  99. package/node_modules/@comis/core/dist/config/schema-monitoring.js +0 -67
  100. package/node_modules/@comis/core/dist/ports/media-ports.d.ts +0 -278
  101. package/node_modules/@comis/core/dist/ports/media-ports.js +0 -1
  102. package/node_modules/@comis/core/dist/security/input-guard.d.ts +0 -46
  103. package/node_modules/@comis/core/dist/security/input-guard.js +0 -166
  104. package/node_modules/@comis/core/dist/security/scoped-secret-manager.d.ts +0 -38
  105. package/node_modules/@comis/core/dist/security/scoped-secret-manager.js +0 -94
  106. package/node_modules/@comis/daemon/dist/observability/delivery-context.d.ts +0 -37
  107. package/node_modules/@comis/daemon/dist/observability/delivery-context.js +0 -1
  108. package/node_modules/@comis/daemon/dist/observability/log-level-manager.d.ts +0 -23
  109. package/node_modules/@comis/daemon/dist/observability/log-level-manager.js +0 -34
  110. package/node_modules/@comis/daemon/dist/observability/log-transport.d.ts +0 -44
  111. package/node_modules/@comis/daemon/dist/observability/log-transport.js +0 -74
  112. package/node_modules/@comis/daemon/dist/observability/obs-write-buffer.d.ts +0 -53
  113. package/node_modules/@comis/daemon/dist/observability/obs-write-buffer.js +0 -68
  114. package/node_modules/@comis/daemon/dist/observability/types.d.ts +0 -6
  115. package/node_modules/@comis/daemon/dist/observability/types.js +0 -1
  116. package/node_modules/@comis/daemon/dist/wiring/seed-bundled-skills.d.ts +0 -41
  117. package/node_modules/@comis/daemon/dist/wiring/seed-bundled-skills.js +0 -84
  118. package/node_modules/@comis/daemon/dist/wiring/setup-delivery-mirror.d.ts +0 -24
  119. package/node_modules/@comis/daemon/dist/wiring/setup-delivery-mirror.js +0 -88
  120. package/node_modules/@comis/daemon/dist/wiring/setup-delivery-queue.d.ts +0 -31
  121. package/node_modules/@comis/daemon/dist/wiring/setup-delivery-queue.js +0 -132
  122. package/node_modules/@comis/daemon/dist/wiring/setup-monitoring.d.ts +0 -38
  123. package/node_modules/@comis/daemon/dist/wiring/setup-monitoring.js +0 -100
  124. package/node_modules/@comis/daemon/dist/wiring/setup-rpc-bridge.d.ts +0 -34
  125. package/node_modules/@comis/daemon/dist/wiring/setup-rpc-bridge.js +0 -52
  126. package/node_modules/@comis/daemon/dist/wiring/setup-task-extraction.d.ts +0 -41
  127. package/node_modules/@comis/daemon/dist/wiring/setup-task-extraction.js +0 -86
  128. package/node_modules/@comis/memory/dist/embedding-cache.d.ts +0 -36
  129. package/node_modules/@comis/memory/dist/embedding-cache.js +0 -94
  130. package/node_modules/@comis/skills/dist/bridge/tool-output-schemas.d.ts +0 -17
  131. package/node_modules/@comis/skills/dist/bridge/tool-output-schemas.js +0 -125
  132. package/node_modules/@comis/skills/dist/bridge/tool-parallelism-metadata.d.ts +0 -14
  133. package/node_modules/@comis/skills/dist/bridge/tool-parallelism-metadata.js +0 -92
  134. package/node_modules/@comis/skills/dist/bridge/tool-result-caps.d.ts +0 -14
  135. package/node_modules/@comis/skills/dist/bridge/tool-result-caps.js +0 -36
  136. package/node_modules/@comis/skills/dist/bridge/tool-search-hints.d.ts +0 -15
  137. package/node_modules/@comis/skills/dist/bridge/tool-search-hints.js +0 -68
  138. package/node_modules/@comis/skills/dist/bridge/tool-validators.d.ts +0 -11
  139. package/node_modules/@comis/skills/dist/bridge/tool-validators.js +0 -105
  140. package/node_modules/@comis/skills/dist/builtin/file/find-sort-wrapper.d.ts +0 -22
  141. package/node_modules/@comis/skills/dist/builtin/file/find-sort-wrapper.js +0 -95
  142. package/node_modules/@comis/skills/dist/builtin/file/grep-output-mode-wrapper.d.ts +0 -24
  143. package/node_modules/@comis/skills/dist/builtin/file/grep-output-mode-wrapper.js +0 -167
  144. package/node_modules/@comis/skills/dist/builtin/task-plan-tool.d.ts +0 -25
  145. package/node_modules/@comis/skills/dist/builtin/task-plan-tool.js +0 -67
  146. package/node_modules/@comis/skills/dist/integrations/mcp-tool-bridge.d.ts +0 -75
  147. 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
- // Client-side validation errors (e.g. Anthropic 400 invalid_request_error
256
- // on thinking-block immutability) are deterministic retrying with the
257
- // same message body will reproduce the same failure. Short-circuit before
258
- // the strip+retry block to avoid wasting tokens on a repeat request.
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 === "client_request") {
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
  *
@@ -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
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/agent",
3
3
  "private": true,
4
- "version": "1.0.19",
4
+ "version": "1.0.22",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "AI agent executor, budget control, and session management for Comis",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/channels",
3
3
  "private": true,
4
- "version": "1.0.19",
4
+ "version": "1.0.22",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Chat platform adapters — Discord, Telegram, Slack, WhatsApp, Signal, iMessage, IRC, LINE",
File without changes
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/cli",
3
3
  "private": true,
4
- "version": "1.0.19",
4
+ "version": "1.0.22",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Command-line interface for the Comis AI agent platform",
@@ -11,6 +11,7 @@
11
11
  *
12
12
  * @module
13
13
  */
14
+ import { resolve as resolvePath } from "node:path";
14
15
  import { ok, err } from "@comis/shared";
15
16
  // ---------------------------------------------------------------------------
16
17
  // Constants
@@ -179,10 +180,15 @@ export function createConfigGitManager(deps) {
179
180
  if (initialized) {
180
181
  return ok(undefined);
181
182
  }
182
- // Check if .git already exists by running git status
183
- const statusResult = await execGit(["status", "--porcelain"], configDir);
184
- if (statusResult.ok) {
185
- // Repo already exists and is functional
183
+ // Probe for an existing git repo whose toplevel IS configDir.
184
+ // `git status` succeeds whenever any ancestor has a .git directory,
185
+ // which would silently treat a parent project's repo as ours and
186
+ // commit config changes onto whatever branch is checked out there.
187
+ // Use rev-parse --show-toplevel and require an exact path match;
188
+ // otherwise fall through and `git init` our own nested repo.
189
+ const toplevelResult = await execGit(["rev-parse", "--show-toplevel"], configDir);
190
+ if (toplevelResult.ok &&
191
+ resolvePath(toplevelResult.value.trim()) === resolvePath(configDir)) {
186
192
  initialized = true;
187
193
  return ok(undefined);
188
194
  }
@@ -65,6 +65,7 @@ export { substituteEnvVars, warnSuspiciousEnvValues, extractReferencedSecretName
65
65
  export { migrateConfig } from "./migrate.js";
66
66
  export { deepMerge, mergeLayered, loadLayered } from "./layered.js";
67
67
  export { IMMUTABLE_CONFIG_PREFIXES, MUTABLE_CONFIG_OVERRIDES, isImmutableConfigPath, matchesOverridePattern, getMutableOverridesForSection } from "./immutable-keys.js";
68
+ export { MANAGED_SECTIONS, getManagedSectionRedirect, formatRedirectHint, type ManagedSectionRedirect, } from "./managed-sections.js";
68
69
  export { getConfigSchema, getConfigSections } from "./schema-serializer.js";
69
70
  export { createTimestampedBackup } from "./backup.js";
70
71
  export type { BackupDeps, BackupOptions } from "./backup.js";
@@ -58,6 +58,8 @@ export { migrateConfig } from "./migrate.js";
58
58
  export { deepMerge, mergeLayered, loadLayered } from "./layered.js";
59
59
  // Immutable key guard (runtime config mutation protection)
60
60
  export { IMMUTABLE_CONFIG_PREFIXES, MUTABLE_CONFIG_OVERRIDES, isImmutableConfigPath, matchesOverridePattern, getMutableOverridesForSection } from "./immutable-keys.js";
61
+ // Managed-section redirects (LLM-readable hints for immutable rejections)
62
+ export { MANAGED_SECTIONS, getManagedSectionRedirect, formatRedirectHint, } from "./managed-sections.js";
61
63
  // Schema serializer (Zod to JSON Schema conversion)
62
64
  export { getConfigSchema, getConfigSections } from "./schema-serializer.js";
63
65
  // Config backup (timestamped backup creation with rotation)