comisai 1.0.25 → 1.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/node_modules/@comis/agent/dist/bootstrap/sections/tool-descriptions.js +130 -10
  2. package/node_modules/@comis/agent/dist/bootstrap/sections/tooling-sections.d.ts +1 -1
  3. package/node_modules/@comis/agent/dist/bootstrap/sections/tooling-sections.js +9 -2
  4. package/node_modules/@comis/agent/dist/bridge/bridge-metrics.d.ts +8 -0
  5. package/node_modules/@comis/agent/dist/bridge/bridge-metrics.js +2 -0
  6. package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.d.ts +29 -0
  7. package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.js +242 -2
  8. package/node_modules/@comis/agent/dist/bridge/thinking-block-hash-invariant.d.ts +210 -0
  9. package/node_modules/@comis/agent/dist/bridge/thinking-block-hash-invariant.js +566 -0
  10. package/node_modules/@comis/agent/dist/context-engine/context-engine.js +8 -6
  11. package/node_modules/@comis/agent/dist/context-engine/signature-replay-scrubber.d.ts +51 -30
  12. package/node_modules/@comis/agent/dist/context-engine/signature-replay-scrubber.js +109 -36
  13. package/node_modules/@comis/agent/dist/executor/executor-context-engine-setup.js +5 -1
  14. package/node_modules/@comis/agent/dist/executor/executor-post-execution.js +22 -20
  15. package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.d.ts +2 -0
  16. package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.js +111 -15
  17. package/node_modules/@comis/agent/dist/executor/executor-response-filter.d.ts +20 -17
  18. package/node_modules/@comis/agent/dist/executor/executor-response-filter.js +132 -52
  19. package/node_modules/@comis/agent/dist/executor/executor-tool-assembly.js +16 -3
  20. package/node_modules/@comis/agent/dist/executor/model-retry.d.ts +14 -0
  21. package/node_modules/@comis/agent/dist/executor/model-retry.js +72 -1
  22. package/node_modules/@comis/agent/dist/executor/pi-executor.d.ts +3 -0
  23. package/node_modules/@comis/agent/dist/executor/pi-executor.js +68 -9
  24. package/node_modules/@comis/agent/dist/executor/post-batch-continuation.d.ts +82 -0
  25. package/node_modules/@comis/agent/dist/executor/post-batch-continuation.js +200 -0
  26. package/node_modules/@comis/agent/dist/executor/stream-wrappers/request-body-injector.js +1 -9
  27. package/node_modules/@comis/agent/dist/executor/tool-deferral.d.ts +37 -2
  28. package/node_modules/@comis/agent/dist/executor/tool-deferral.js +45 -3
  29. package/node_modules/@comis/agent/dist/executor/tool-parallelism.js +0 -1
  30. package/node_modules/@comis/agent/dist/executor/types.d.ts +11 -2
  31. package/node_modules/@comis/agent/dist/index.d.ts +3 -1
  32. package/node_modules/@comis/agent/dist/index.js +2 -0
  33. package/node_modules/@comis/agent/dist/model/last-known-model.d.ts +36 -0
  34. package/node_modules/@comis/agent/dist/model/last-known-model.js +49 -0
  35. package/node_modules/@comis/agent/dist/model/model-registry-adapter.d.ts +16 -4
  36. package/node_modules/@comis/agent/dist/model/model-registry-adapter.js +65 -21
  37. package/node_modules/@comis/agent/dist/planner/types.d.ts +0 -2
  38. package/node_modules/@comis/agent/dist/session/comis-session-manager.d.ts +10 -0
  39. package/node_modules/@comis/agent/dist/session/comis-session-manager.js +5 -0
  40. package/node_modules/@comis/agent/dist/spawn/pi-mono-adapters.js +7 -0
  41. package/node_modules/@comis/agent/package.json +1 -1
  42. package/node_modules/@comis/channels/package.json +1 -1
  43. package/node_modules/@comis/cli/dist/client/rpc-client.js +6 -1
  44. package/node_modules/@comis/cli/dist/commands/doctor.js +5 -3
  45. package/node_modules/@comis/cli/dist/commands/health.js +5 -2
  46. package/node_modules/@comis/cli/dist/wizard/json-output.js +7 -3
  47. package/node_modules/@comis/cli/dist/wizard/steps/11-daemon-start.js +130 -0
  48. package/node_modules/@comis/cli/package.json +1 -1
  49. package/node_modules/@comis/core/dist/config/immutable-keys.d.ts +2 -2
  50. package/node_modules/@comis/core/dist/config/immutable-keys.js +8 -3
  51. package/node_modules/@comis/core/dist/config/managed-sections.d.ts +43 -4
  52. package/node_modules/@comis/core/dist/config/managed-sections.js +100 -6
  53. package/node_modules/@comis/core/dist/config/schema-agent.d.ts +39 -0
  54. package/node_modules/@comis/core/dist/config/schema-agent.js +14 -0
  55. package/node_modules/@comis/core/dist/config/schema.d.ts +4 -0
  56. package/node_modules/@comis/core/dist/config/schema.js +14 -0
  57. package/node_modules/@comis/core/dist/domain/execution-graph.d.ts +1 -1
  58. package/node_modules/@comis/core/dist/event-bus/events-agent.d.ts +17 -2
  59. package/node_modules/@comis/core/dist/exports/config.d.ts +2 -2
  60. package/node_modules/@comis/core/dist/exports/config.js +1 -1
  61. package/node_modules/@comis/core/package.json +1 -1
  62. package/node_modules/@comis/daemon/dist/daemon.d.ts +22 -0
  63. package/node_modules/@comis/daemon/dist/daemon.js +42 -0
  64. package/node_modules/@comis/daemon/dist/rpc/agent-handlers.d.ts +5 -2
  65. package/node_modules/@comis/daemon/dist/rpc/agent-handlers.js +80 -1
  66. package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.d.ts +67 -0
  67. package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.js +139 -0
  68. package/node_modules/@comis/daemon/dist/rpc/model-handlers.d.ts +3 -0
  69. package/node_modules/@comis/daemon/dist/rpc/model-handlers.js +29 -5
  70. package/node_modules/@comis/daemon/dist/rpc/probe-provider-auth.d.ts +30 -0
  71. package/node_modules/@comis/daemon/dist/rpc/probe-provider-auth.js +59 -0
  72. package/node_modules/@comis/daemon/dist/rpc/provider-handlers.d.ts +37 -0
  73. package/node_modules/@comis/daemon/dist/rpc/provider-handlers.js +330 -0
  74. package/node_modules/@comis/daemon/dist/rpc/rpc-dispatch.js +18 -1
  75. package/node_modules/@comis/daemon/dist/setup-docker-restart-warn.d.ts +4 -0
  76. package/node_modules/@comis/daemon/dist/setup-docker-restart-warn.js +30 -0
  77. package/node_modules/@comis/daemon/dist/wiring/setup-agents.d.ts +3 -1
  78. package/node_modules/@comis/daemon/dist/wiring/setup-agents.js +28 -2
  79. package/node_modules/@comis/daemon/dist/wiring/setup-cross-session.js +1 -0
  80. package/node_modules/@comis/daemon/dist/wiring/setup-tools.js +7 -4
  81. package/node_modules/@comis/daemon/package.json +1 -1
  82. package/node_modules/@comis/gateway/package.json +1 -1
  83. package/node_modules/@comis/infra/dist/index.d.ts +1 -0
  84. package/node_modules/@comis/infra/dist/index.js +2 -0
  85. package/node_modules/@comis/infra/dist/runtime/is-docker.d.ts +1 -0
  86. package/node_modules/@comis/infra/dist/runtime/is-docker.js +25 -0
  87. package/node_modules/@comis/infra/package.json +1 -1
  88. package/node_modules/@comis/memory/package.json +1 -1
  89. package/node_modules/@comis/scheduler/package.json +1 -1
  90. package/node_modules/@comis/shared/package.json +1 -1
  91. package/node_modules/@comis/skills/dist/bridge/tool-metadata-registry.js +1 -3
  92. package/node_modules/@comis/skills/dist/builtin/platform/admin-manage-factory.js +24 -1
  93. package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.d.ts +53 -7
  94. package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.js +218 -24
  95. package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.d.ts +4 -1
  96. package/node_modules/@comis/skills/dist/builtin/platform/gateway-tool.js +16 -1
  97. package/node_modules/@comis/skills/dist/builtin/platform/index.d.ts +1 -1
  98. package/node_modules/@comis/skills/dist/builtin/platform/index.js +1 -1
  99. package/node_modules/@comis/skills/dist/builtin/platform/providers-manage-tool.d.ts +56 -0
  100. package/node_modules/@comis/skills/dist/builtin/platform/providers-manage-tool.js +203 -0
  101. package/node_modules/@comis/skills/dist/index.d.ts +1 -1
  102. package/node_modules/@comis/skills/dist/index.js +2 -2
  103. package/node_modules/@comis/skills/dist/policy/tool-policy.js +0 -1
  104. package/node_modules/@comis/skills/package.json +1 -1
  105. package/node_modules/@comis/web/dist/assets/{agent-detail-ru-AhppM.js → agent-detail-DqL6Artv.js} +1 -1
  106. package/node_modules/@comis/web/dist/assets/{agent-editor-hjwRuFVp.js → agent-editor-CNM_h94Y.js} +1 -1
  107. package/node_modules/@comis/web/dist/assets/{agent-list-6Uotjatr.js → agent-list-Dbh-xD_F.js} +1 -1
  108. package/node_modules/@comis/web/dist/assets/{billing-view-CxysXH0p.js → billing-view-C1DmtyzK.js} +1 -1
  109. package/node_modules/@comis/web/dist/assets/{channel-detail-BBCKtmne.js → channel-detail-CtCH22N1.js} +1 -1
  110. package/node_modules/@comis/web/dist/assets/{channel-list-FkfeOLBQ.js → channel-list-C7xXn-60.js} +1 -1
  111. package/node_modules/@comis/web/dist/assets/{chat-console-BumBaIgO.js → chat-console-C51pjFwk.js} +1 -1
  112. package/node_modules/@comis/web/dist/assets/{config-editor-C9BSwHGy.js → config-editor-BLArYRB7.js} +1 -1
  113. package/node_modules/@comis/web/dist/assets/{context-dag-browser-BHm00mJD.js → context-dag-browser-fuyMinNI.js} +1 -1
  114. package/node_modules/@comis/web/dist/assets/{context-engine-BENY3pWE.js → context-engine-Bngf2bH0.js} +1 -1
  115. package/node_modules/@comis/web/dist/assets/{delivery-view-BCnkPsAp.js → delivery-view-C80hucxX.js} +1 -1
  116. package/node_modules/@comis/web/dist/assets/{diagnostics-view-C_jQFG2H.js → diagnostics-view-Cl4VbHZ6.js} +1 -1
  117. package/node_modules/@comis/web/dist/assets/{ic-chat-message-FdQcZsSQ.js → ic-chat-message-ByFUoMm6.js} +1 -1
  118. package/node_modules/@comis/web/dist/assets/{ic-connection-dot-BgYiK2N4.js → ic-connection-dot-C4nDHgY2.js} +1 -1
  119. package/node_modules/@comis/web/dist/assets/{ic-tool-call-DMPHsLyx.js → ic-tool-call-Bh5kq-yY.js} +1 -1
  120. package/node_modules/@comis/web/dist/assets/{index-FLPhHz8p.js → index-BBkuC-EU.js} +2 -2
  121. package/node_modules/@comis/web/dist/assets/{mcp-management-5jyScQis.js → mcp-management-DB-phOo7.js} +1 -1
  122. package/node_modules/@comis/web/dist/assets/{media-config-J9oT9PPs.js → media-config-CRqZ1ZUH.js} +1 -1
  123. package/node_modules/@comis/web/dist/assets/{media-test-DGTCtM8-.js → media-test-C9vE20Oy.js} +1 -1
  124. package/node_modules/@comis/web/dist/assets/{memory-inspector-D5Re9ptG.js → memory-inspector-CeqfnxMZ.js} +1 -1
  125. package/node_modules/@comis/web/dist/assets/{message-center-cRLK6ZmG.js → message-center-Daup7Mof.js} +1 -1
  126. package/node_modules/@comis/web/dist/assets/{models-D5vu07MR.js → models-DLYnEU8E.js} +1 -1
  127. package/node_modules/@comis/web/dist/assets/{observe-view-CalNNEmd.js → observe-view-BTSt_PO5.js} +1 -1
  128. package/node_modules/@comis/web/dist/assets/{pipeline-builder-DUYDGwZf.js → pipeline-builder-DknfzyLt.js} +1 -1
  129. package/node_modules/@comis/web/dist/assets/{pipeline-history-BAO8brOe.js → pipeline-history-JnHZdeU_.js} +1 -1
  130. package/node_modules/@comis/web/dist/assets/{pipeline-history-detail-DectIoQt.js → pipeline-history-detail-Dg4knsEb.js} +1 -1
  131. package/node_modules/@comis/web/dist/assets/{pipeline-list-BHlaBKww.js → pipeline-list-AEnibjsp.js} +1 -1
  132. package/node_modules/@comis/web/dist/assets/{pipeline-monitor-BhtpNEHf.js → pipeline-monitor-DG7RbIOO.js} +1 -1
  133. package/node_modules/@comis/web/dist/assets/{scheduler-VafN_8xi.js → scheduler-uL1fYKAT.js} +1 -1
  134. package/node_modules/@comis/web/dist/assets/{security-QQXMRTlo.js → security-C3DywRLH.js} +1 -1
  135. package/node_modules/@comis/web/dist/assets/{session-detail-BpZ_8Yih.js → session-detail-BtqCNWXV.js} +1 -1
  136. package/node_modules/@comis/web/dist/assets/{session-list-DfCm8Cec.js → session-list-CJXWa2XT.js} +1 -1
  137. package/node_modules/@comis/web/dist/assets/{setup-wizard-C-z477CG.js → setup-wizard-ywn7oJvu.js} +1 -1
  138. package/node_modules/@comis/web/dist/assets/{skills-BCOGPf6s.js → skills-DX0KYnWD.js} +1 -1
  139. package/node_modules/@comis/web/dist/assets/{subagents-l-auUraL.js → subagents-B8p5YJEB.js} +1 -1
  140. package/node_modules/@comis/web/dist/assets/{workspace-manager-DlvBixiq.js → workspace-manager-CgzNIrw1.js} +1 -1
  141. package/node_modules/@comis/web/dist/index.html +1 -1
  142. package/node_modules/@comis/web/package.json +1 -1
  143. package/package.json +13 -13
  144. package/node_modules/@comis/skills/dist/builtin/platform/agents-list-tool.d.ts +0 -19
  145. package/node_modules/@comis/skills/dist/builtin/platform/agents-list-tool.js +0 -39
@@ -53,7 +53,6 @@ export const TOOL_SUMMARIES = {
53
53
  pipeline: "Execute multi-node DAG workflow pipelines",
54
54
  session_status: "Show agent status and usage",
55
55
  session_search: "Search full session transcript history",
56
- agents_list: "List all available agent IDs",
57
56
  // Platform
58
57
  cron: "Manage cron jobs and reminders",
59
58
  gateway: "Read or patch system config",
@@ -81,6 +80,7 @@ export const TOOL_SUMMARIES = {
81
80
  channels_manage: "Manage channel adapter status (admin)",
82
81
  tokens_manage: "Manage gateway API tokens (admin)",
83
82
  models_manage: "List models and test availability",
83
+ providers_manage: "Manage LLM provider endpoints (admin)",
84
84
  skills_manage: "Manage skill registry entries (admin)",
85
85
  mcp_manage: "Manage MCP server connections (admin)",
86
86
  heartbeat_manage: "Manage agent heartbeat schedules (admin)",
@@ -117,8 +117,7 @@ export const LEAN_TOOL_DESCRIPTIONS = {
117
117
  return `Send, reply, react, edit, delete, fetch messages on ${ch}. For inter-session messaging, use sessions_send.`;
118
118
  },
119
119
  // ----- Sessions -----
120
- // Confusable pair: sessions_list / agents_list
121
- sessions_list: "List active sessions with filters. For available agent IDs, use agents_list.",
120
+ sessions_list: "List active sessions with filters. For available agent IDs, use agents_manage({action:'list'}).",
122
121
  sessions_history: "Fetch conversation history for another session or sub-agent.",
123
122
  // Confusable pair: sessions_send / message
124
123
  sessions_send: "Send message to another session. For chat channel messages, use message.",
@@ -128,8 +127,6 @@ export const LEAN_TOOL_DESCRIPTIONS = {
128
127
  session_status: "Show agent status card: usage, model, steps. Optional per-session model override.",
129
128
  // Confusable pair: session_search / memory_search
130
129
  session_search: "Search full session transcript including evicted content. For stored facts, use memory_search.",
131
- // Confusable pair: agents_list / sessions_list
132
- agents_list: "List available agent IDs for spawning. For active sessions, use sessions_list.",
133
130
  // ----- Platform -----
134
131
  cron: "Manage cron jobs, scheduled tasks, and reminders.",
135
132
  gateway: "Read/patch config, restart gateway, check status.",
@@ -151,7 +148,7 @@ export const LEAN_TOOL_DESCRIPTIONS = {
151
148
  ctx_recall: "Recall evicted context entries by semantic query.",
152
149
  // ----- Privileged / Supervisor (dynamic: admin suffix) -----
153
150
  agents_manage: (ctx) => {
154
- const base = "Manage agent fleet: create, get, update, delete, suspend, resume.";
151
+ const base = "Manage agent fleet: list, create, get, update, delete, suspend, resume. For batch creation, pass workspace.role/identity inline to skip the 2-step write flow.";
155
152
  return ctx.trustLevel === "admin" ? base : base + " Admin required.";
156
153
  },
157
154
  obs_query: (ctx) => {
@@ -177,6 +174,10 @@ export const LEAN_TOOL_DESCRIPTIONS = {
177
174
  return ctx.trustLevel === "admin" ? base : base + " Admin required.";
178
175
  },
179
176
  models_manage: "List available models and test provider availability.",
177
+ providers_manage: (ctx) => {
178
+ const base = "Manage LLM providers: list, get, create, update, delete, enable, disable.";
179
+ return ctx.trustLevel === "admin" ? base : base + " Admin required.";
180
+ },
180
181
  skills_manage: (ctx) => {
181
182
  const base = "Manage skill registry: list, reload, enable, disable skills.";
182
183
  return ctx.trustLevel === "admin" ? base : base + " Admin required.";
@@ -206,7 +207,7 @@ export const TOOL_ORDER = [
206
207
  // Middle (low-frequency): platform actions, privileged, context, media
207
208
  "discord_action", "telegram_action", "slack_action", "whatsapp_action",
208
209
  "agents_manage", "obs_query", "sessions_manage", "memory_manage",
209
- "channels_manage", "tokens_manage", "models_manage", "skills_manage", "mcp_manage", "heartbeat_manage",
210
+ "channels_manage", "tokens_manage", "models_manage", "providers_manage", "skills_manage", "mcp_manage", "heartbeat_manage",
210
211
  "ctx_search", "ctx_inspect", "ctx_expand", "ctx_recall",
211
212
  "image_analyze", "tts_synthesize", "transcribe_audio", "describe_video", "extract_document",
212
213
  "browser", "gateway",
@@ -215,7 +216,7 @@ export const TOOL_ORDER = [
215
216
  "memory_store", "memory_get",
216
217
  "web_fetch",
217
218
  "sessions_list", "sessions_history", "sessions_send", "sessions_spawn",
218
- "subagents", "pipeline", "session_status", "session_search", "agents_list",
219
+ "subagents", "pipeline", "session_status", "session_search",
219
220
  "cron", "process",
220
221
  "discover_tools",
221
222
  ];
@@ -230,7 +231,28 @@ export const TOOL_ORDER = [
230
231
  * Not all tools need guides -- most are self-explanatory from their lean description.
231
232
  */
232
233
  export const TOOL_GUIDES = {
233
- agents_manage: `## Workspace Customization Guide
234
+ agents_manage: `## Single-call creation (PREFERRED for batch fleet creation)
235
+ For batch creation (multiple agents in one turn) and any case where you already know the agent's role and identity, use the SINGLE-CALL form. This collapses the previous 3-call workflow (create + 2× write) into 1 call per agent — critical when creating fleets of 5+ agents in parallel.
236
+
237
+ agents_manage({action:"create", agent_id, config:{
238
+ name, model, provider, maxSteps,
239
+ workspace:{
240
+ profile:"full"|"specialist",
241
+ role:"...persona, role, behavioral guidelines, domain conventions...", // inline ROLE.md content (max 16384 chars)
242
+ identity:"...name, creature, vibe, emoji, ethos..." // inline IDENTITY.md content (max 4096 chars)
243
+ },
244
+ skills?:{...}
245
+ }})
246
+
247
+ The daemon writes ROLE.md and IDENTITY.md atomically as part of create. The tool result reports inlineWritesResult; on success the next-step contract says "No further setup needed — agent is operationally ready" and you SKIP the write() roundtrip entirely.
248
+
249
+ ## Two-step creation flow (FALLBACK)
250
+ Use this only when role/identity cannot be inlined (e.g. content discovered after create, multi-step interactive design):
251
+ Step 1 — call agents_manage({action:"create", agent_id, config:{name, model, provider, maxSteps, workspace?:{profile:"full"|"specialist"}, skills?:{...}}}). Only those fields are accepted; the schema is z.strictObject so unknown keys are rejected. Do NOT pass persona/role/description/prompt/instructions in create config when omitting the inline form.
252
+ Step 2 — call write({path: "~/.comis/workspace-{agent_id}/ROLE.md", content: "...persona/role/behavioral guidance..."}) to set the agent's role and identity. Use IDENTITY.md for name/creature/vibe/emoji.
253
+
254
+ Workspace.profile values: "full" or "specialist" ONLY. No "none", "minimal", "compact", or other values.
255
+ ## Workspace Customization Guide
234
256
  Each agent gets a workspace at ~/.comis/workspace-{agentId}/ with these files:
235
257
  IDENTITY.md (CRITICAL): Set Name, Creature, Vibe, Emoji. A filled Name auto-skips onboarding.
236
258
  ROLE.md (CRITICAL): Agent role, behavioral guidelines, domain conventions.
@@ -247,7 +269,103 @@ All built-in tools ENABLED by default (except browser). Do NOT disable tools unl
247
269
  maxSteps default: 50. Do NOT set below 20.
248
270
  ## Batch Creation
249
271
  Present a plan to the user before creating agents in batch.
250
- Multiple agents can be created in one turn. Customize ALL workspace files for each agent after creation.`,
272
+ Multiple agents can be created in one turn. Customize ALL workspace files for each agent after creation — or use single-call creation above to inline ROLE.md/IDENTITY.md and skip the post-create writes entirely.`,
273
+ providers_manage: `## Provider Configuration Guide
274
+
275
+ ### Built-in vs Custom Provider Check (MANDATORY first step)
276
+ Before creating a custom provider, check if the model already exists in the built-in catalog. Built-in providers (anthropic, google, openai, groq, mistral, deepseek, cerebras, xai, openrouter) already have their models registered — creating a redundant custom entry is wrong and will be ignored. Use models_manage({ action: "list" }) to see available built-in models.
277
+
278
+ If the model IS built-in: skip provider creation. Just store the API key (gateway env_set) and switch the agent directly.
279
+ If the model is NOT built-in: you need a custom provider. Proceed to the steps below, but first gather ALL required configuration.
280
+
281
+ ### Information Gathering for Custom Providers
282
+ When creating a non-built-in provider, you MUST have: (1) the API base URL, (2) the exact model ID string, (3) the API protocol type. If the user did not supply all three:
283
+ 1. Use web_search to look up the provider's API documentation (search for "<provider name> API base URL" or "<provider name> API docs").
284
+ 2. If web search finds the information, use it to fill in the missing fields.
285
+ 3. If web search does NOT find the information, ask the user to supply the missing fields before proceeding. Do NOT guess or invent URLs.
286
+
287
+ ### Credential Workflow
288
+ API keys are NEVER stored in provider config. Always use this two-step process:
289
+ 1. Store the API key: gateway({ action: "env_set", env_key: "<KEY_NAME>", env_value: "<key>" })
290
+ 2. Create the provider: providers_manage({ action: "create", provider_id: "<name>", config: { type: "openai", baseUrl: "<url>", apiKeyName: "<KEY_NAME>", models: [{ id: "<model>" }] } })
291
+
292
+ For local providers (Ollama, LM Studio, vLLM) that don't need API keys, omit apiKeyName.
293
+
294
+ ### After Creating a Provider
295
+ Switch an agent to use the new provider:
296
+ agents_manage({ action: "update", agent_id: "<id>", config: { provider: "<provider_id>", model: "<model_id>" } })
297
+
298
+ ### Switching an Agent's Provider or Model
299
+ To switch an agent to a different provider/model, call agents_manage update with the new pair:
300
+ agents_manage({ action: "update", agent_id: "<id>", config: { provider: "<provider_id>", model: "<model_id>" } })
301
+
302
+ \`agents.*.model\` and \`agents.*.provider\` are listed in MUTABLE_CONFIG_OVERRIDES, so the immutability guard does not block the patch.
303
+
304
+ **Two preconditions the LLM MUST verify before issuing the update:**
305
+ 1. The target provider exists as a \`providers.entries.<provider_id>\` key. If it does not, call providers_manage create FIRST (and gateway env_set for the API key if needed). Patching an agent to a provider that has no entry resolves under the wrong provider family at the next session — the original bug.
306
+ 2. The model id matches a \`models[].id\` in that provider entry (or is a built-in known to the pi-ai catalog for that provider type). Otherwise \`registry.find(provider, model)\` returns undefined and the next session falls back with a "Model not found" message.
307
+
308
+ **Timing — the change is NOT hot-applied to the active session.**
309
+ agents_manage update writes through persistToConfig WITHOUT a hot-update callback, which triggers a SIGUSR2 daemon restart (2-second debounce). The new provider/model takes effect on the next session, not the currently-running prompt. Tell the user the switch is queued and will take effect after the daemon settles.
310
+
311
+ ### Configuring Model Failover
312
+ After creating providers, configure automatic failover so the agent recovers from provider outages without user intervention:
313
+ agents_manage({ action: "update", agent_id: "<id>", config: { modelFailover: { fallbackModels: [{ provider: "<fallback_provider>", modelId: "<fallback_model>" }] } } })
314
+
315
+ Failover order: primary model > cache-aware short retry (same model) > auth key rotation (same provider) > fallback models in order. Each fallback entry is a { provider, modelId } pair referencing a configured provider.
316
+
317
+ ### Adding vs Replacing a Fallback (read-modify-write)
318
+ \`fallbackModels\` and \`authProfiles\` are **replaced wholesale** on update — the array you send becomes the complete new state. Scalar fields (cooldownInitialMs, maxAttempts, etc.) are deep-merged and preserved.
319
+
320
+ When the user asks to **add** a fallback to an agent (vs. replace the whole chain), do this:
321
+ 1. agents_manage({ action: "get", agent_id: "<id>" }) > read existing config.modelFailover.fallbackModels
322
+ 2. Append the new { provider, modelId } entry to that array (preserving order)
323
+ 3. agents_manage({ action: "update", agent_id: "<id>", config: { modelFailover: { fallbackModels: [...existing, ...new] } } })
324
+
325
+ Same pattern applies to authProfiles. Skipping the read step silently drops previously-configured fallbacks. When the user says "set" / "use" / "switch fallback to X", a direct overwrite is correct; when they say "add" / "also" / "in addition", read first.
326
+
327
+ To add auth key rotation for rate-limited providers (multiple API keys for the same provider):
328
+ 1. Store additional keys: gateway({ action: "env_set", env_key: "ANTHROPIC_API_KEY_2", env_value: "<key>" })
329
+ 2. Configure auth profiles: agents_manage({ action: "update", agent_id: "<id>", config: { modelFailover: { authProfiles: [{ keyName: "ANTHROPIC_API_KEY_2", provider: "anthropic" }] } } })
330
+
331
+ ### Provider Types
332
+ The "type" field selects the SDK code path (API protocol). Common values:
333
+ - **openai** — Any OpenAI-compatible endpoint: NVIDIA NIM, Groq, Together, Fireworks, Perplexity, DeepSeek, vLLM, LM Studio, llama.cpp, or any custom endpoint
334
+ - **anthropic** — Anthropic Claude API
335
+ - **google** — Google Gemini API
336
+ - **ollama** — Local Ollama server
337
+ - **mistral** — Mistral AI API
338
+ - **groq** — Groq API (also works with type "openai")
339
+ - **together** — Together AI API
340
+ - **deepseek** — DeepSeek API (also works with type "openai")
341
+ - **cerebras** — Cerebras API
342
+ - **xai** — xAI Grok API
343
+ - **openrouter** — OpenRouter multi-provider gateway
344
+
345
+ When in doubt, use type "openai" with a custom baseUrl — most third-party and self-hosted providers speak the OpenAI API format.
346
+
347
+ ### Local Providers (no API key)
348
+ For local inference servers, set baseUrl to the local endpoint and omit apiKeyName:
349
+ providers_manage({ action: "create", provider_id: "local-ollama", config: { type: "ollama", baseUrl: "http://localhost:11434", models: [{ id: "llama3.3" }] } })
350
+
351
+ ### Models
352
+ Only the model "id" is required. All other fields (name, contextWindow, maxTokens, reasoning, input) are optional and will be filled with defaults.
353
+
354
+ ### Adding vs Replacing a Model (read-modify-write)
355
+ \`models\` is **replaced wholesale** on update — the array you send becomes the complete new state. To **add** a model to an existing provider (vs. replace the whole list):
356
+ 1. providers_manage({ action: "get", provider_id: "<id>" }) > read existing config.models
357
+ 2. Append the new { id: "<new-model>" } entry to that array (preserving order)
358
+ 3. providers_manage({ action: "update", provider_id: "<id>", config: { models: [...existing, {id: "<new-model>"}] } })
359
+
360
+ When the user says "add" / "also" / "in addition", read first. When they say "set" / "replace" / "use only", a direct overwrite is correct. Same pattern as agents_manage's modelFailover.fallbackModels.
361
+
362
+ \`headers\` follows a different rule: it is **shallow-merged per key**, so adding one custom header does NOT erase others — direct \`{ headers: { "X-New": "v" } }\` updates work without read-modify-write.
363
+
364
+ ### Clearing a Field
365
+ \`persistToConfig\` cannot remove keys via patch — only set or replace. To clear an apiKeyName (e.g., to convert a cloud provider to keyless), the recipe is: disable > delete > recreate without apiKeyName. Direct YAML edits also work for operators with shell access.
366
+
367
+ ### Fleet-Wide Operations
368
+ providers_manage and agents_manage operate on one entity at a time. For fleet-wide provider/model/failover changes: (1) create new provider(s) first, (2) agents_manage list to discover agents, (3) agents_manage update x N in parallel (one call per agent in the same turn). Group agents by model tier for tiered failover (e.g. opus agents get different fallbacks than sonnet agents).`,
251
369
  pipeline: `## Pipeline Usage Guide
252
370
  Use 'define' action first to validate graph structure before save/execute.
253
371
  CRITICAL: A node receives ONLY the outputs from nodes listed in its depends_on. This is the sole data flow mechanism -- there is no shared state. For fan-in, list ALL required upstream sources in each consumer's depends_on. If node C needs outputs from both A and B, set depends_on: ["A", "B"] -- depending only on an intermediate node that consumed A and B does NOT propagate their outputs.
@@ -476,10 +594,12 @@ calling a gated action will pause execution until the operator approves or denie
476
594
  - memory_manage: delete, flush
477
595
  - channels_manage: enable, disable, restart
478
596
  - tokens_manage: create, revoke, rotate
597
+ - providers_manage: create, delete
479
598
 
480
599
  **Read-only (no approval needed):**
481
600
  - obs_query: all actions (diagnostics, billing, traces, activity)
482
601
  - models_manage: all actions (list models, test availability)
602
+ - providers_manage: list, get, update, enable, disable, set_default, test
483
603
  - agents_manage: get, update, suspend, resume
484
604
  - sessions_manage: export, compact
485
605
  - memory_manage: stats, browse, export
@@ -32,7 +32,7 @@ export declare function buildCompactedOutputRecoverySection(isMinimal: boolean):
32
32
  * @param deferred - When true, returns empty (content delivered via JIT tool result injection).
33
33
  */
34
34
  export declare function buildCodingFallbackSection(toolNames: string[], isMinimal: boolean, deferred?: boolean): string[];
35
- /** The 10 privileged/supervisor tool names. */
35
+ /** The 11 privileged/supervisor tool names. */
36
36
  export declare const PRIVILEGED_TOOL_NAMES: string[];
37
37
  /**
38
38
  * Build the Privileged Tools & Approval Gate section.
@@ -200,10 +200,10 @@ export function buildCodingFallbackSection(toolNames, isMinimal, deferred) {
200
200
  // ---------------------------------------------------------------------------
201
201
  // 5b. Privileged Tools & Approval Gate (skip if minimal or no privileged tools)
202
202
  // ---------------------------------------------------------------------------
203
- /** The 10 privileged/supervisor tool names. */
203
+ /** The 11 privileged/supervisor tool names. */
204
204
  export const PRIVILEGED_TOOL_NAMES = [
205
205
  "agents_manage", "obs_query", "sessions_manage", "memory_manage",
206
- "channels_manage", "tokens_manage", "models_manage",
206
+ "channels_manage", "tokens_manage", "models_manage", "providers_manage",
207
207
  "skills_manage", "mcp_manage", "heartbeat_manage",
208
208
  ];
209
209
  /**
@@ -328,6 +328,7 @@ export function buildPrivilegedToolsSection(toolNames, isMinimal, deferred) {
328
328
  "- memory_manage: delete, flush",
329
329
  "- channels_manage: enable, disable, restart",
330
330
  "- tokens_manage: create, revoke, rotate",
331
+ "- providers_manage: create, delete",
331
332
  "",
332
333
  "**Read-only (no approval needed):**",
333
334
  "- obs_query: all actions (diagnostics, billing, traces, activity)",
@@ -337,6 +338,7 @@ export function buildPrivilegedToolsSection(toolNames, isMinimal, deferred) {
337
338
  "- memory_manage: stats, browse, export",
338
339
  "- channels_manage: list, get",
339
340
  "- tokens_manage: list",
341
+ "- providers_manage: list, get, update, enable, disable",
340
342
  "",
341
343
  "### Approval Gate Behavior",
342
344
  "",
@@ -358,6 +360,11 @@ export function buildPrivilegedToolsSection(toolNames, isMinimal, deferred) {
358
360
  "- **Reset vs delete session**: Reset clears messages but keeps the session identity (good for \"start fresh\"). Delete archives the transcript and removes the session entirely.",
359
361
  "- **Memory delete vs flush**: Delete removes specific entries by ID (surgical). Flush removes all entries for a scope (nuclear -- use with caution, requires approval).",
360
362
  "- **Token rotation**: Prefer rotate over revoke+create -- rotation is atomic and prevents downtime.",
363
+ "- **Built-in first**: Before creating a custom provider, check if the model is already built-in (models_manage list). Built-in providers (anthropic, google, openai, groq, mistral, deepseek, cerebras, xai, openrouter) need only an API key — no custom provider entry. Only create a custom provider for models NOT in the built-in catalog.",
364
+ "- **Provider then agent**: When adding a custom (non-built-in) provider, first create the provider entry (providers_manage create), store the API key if needed (gateway env_set -- skip for keyless providers like Ollama), then switch the agent (agents_manage update). Never set an agent's model to a name that has no matching provider. If you lack the provider's base URL or model ID, use web_search to find it; if that fails, ask the user.",
365
+ "- **Failover chain**: After creating multiple providers, configure automatic model failover on the agent (agents_manage update with modelFailover.fallbackModels). Each fallback entry is a {provider, modelId} pair referencing a configured provider. Failover order: primary > cache-aware retry > auth key rotation > fallback models in order. Never add a fallback model whose provider does not exist.",
366
+ "- **Add vs replace fallback**: modelFailover.fallbackModels and authProfiles are REPLACED wholesale on update (scalar fields deep-merge; arrays do not). When the user says 'add' / 'also' / 'in addition', call agents_manage get FIRST to read the current array, append, then update with the full list. When the user says 'set' / 'use' / 'switch to', overwrite directly.",
367
+ "- **Fleet-wide changes**: providers_manage and agents_manage operate on one entity at a time. For fleet-wide provider/model/failover changes: (1) create new provider(s) first, (2) agents_manage list to discover agents, (3) agents_manage update x N in parallel (one call per agent in the same turn). Group agents by model tier for tiered failover (e.g. opus agents get different fallbacks than sonnet agents).",
361
368
  ];
362
369
  return lines;
363
370
  }
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import type { ExecutionResult } from "../executor/types.js";
12
12
  import type { ContextUsageData } from "../safety/context-window-guard.js";
13
+ import type { ThinkingBlockHash } from "./thinking-block-hash-invariant.js";
13
14
  /** Internal metrics state managed by the bridge. */
14
15
  export interface BridgeMetricsState {
15
16
  totalInputTokens: number;
@@ -56,6 +57,13 @@ export interface BridgeMetricsState {
56
57
  sessionCumulativeCacheSavedUsd: number;
57
58
  totalThinkingTokens: number;
58
59
  budgetWarningEmitted: boolean;
60
+ thinkingBlockHashes: Map<string, ThinkingBlockHash[]>;
61
+ /** 260428-hoy: Canonical (pre-mutation) snapshot of each assistant message's
62
+ * full content array, captured at stream close in lockstep with thinkingBlockHashes.
63
+ * Keyed by responseId; capped at 32 with FIFO eviction in lockstep with the
64
+ * hash store. Used by the pre-LLM-call restoration pass to heal cross-turn
65
+ * mutation of signed thinking blocks before pi-ai serializes the next request. */
66
+ thinkingBlockCanonical: Map<string, ReadonlyArray<unknown>>;
59
67
  }
60
68
  /**
61
69
  * Create a fresh metrics state with all counters zeroed.
@@ -51,6 +51,8 @@ export function createBridgeMetrics() {
51
51
  sessionCumulativeCacheSavedUsd: 0,
52
52
  totalThinkingTokens: 0,
53
53
  budgetWarningEmitted: false,
54
+ thinkingBlockHashes: new Map(),
55
+ thinkingBlockCanonical: new Map(),
54
56
  };
55
57
  }
56
58
  /**
@@ -20,6 +20,7 @@ import type { ProviderHealthMonitor } from "../safety/provider-health-monitor.js
20
20
  import type { ContextWindowGuard, ContextUsageData } from "../safety/context-window-guard.js";
21
21
  import type { ExecutionResult } from "../executor/types.js";
22
22
  import type { ExecutionPlan } from "../planner/types.js";
23
+ import { type ThinkingBlockHash } from "./thinking-block-hash-invariant.js";
23
24
  /** Per-call TTL split estimate, populated by requestBodyInjector's onPayload.
24
25
  * Shared mutable object — written by the stream wrapper, read by the bridge. */
25
26
  export interface TtlSplitEstimate {
@@ -126,6 +127,24 @@ export interface PiEventBridgeDeps {
126
127
  * on each API call, read by the bridge on turn_end for per-TTL cost calculation.
127
128
  * The bridge normalizes these estimates against the actual SDK-reported cacheWriteTokens. */
128
129
  ttlSplit?: TtlSplitEstimate;
130
+ /** 260428-hoy pre-call hook: invoked once per `turn_start` event, BEFORE
131
+ * pi-ai serializes the next request. The closure (defined in pi-executor)
132
+ * walks `session.agent.state.messages`, asserts the cross-turn
133
+ * hash-invariant per assistant message with a stored hash entry (logs
134
+ * ERROR on mutation), then runs the canonical-restore helper against the
135
+ * canonical store (heals any mutation in-place by writing the result
136
+ * back to `session.agent.state.messages`). The return value is unused by
137
+ * the bridge -- the side effect is the heal write-back. Optional: when
138
+ * omitted, both the diagnostic and the heal are silently disabled
139
+ * (e.g., unit tests that don't drive a full agent session). */
140
+ getSessionMessages?: () => ReadonlyArray<unknown> | undefined;
141
+ /** 260428-iag wire-edge diagnostic: returns the absolute path to the
142
+ * per-session JSONL on disk. The bridge invokes this only when the LLM
143
+ * error path detects the signed-replay rejection signature, then
144
+ * diff'd against the persisted canonical to surface mutation that
145
+ * occurred AFTER the bridge's restoration hook. Optional — when
146
+ * omitted, the wire-edge diagnostic is a silent no-op. */
147
+ getSessionJsonlPath?: () => string | null;
129
148
  }
130
149
  /** Estimated cost payload for a timed-out API request. */
131
150
  export interface GhostCostEstimate {
@@ -167,6 +186,16 @@ export interface PiEventBridgeResult {
167
186
  };
168
187
  /** Accumulate estimated cost from a timed-out API request. */
169
188
  addGhostCost: (estimated: GhostCostEstimate) => void;
189
+ /** 260428-hoy: ReadonlyMap views of the per-responseId hash store and
190
+ * canonical-snapshot store, both populated at stream-close in lockstep.
191
+ * The executor's pre-LLM-call closure reads both stores to drive the
192
+ * hash-invariant assertion plus the canonical restore helper. Returns
193
+ * ReadonlyMap views to preserve internal-state encapsulation -- the
194
+ * underlying `m` object is never exported. */
195
+ getThinkingBlockStores: () => {
196
+ hashes: ReadonlyMap<string, ReadonlyArray<ThinkingBlockHash>>;
197
+ canonical: ReadonlyMap<string, ReadonlyArray<unknown>>;
198
+ };
170
199
  }
171
200
  export { sanitizeToolArgs, extractErrorText } from "./bridge-event-handlers.js";
172
201
  /**
@@ -20,6 +20,7 @@ import { extractPlanFromResponse } from "../planner/plan-extractor.js";
20
20
  import { extractMcpServerName, classifyMcpErrorType, sanitizeToolArgs, extractErrorText } from "./bridge-event-handlers.js";
21
21
  import { createBridgeMetrics, buildBridgeResult } from "./bridge-metrics.js";
22
22
  import { checkStepLimit, emitStepLimitAbort, checkBudgetLimit, emitBudgetAbort, checkBudgetTrajectory, checkContextWindow, emitContextAbort, checkCircuitBreaker, emitCircuitBreakerAbort } from "./bridge-safety-controls.js";
23
+ import { computeThinkingBlockHashes, diffThinkingBlocksAgainstPersisted, WIRE_DIFF_HINT_FILE_MISSING, WIRE_DIFF_HINT_NOT_FOUND, } from "./thinking-block-hash-invariant.js";
23
24
  // Re-export helper functions for backward compatibility with existing imports
24
25
  export { sanitizeToolArgs, extractErrorText } from "./bridge-event-handlers.js";
25
26
  // ---------------------------------------------------------------------------
@@ -217,6 +218,88 @@ export function createPiEventBridge(deps) {
217
218
  break;
218
219
  }
219
220
  // -----------------------------------------------------------------
221
+ // LLM turn about to start (pre-serialize hook for assert+restore)
222
+ // -----------------------------------------------------------------
223
+ case "turn_start": {
224
+ // 260428-hoy: Run the executor-supplied pre-call closure once per
225
+ // turn, before pi-ai reads `session.agent.state.messages` to
226
+ // serialize the next API request. The closure performs the
227
+ // assert-then-restore pass over the live transcript and writes the
228
+ // healed array back into session state when at least one swap
229
+ // happens, so the bytes Anthropic sees match the canonical
230
+ // stream-close snapshot. The closure swallows its own throws; the
231
+ // wrapper here is belt-and-braces.
232
+ //
233
+ // 260428-j0v: ALWAYS emit ONE INFO log carrying the counters the
234
+ // bridge can derive — even when the closure is unwired or returns
235
+ // undefined / no candidates. This closes the silent-success
236
+ // ambiguity observed on trace c5680133 where ZERO agent.bridge.*
237
+ // events appeared despite the helpers having shipped.
238
+ //
239
+ // Counters are computed by the bridge's own walk of the messages
240
+ // returned by the closure (or empty when unwired) so the executor
241
+ // closure stays untouched. `mismatchesLogged` and `restoredCount`
242
+ // are derived from positional hash diffs — they equal the work the
243
+ // closure's helpers actually emit/heal.
244
+ const hashStoreSize = m.thinkingBlockHashes.size;
245
+ const canonicalStoreSize = m.thinkingBlockCanonical.size;
246
+ let candidatesChecked = 0;
247
+ let mismatchesLogged = 0;
248
+ let anyResponseIdMatched = false;
249
+ if (deps.getSessionMessages) {
250
+ let liveBeforeClosure;
251
+ try {
252
+ liveBeforeClosure = deps.getSessionMessages();
253
+ }
254
+ catch {
255
+ // Pre-call hook must NEVER abort agent flow.
256
+ liveBeforeClosure = undefined;
257
+ }
258
+ if (Array.isArray(liveBeforeClosure)) {
259
+ for (const msg of liveBeforeClosure) {
260
+ if (!msg || typeof msg !== "object")
261
+ continue;
262
+ const sm = msg;
263
+ if (sm.role !== "assistant")
264
+ continue;
265
+ if (typeof sm.responseId !== "string")
266
+ continue;
267
+ const prior = m.thinkingBlockHashes.get(sm.responseId);
268
+ if (!prior)
269
+ continue;
270
+ candidatesChecked++;
271
+ anyResponseIdMatched = true;
272
+ const currentBlocks = Array.isArray(sm.content)
273
+ ? sm.content
274
+ : [];
275
+ const currentHashes = computeThinkingBlockHashes(currentBlocks);
276
+ const byIndex = new Map();
277
+ for (const h of currentHashes)
278
+ byIndex.set(h.blockIndex, h);
279
+ for (const old of prior) {
280
+ const now = byIndex.get(old.blockIndex);
281
+ if (!now || now.hash !== old.hash)
282
+ mismatchesLogged++;
283
+ }
284
+ }
285
+ }
286
+ }
287
+ // restoredCount equals mismatchesLogged in the current symmetric
288
+ // implementation; surfaced as a separate field so future asymmetric
289
+ // assert/restore semantics are observable.
290
+ const restoredCount = mismatchesLogged;
291
+ deps.logger.info({
292
+ module: "agent.bridge.hash-invariant",
293
+ candidatesChecked,
294
+ mismatchesLogged,
295
+ restoredCount,
296
+ anyResponseIdMatched,
297
+ hashStoreSize,
298
+ canonicalStoreSize,
299
+ }, "Pre-call assertion ran");
300
+ break;
301
+ }
302
+ // -----------------------------------------------------------------
220
303
  // LLM turn completed
221
304
  // -----------------------------------------------------------------
222
305
  case "turn_end": {
@@ -253,6 +336,41 @@ export function createPiEventBridge(deps) {
253
336
  thinkingLen: typeof b.thinking === "string" ? b.thinking.length : 0,
254
337
  })),
255
338
  }, "Assistant message block accounting at stream close");
339
+ // Bug A diagnostic + 260428-hoy heal: capture hashes AND a
340
+ // canonical (pre-mutation) snapshot of the full content array,
341
+ // keyed by responseId, in lockstep across both stores. The
342
+ // hash store powers the assertion ERROR log (mutation
343
+ // diagnostic); the canonical store powers the pre-call
344
+ // restore pass that heals cross-turn mutation before the next
345
+ // API serialize. Both stores are FIFO-evicted at 32 entries
346
+ // in lockstep so they always share the same keyset.
347
+ if (typeof responseIdForLog === "string") {
348
+ const hashes = computeThinkingBlockHashes(blocks);
349
+ if (hashes.length > 0) {
350
+ while (m.thinkingBlockHashes.size >= 32) {
351
+ const oldestKey = m.thinkingBlockHashes.keys().next().value;
352
+ if (oldestKey === undefined)
353
+ break;
354
+ m.thinkingBlockHashes.delete(oldestKey);
355
+ m.thinkingBlockCanonical.delete(oldestKey);
356
+ }
357
+ m.thinkingBlockHashes.set(responseIdForLog, hashes);
358
+ // 260428-hoy: capture canonical (pre-mutation) full
359
+ // content array so the pre-LLM-call restore pass can heal
360
+ // any cross-turn mutation before pi-ai serializes the
361
+ // next request. structuredClone is a Node 22 global; the
362
+ // try/catch is defensive against rare exotic input shapes.
363
+ try {
364
+ const canonical = Object.freeze(structuredClone(blocks));
365
+ m.thinkingBlockCanonical.set(responseIdForLog, canonical);
366
+ }
367
+ catch {
368
+ // Canonical capture failure is non-fatal: the hash store
369
+ // still fires the assertion diagnostic on resend; only
370
+ // the heal step degrades to no-op for this responseId.
371
+ }
372
+ }
373
+ }
256
374
  }
257
375
  }
258
376
  // Compute LLM latency: turn wallclock minus tool execution time
@@ -580,7 +698,6 @@ export function createPiEventBridge(deps) {
580
698
  request: (deps.sepMessageText ?? "").slice(0, 200),
581
699
  steps,
582
700
  completedCount: 0,
583
- nudged: false,
584
701
  createdAtMs: Date.now(),
585
702
  };
586
703
  deps.executionPlan.current = plan;
@@ -741,6 +858,122 @@ export function createPiEventBridge(deps) {
741
858
  hint: "Check LLM provider status",
742
859
  errorKind: "dependency",
743
860
  }, "LLM call returned error");
861
+ // 260428-iag wire-edge diagnostic: when the LLM error matches the
862
+ // Anthropic signed-replay rejection signature ("thinking blocks ...
863
+ // cannot be modified"), diff the in-memory content against the
864
+ // persisted JSONL canonical and emit one ERROR per divergent block.
865
+ // Fully async / fire-and-forget — never blocks the existing error
866
+ // path. Silent no-op when the signature doesn't match or when
867
+ // either getSessionMessages / getSessionJsonlPath is unwired.
868
+ //
869
+ // 260428-j0v: ALWAYS emit ONE dispatch-decision INFO log carrying
870
+ // boolean flags that explain WHY the wire-diff dispatch was or was
871
+ // not entered (regex match, candidate count, callback presence) —
872
+ // even when regexMatched is false or callbacks are unwired. When
873
+ // the dispatch IS entered, emit a second dispatch-completion INFO
874
+ // after the async candidates loop completes.
875
+ //
876
+ // The signature regex matches Anthropic's actual 400 message:
877
+ // "messages.N.content.M: thinking blocks cannot be modified"
878
+ // and the redacted_thinking variant. Both `thinking|redacted_thinking`
879
+ // AND `modif|cannot` must be present to avoid false positives on
880
+ // unrelated 400s (rate limits, auth, schema errors).
881
+ {
882
+ const errMsg = m.lastLlmErrorMessage;
883
+ const regexMatched = typeof errMsg === "string" &&
884
+ /thinking|redacted_thinking/.test(errMsg) &&
885
+ /modif|cannot/.test(errMsg);
886
+ const liveForDecision = deps.getSessionMessages?.();
887
+ const jsonlPathForDecision = deps.getSessionJsonlPath?.();
888
+ const candidates = [];
889
+ if (Array.isArray(liveForDecision)) {
890
+ for (let i = liveForDecision.length - 1; i >= 0 && candidates.length < 3; i--) {
891
+ // eslint-disable-next-line security/detect-object-injection -- numeric loop index
892
+ const msg = liveForDecision[i];
893
+ if (!msg || typeof msg !== "object")
894
+ continue;
895
+ if (msg.role !== "assistant")
896
+ continue;
897
+ if (typeof msg.responseId !== "string")
898
+ continue;
899
+ if (!Array.isArray(msg.content))
900
+ continue;
901
+ const blocks = msg.content;
902
+ const hasSigned = blocks.some((b) => b.type === "thinking" &&
903
+ typeof b.thinkingSignature === "string" &&
904
+ b.thinkingSignature.length > 0 &&
905
+ b.redacted !== true);
906
+ if (!hasSigned)
907
+ continue;
908
+ candidates.push({ responseId: msg.responseId, content: blocks });
909
+ }
910
+ }
911
+ const jsonlPathPresent = typeof jsonlPathForDecision === "string" && jsonlPathForDecision.length > 0;
912
+ deps.logger.info({
913
+ module: "agent.bridge.wire-diff",
914
+ regexMatched,
915
+ candidatesFound: candidates.length,
916
+ jsonlPathPresent,
917
+ getSessionMessagesPresent: typeof deps.getSessionMessages === "function",
918
+ getSessionJsonlPathPresent: typeof deps.getSessionJsonlPath === "function",
919
+ }, "Wire-edge diff dispatch decision");
920
+ if (regexMatched && jsonlPathPresent && candidates.length > 0) {
921
+ const capturedJsonlPath = jsonlPathForDecision;
922
+ // Async non-blocking dispatch -- never blocks the error path.
923
+ void Promise.resolve().then(async () => {
924
+ let candidatesProcessed = 0;
925
+ let totalDivergences = 0;
926
+ let persistedNotFound = 0;
927
+ let fileReadErrors = 0;
928
+ // Wrapped logger forwards to deps.logger AND counts the
929
+ // helper's WARN outcomes by hint-constant identity (no regex).
930
+ const countingLogger = {
931
+ warn: (obj, msg) => {
932
+ deps.logger.warn(obj, msg);
933
+ if (obj.hint === WIRE_DIFF_HINT_FILE_MISSING)
934
+ fileReadErrors++;
935
+ else if (obj.hint === WIRE_DIFF_HINT_NOT_FOUND)
936
+ persistedNotFound++;
937
+ },
938
+ };
939
+ try {
940
+ for (const c of candidates) {
941
+ candidatesProcessed++;
942
+ const entries = await diffThinkingBlocksAgainstPersisted(c.content, c.responseId, capturedJsonlPath, { logger: countingLogger });
943
+ totalDivergences += entries.length;
944
+ for (const entry of entries) {
945
+ deps.logger.error({
946
+ module: "agent.bridge.wire-diff",
947
+ responseId: c.responseId,
948
+ blockIndex: entry.blockIndex,
949
+ persistedHash: entry.persistedHash,
950
+ inMemoryHash: entry.inMemoryHash,
951
+ persistedText: entry.persistedText,
952
+ inMemoryText: entry.inMemoryText,
953
+ persistedSigLen: entry.persistedSigLen,
954
+ inMemorySigLen: entry.inMemorySigLen,
955
+ errorKind: "internal",
956
+ hint: "Mutation occurred between bridge restoration hook and " +
957
+ "pi-ai serialization — likely inside pi-ai or its dependencies",
958
+ }, "Wire-edge thinking-block divergence vs persisted JSONL");
959
+ }
960
+ }
961
+ }
962
+ catch {
963
+ // Diagnostic must NEVER abort the error path.
964
+ }
965
+ // ALWAYS emit the completion INFO, even on totalDivergences=0
966
+ // or when every helper call hit a read error.
967
+ deps.logger.info({
968
+ module: "agent.bridge.wire-diff",
969
+ candidatesProcessed,
970
+ totalDivergences,
971
+ persistedNotFound,
972
+ fileReadErrors,
973
+ }, "Wire-edge diff dispatch complete");
974
+ });
975
+ }
976
+ }
744
977
  deps.circuitBreaker.recordFailure();
745
978
  deps.providerHealth?.recordFailure(deps.provider, deps.agentId);
746
979
  // If circuit breaker just opened, abort mid-execution
@@ -772,5 +1005,12 @@ export function createPiEventBridge(deps) {
772
1005
  m.ghostCostUsd += estimated.costUsd;
773
1006
  m.timedOutRequests += 1;
774
1007
  };
775
- return { listener, getResult, addGhostCost };
1008
+ // 260428-hoy: typed ReadonlyMap accessor for the executor's pre-call
1009
+ // closure. Returns views over the live maps -- the executor never receives
1010
+ // the mutable `m` object itself.
1011
+ const getThinkingBlockStores = () => ({
1012
+ hashes: m.thinkingBlockHashes,
1013
+ canonical: m.thinkingBlockCanonical,
1014
+ });
1015
+ return { listener, getResult, addGhostCost, getThinkingBlockStores };
776
1016
  }