comisai 1.0.36 → 1.0.37

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 (239) hide show
  1. package/node_modules/@comis/agent/dist/background/auto-background-middleware.js +9 -0
  2. package/node_modules/@comis/agent/dist/background/background-task-manager.d.ts +22 -2
  3. package/node_modules/@comis/agent/dist/background/background-task-manager.js +48 -41
  4. package/node_modules/@comis/agent/dist/background/background-task-persistence.js +28 -5
  5. package/node_modules/@comis/agent/dist/background/background-task-types.d.ts +49 -0
  6. package/node_modules/@comis/agent/dist/background/completion-dispatcher.d.ts +130 -0
  7. package/node_modules/@comis/agent/dist/background/completion-dispatcher.js +215 -0
  8. package/node_modules/@comis/agent/dist/background/completion-runner.d.ts +10 -1
  9. package/node_modules/@comis/agent/dist/background/completion-runner.js +98 -15
  10. package/node_modules/@comis/agent/dist/background/index.d.ts +6 -1
  11. package/node_modules/@comis/agent/dist/background/index.js +2 -0
  12. package/node_modules/@comis/agent/dist/background/session-resolver.d.ts +85 -0
  13. package/node_modules/@comis/agent/dist/background/session-resolver.js +78 -0
  14. package/node_modules/@comis/agent/dist/bootstrap/sections/messaging-sections.js +1 -0
  15. package/node_modules/@comis/agent/dist/bootstrap/sections/tool-descriptions.js +3 -3
  16. package/node_modules/@comis/agent/dist/bootstrap/sections/tooling-sections.d.ts +30 -2
  17. package/node_modules/@comis/agent/dist/bootstrap/sections/tooling-sections.js +51 -2
  18. package/node_modules/@comis/agent/dist/bootstrap/system-prompt-assembler.d.ts +22 -0
  19. package/node_modules/@comis/agent/dist/bootstrap/system-prompt-assembler.js +2 -2
  20. package/node_modules/@comis/agent/dist/bridge/bridge-event-handlers.d.ts +1 -5
  21. package/node_modules/@comis/agent/dist/bridge/bridge-event-handlers.js +2 -14
  22. package/node_modules/@comis/agent/dist/bridge/bridge-metrics.d.ts +26 -0
  23. package/node_modules/@comis/agent/dist/bridge/bridge-metrics.js +3 -0
  24. package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.d.ts +9 -0
  25. package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.js +73 -2
  26. package/node_modules/@comis/agent/dist/context-engine/signature-surrogate-guard.d.ts +10 -10
  27. package/node_modules/@comis/agent/dist/context-engine/signature-surrogate-guard.js +14 -14
  28. package/node_modules/@comis/agent/dist/context-engine/thinking-block-cleaner.d.ts +11 -13
  29. package/node_modules/@comis/agent/dist/context-engine/thinking-block-cleaner.js +14 -15
  30. package/node_modules/@comis/agent/dist/executor/capability-index-context.d.ts +72 -0
  31. package/node_modules/@comis/agent/dist/executor/capability-index-context.js +329 -0
  32. package/node_modules/@comis/agent/dist/executor/drain-helper.d.ts +122 -0
  33. package/node_modules/@comis/agent/dist/executor/drain-helper.js +173 -0
  34. package/node_modules/@comis/agent/dist/executor/error-classifier.js +2 -2
  35. package/node_modules/@comis/agent/dist/executor/executor-post-execution.d.ts +48 -4
  36. package/node_modules/@comis/agent/dist/executor/executor-post-execution.js +134 -31
  37. package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.d.ts +7 -0
  38. package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.js +25 -4
  39. package/node_modules/@comis/agent/dist/executor/executor-tool-assembly.d.ts +18 -1
  40. package/node_modules/@comis/agent/dist/executor/executor-tool-assembly.js +19 -16
  41. package/node_modules/@comis/agent/dist/executor/jit-guide-injector.d.ts +11 -2
  42. package/node_modules/@comis/agent/dist/executor/jit-guide-injector.js +16 -2
  43. package/node_modules/@comis/agent/dist/executor/pi-executor.d.ts +8 -2
  44. package/node_modules/@comis/agent/dist/executor/pi-executor.js +25 -12
  45. package/node_modules/@comis/agent/dist/executor/prompt-assembly.d.ts +9 -1
  46. package/node_modules/@comis/agent/dist/executor/prompt-assembly.js +15 -1
  47. package/node_modules/@comis/agent/dist/executor/tool-deferral.d.ts +18 -27
  48. package/node_modules/@comis/agent/dist/executor/tool-deferral.js +29 -38
  49. package/node_modules/@comis/agent/dist/model/model-registry-adapter.js +1 -1
  50. package/node_modules/@comis/agent/dist/model/model-scanner.js +1 -1
  51. package/node_modules/@comis/agent/dist/safety/tool-retry-breaker.d.ts +11 -1
  52. package/node_modules/@comis/agent/dist/safety/tool-retry-breaker.js +19 -22
  53. package/node_modules/@comis/agent/dist/session/comis-session-manager.d.ts +16 -2
  54. package/node_modules/@comis/agent/dist/spawn/pi-mono-adapters.d.ts +1 -1
  55. package/node_modules/@comis/agent/dist/spawn/pi-mono-adapters.js +5 -5
  56. package/node_modules/@comis/agent/dist/workspace/data-env.d.ts +38 -0
  57. package/node_modules/@comis/agent/dist/workspace/data-env.js +56 -0
  58. package/node_modules/@comis/agent/dist/workspace/index.d.ts +1 -0
  59. package/node_modules/@comis/agent/dist/workspace/index.js +1 -0
  60. package/node_modules/@comis/agent/dist/workspace/templates.js +5 -1
  61. package/node_modules/@comis/agent/package.json +1 -1
  62. package/node_modules/@comis/channels/dist/index.d.ts +1 -1
  63. package/node_modules/@comis/channels/dist/index.js +1 -1
  64. package/node_modules/@comis/channels/dist/shared/channel-manager.d.ts +9 -3
  65. package/node_modules/@comis/channels/dist/shared/inbound-gate.d.ts +1 -1
  66. package/node_modules/@comis/channels/dist/shared/inbound-gate.js +22 -7
  67. package/node_modules/@comis/channels/dist/shared/inbound-pipeline.d.ts +10 -3
  68. package/node_modules/@comis/channels/dist/shared/inbound-route.d.ts +1 -1
  69. package/node_modules/@comis/channels/dist/shared/inbound-route.js +13 -2
  70. package/node_modules/@comis/channels/dist/shared/response-filter.d.ts +11 -24
  71. package/node_modules/@comis/channels/dist/shared/response-filter.js +25 -53
  72. package/node_modules/@comis/channels/package.json +1 -1
  73. package/node_modules/@comis/cli/dist/commands/providers.d.ts +1 -2
  74. package/node_modules/@comis/cli/dist/commands/providers.js +5 -6
  75. package/node_modules/@comis/cli/package.json +1 -1
  76. package/node_modules/@comis/core/dist/config/field-metadata.js +2 -0
  77. package/node_modules/@comis/core/dist/config/immutable-keys.js +4 -1
  78. package/node_modules/@comis/core/dist/config/index.d.ts +4 -0
  79. package/node_modules/@comis/core/dist/config/index.js +2 -0
  80. package/node_modules/@comis/core/dist/config/schema-agent.d.ts +0 -792
  81. package/node_modules/@comis/core/dist/config/schema-approvals.d.ts +0 -14
  82. package/node_modules/@comis/core/dist/config/schema-auto-reply-engine.d.ts +0 -6
  83. package/node_modules/@comis/core/dist/config/schema-background-tasks.d.ts +0 -12
  84. package/node_modules/@comis/core/dist/config/schema-browser.d.ts +0 -18
  85. package/node_modules/@comis/core/dist/config/schema-channel.d.ts +0 -158
  86. package/node_modules/@comis/core/dist/config/schema-coalescer.d.ts +0 -5
  87. package/node_modules/@comis/core/dist/config/schema-daemon.d.ts +0 -32
  88. package/node_modules/@comis/core/dist/config/schema-delivery.d.ts +0 -18
  89. package/node_modules/@comis/core/dist/config/schema-documentation.d.ts +0 -12
  90. package/node_modules/@comis/core/dist/config/schema-embedding.d.ts +0 -20
  91. package/node_modules/@comis/core/dist/config/schema-envelope.d.ts +0 -15
  92. package/node_modules/@comis/core/dist/config/schema-gateway.d.ts +0 -37
  93. package/node_modules/@comis/core/dist/config/schema-gemini-cache.d.ts +0 -2
  94. package/node_modules/@comis/core/dist/config/schema-integrations.d.ts +0 -318
  95. package/node_modules/@comis/core/dist/config/schema-lifecycle-reactions.d.ts +0 -18
  96. package/node_modules/@comis/core/dist/config/schema-memory-review.d.ts +0 -7
  97. package/node_modules/@comis/core/dist/config/schema-memory.d.ts +0 -16
  98. package/node_modules/@comis/core/dist/config/schema-messages.d.ts +0 -8
  99. package/node_modules/@comis/core/dist/config/schema-models.d.ts +0 -15
  100. package/node_modules/@comis/core/dist/config/schema-notification.d.ts +0 -5
  101. package/node_modules/@comis/core/dist/config/schema-oauth.d.ts +0 -5
  102. package/node_modules/@comis/core/dist/config/schema-observability.d.ts +0 -38
  103. package/node_modules/@comis/core/dist/config/schema-output-retention.d.ts +34 -0
  104. package/node_modules/@comis/core/dist/config/schema-output-retention.js +48 -0
  105. package/node_modules/@comis/core/dist/config/schema-plugins.d.ts +0 -8
  106. package/node_modules/@comis/core/dist/config/schema-providers.d.ts +0 -64
  107. package/node_modules/@comis/core/dist/config/schema-queue.d.ts +0 -58
  108. package/node_modules/@comis/core/dist/config/schema-response-prefix.d.ts +0 -2
  109. package/node_modules/@comis/core/dist/config/schema-retry.d.ts +0 -6
  110. package/node_modules/@comis/core/dist/config/schema-scheduler.d.ts +0 -39
  111. package/node_modules/@comis/core/dist/config/schema-secrets.d.ts +0 -3
  112. package/node_modules/@comis/core/dist/config/schema-security.d.ts +0 -18
  113. package/node_modules/@comis/core/dist/config/schema-send-policy.d.ts +0 -13
  114. package/node_modules/@comis/core/dist/config/schema-sender-trust-display.d.ts +0 -5
  115. package/node_modules/@comis/core/dist/config/schema-serializer.js +2 -0
  116. package/node_modules/@comis/core/dist/config/schema-skills.d.ts +0 -61
  117. package/node_modules/@comis/core/dist/config/schema-streaming.d.ts +0 -38
  118. package/node_modules/@comis/core/dist/config/schema-telegram-file-guard.d.ts +0 -3
  119. package/node_modules/@comis/core/dist/config/schema-tooling.d.ts +87 -0
  120. package/node_modules/@comis/core/dist/config/schema-tooling.js +152 -0
  121. package/node_modules/@comis/core/dist/config/schema-verbosity.d.ts +0 -12
  122. package/node_modules/@comis/core/dist/config/schema-webhooks.d.ts +0 -40
  123. package/node_modules/@comis/core/dist/config/schema.d.ts +41 -38
  124. package/node_modules/@comis/core/dist/config/schema.js +6 -0
  125. package/node_modules/@comis/core/dist/context/context.d.ts +0 -4
  126. package/node_modules/@comis/core/dist/domain/approval-request.d.ts +0 -17
  127. package/node_modules/@comis/core/dist/domain/background-task-origin.d.ts +0 -10
  128. package/node_modules/@comis/core/dist/domain/delivery-origin.d.ts +0 -5
  129. package/node_modules/@comis/core/dist/domain/execution-graph.d.ts +0 -48
  130. package/node_modules/@comis/core/dist/domain/memory-entry.d.ts +0 -3
  131. package/node_modules/@comis/core/dist/domain/model-compat.d.ts +0 -4
  132. package/node_modules/@comis/core/dist/domain/normalized-message.d.ts +0 -15
  133. package/node_modules/@comis/core/dist/domain/provider-capabilities.d.ts +0 -6
  134. package/node_modules/@comis/core/dist/domain/rich-message.d.ts +0 -14
  135. package/node_modules/@comis/core/dist/domain/subagent-context-config.d.ts +0 -22
  136. package/node_modules/@comis/core/dist/domain/subagent-context-types.d.ts +0 -8
  137. package/node_modules/@comis/core/dist/event-bus/events-agent.d.ts +31 -0
  138. package/node_modules/@comis/core/dist/event-bus/events-infra.d.ts +5 -0
  139. package/node_modules/@comis/core/dist/exports/config.d.ts +2 -2
  140. package/node_modules/@comis/core/dist/exports/config.js +3 -1
  141. package/node_modules/@comis/core/dist/exports/hooks.d.ts +1 -1
  142. package/node_modules/@comis/core/dist/exports/ports.d.ts +2 -2
  143. package/node_modules/@comis/core/dist/exports/ports.js +1 -1
  144. package/node_modules/@comis/core/dist/ports/channel-plugin.d.ts +0 -13
  145. package/node_modules/@comis/core/dist/ports/index.d.ts +2 -0
  146. package/node_modules/@comis/core/dist/ports/index.js +4 -0
  147. package/node_modules/@comis/core/dist/ports/no-op-tool-capability.d.ts +30 -0
  148. package/node_modules/@comis/core/dist/ports/no-op-tool-capability.js +47 -0
  149. package/node_modules/@comis/core/dist/ports/tool-capability.d.ts +165 -0
  150. package/node_modules/@comis/core/dist/ports/tool-capability.js +15 -0
  151. package/node_modules/@comis/core/dist/security/audit.d.ts +0 -11
  152. package/node_modules/@comis/core/dist/tool-metadata.d.ts +21 -1
  153. package/node_modules/@comis/core/dist/tool-metadata.js +1 -1
  154. package/node_modules/@comis/core/package.json +1 -1
  155. package/node_modules/@comis/daemon/bundled-skills/skill-creator/scripts/validate-skill.py +1 -1
  156. package/node_modules/@comis/daemon/dist/daemon.js +89 -14
  157. package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.d.ts +1 -1
  158. package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.js +1 -1
  159. package/node_modules/@comis/daemon/dist/rpc/builtin-provider-guard.js +2 -2
  160. package/node_modules/@comis/daemon/dist/rpc/credential-resolver.js +1 -1
  161. package/node_modules/@comis/daemon/dist/rpc/model-handlers.d.ts +1 -1
  162. package/node_modules/@comis/daemon/dist/rpc/model-handlers.js +2 -2
  163. package/node_modules/@comis/daemon/dist/sub-agent-runner.d.ts +18 -0
  164. package/node_modules/@comis/daemon/dist/sub-agent-runner.js +41 -9
  165. package/node_modules/@comis/daemon/dist/wiring/index.d.ts +2 -0
  166. package/node_modules/@comis/daemon/dist/wiring/index.js +1 -0
  167. package/node_modules/@comis/daemon/dist/wiring/setup-agents.d.ts +36 -2
  168. package/node_modules/@comis/daemon/dist/wiring/setup-agents.js +45 -8
  169. package/node_modules/@comis/daemon/dist/wiring/setup-background-completion-runner.d.ts +28 -9
  170. package/node_modules/@comis/daemon/dist/wiring/setup-background-completion-runner.js +36 -9
  171. package/node_modules/@comis/daemon/dist/wiring/setup-background-tasks.js +2 -2
  172. package/node_modules/@comis/daemon/dist/wiring/setup-channels.d.ts +9 -2
  173. package/node_modules/@comis/daemon/dist/wiring/setup-channels.js +15 -9
  174. package/node_modules/@comis/daemon/dist/wiring/setup-cross-session.d.ts +20 -5
  175. package/node_modules/@comis/daemon/dist/wiring/setup-cross-session.js +20 -15
  176. package/node_modules/@comis/daemon/dist/wiring/setup-delivery.js +14 -2
  177. package/node_modules/@comis/daemon/dist/wiring/setup-gateway.d.ts +4 -6
  178. package/node_modules/@comis/daemon/dist/wiring/setup-gateway.js +3 -5
  179. package/node_modules/@comis/daemon/dist/wiring/setup-heartbeat.d.ts +20 -5
  180. package/node_modules/@comis/daemon/dist/wiring/setup-heartbeat.js +11 -2
  181. package/node_modules/@comis/daemon/dist/wiring/setup-output-retention.d.ts +89 -0
  182. package/node_modules/@comis/daemon/dist/wiring/setup-output-retention.js +212 -0
  183. package/node_modules/@comis/daemon/dist/wiring/setup-tools.d.ts +18 -4
  184. package/node_modules/@comis/daemon/dist/wiring/setup-tools.js +29 -10
  185. package/node_modules/@comis/daemon/dist/wiring/tool-capability-adapter.d.ts +75 -0
  186. package/node_modules/@comis/daemon/dist/wiring/tool-capability-adapter.js +253 -0
  187. package/node_modules/@comis/daemon/package.json +1 -1
  188. package/node_modules/@comis/gateway/dist/webhook/webhook-endpoint.d.ts +0 -4
  189. package/node_modules/@comis/gateway/package.json +1 -1
  190. package/node_modules/@comis/infra/package.json +1 -1
  191. package/node_modules/@comis/memory/package.json +1 -1
  192. package/node_modules/@comis/scheduler/dist/cron/cron-types.d.ts +0 -42
  193. package/node_modules/@comis/scheduler/dist/heartbeat/agent-heartbeat-source.d.ts +29 -8
  194. package/node_modules/@comis/scheduler/dist/heartbeat/agent-heartbeat-source.js +19 -7
  195. package/node_modules/@comis/scheduler/dist/system-events/system-event-types.d.ts +0 -3
  196. package/node_modules/@comis/scheduler/dist/tasks/task-types.d.ts +0 -17
  197. package/node_modules/@comis/scheduler/package.json +1 -1
  198. package/node_modules/@comis/shared/dist/index.d.ts +3 -0
  199. package/node_modules/@comis/shared/dist/index.js +4 -0
  200. package/node_modules/@comis/shared/dist/mcp-tool-name.d.ts +78 -0
  201. package/node_modules/@comis/shared/dist/mcp-tool-name.js +92 -0
  202. package/node_modules/@comis/shared/dist/silent-tokens.d.ts +38 -0
  203. package/node_modules/@comis/shared/dist/silent-tokens.js +51 -0
  204. package/node_modules/@comis/shared/dist/visible-delivery.d.ts +28 -0
  205. package/node_modules/@comis/shared/dist/visible-delivery.js +16 -0
  206. package/node_modules/@comis/shared/package.json +1 -1
  207. package/node_modules/@comis/skills/dist/bridge/mcp-tool-bridge.d.ts +2 -13
  208. package/node_modules/@comis/skills/dist/bridge/mcp-tool-bridge.js +3 -21
  209. package/node_modules/@comis/skills/dist/bridge/tool-metadata-enforcement.js +1 -1
  210. package/node_modules/@comis/skills/dist/bridge/tool-metadata-registry.js +4 -4
  211. package/node_modules/@comis/skills/dist/builtin/exec-tool.d.ts +55 -9
  212. package/node_modules/@comis/skills/dist/builtin/exec-tool.js +383 -19
  213. package/node_modules/@comis/skills/dist/builtin/install-detour.d.ts +67 -0
  214. package/node_modules/@comis/skills/dist/builtin/install-detour.js +342 -0
  215. package/node_modules/@comis/skills/dist/builtin/platform/admin-manage-factory.js +5 -5
  216. package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.d.ts +2 -2
  217. package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.js +2 -2
  218. package/node_modules/@comis/skills/dist/builtin/platform/message-tool.js +18 -0
  219. package/node_modules/@comis/skills/dist/builtin/platform/messaging-factory.d.ts +18 -1
  220. package/node_modules/@comis/skills/dist/builtin/platform/messaging-factory.js +18 -2
  221. package/node_modules/@comis/skills/dist/builtin/platform/models-manage-tool.js +3 -3
  222. package/node_modules/@comis/skills/dist/builtin/process-registry.d.ts +14 -0
  223. package/node_modules/@comis/skills/dist/builtin/process-tool.d.ts +24 -4
  224. package/node_modules/@comis/skills/dist/builtin/process-tool.js +25 -7
  225. package/node_modules/@comis/skills/dist/builtin/sandbox/bwrap-provider.d.ts +1 -1
  226. package/node_modules/@comis/skills/dist/builtin/sandbox/bwrap-provider.js +9 -0
  227. package/node_modules/@comis/skills/dist/index.d.ts +4 -1
  228. package/node_modules/@comis/skills/dist/index.js +3 -1
  229. package/node_modules/@comis/skills/dist/manifest/capability-parser.d.ts +44 -0
  230. package/node_modules/@comis/skills/dist/manifest/capability-parser.js +68 -0
  231. package/node_modules/@comis/skills/dist/manifest/schema.d.ts +44 -37
  232. package/node_modules/@comis/skills/dist/manifest/schema.js +35 -0
  233. package/node_modules/@comis/skills/dist/registry/discovery.d.ts +8 -0
  234. package/node_modules/@comis/skills/dist/registry/discovery.js +10 -3
  235. package/node_modules/@comis/skills/dist/registry/skill-registry.d.ts +45 -1
  236. package/node_modules/@comis/skills/dist/registry/skill-registry.js +70 -7
  237. package/node_modules/@comis/skills/package.json +1 -1
  238. package/node_modules/@comis/web/package.json +1 -1
  239. package/package.json +21 -21
@@ -0,0 +1,329 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Per-turn capability-index renderer.
4
+ *
5
+ * Renders the `## Capabilities` block for the dynamic preamble. Lives
6
+ * post-deferral in the executor lifecycle; consumes a `ToolCapabilityPort`
7
+ * for cluster/skill resolution and an `ExcludeDeferralResult` for active
8
+ * vs deferred tool partitioning.
9
+ *
10
+ * Pure-function builder: no logger, no IO, no `Result` envelope, no mutable
11
+ * module state beyond frozen module-level constants. Mirrors the shape of
12
+ * `buildDeferredToolsContext` in `tool-deferral.ts`.
13
+ *
14
+ * IMPORTANT -- cache fence:
15
+ * This module is consumed ONLY by `executor-tool-assembly.ts`. It MUST NOT
16
+ * be imported by `prompt-assembly.ts` -- the static prompt cache prefix MUST
17
+ * stay byte-identical when the skill registry reloads between turns.
18
+ * An architecture-grep enforces this invariant.
19
+ *
20
+ * @module
21
+ */
22
+ import { extractMcpServerName } from "@comis/shared";
23
+ import { TOOL_ORDER } from "../bootstrap/sections/tool-descriptions.js";
24
+ import { CHARS_PER_TOKEN_RATIO } from "../context-engine/constants.js";
25
+ // ---------------------------------------------------------------------------
26
+ // Module-level frozen sentinel + constants
27
+ // ---------------------------------------------------------------------------
28
+ /**
29
+ * Frozen empty result. Returned when the gate is off or when all three
30
+ * surface counts are zero. Identity-stable so callers can do cheap reference
31
+ * equality checks if useful.
32
+ */
33
+ const EMPTY = Object.freeze({
34
+ text: "",
35
+ capabilityIndexTokens: 0,
36
+ clusterCount: 0,
37
+ activeToolCount: 0,
38
+ deferredToolCount: 0,
39
+ promptSkillCount: 0,
40
+ });
41
+ /**
42
+ * Active-tool count threshold above which all per-cluster name lists are
43
+ * dropped (cluster headers + `(N tools)` counts remain).
44
+ * Constant in v1.1; revisit only if telemetry shows fleets clustering near it.
45
+ */
46
+ const ELISION_THRESHOLD = 32;
47
+ /**
48
+ * Maximum names rendered per server (active MCP) or per skill cluster
49
+ * before `+N more` truncation.
50
+ */
51
+ const PER_GROUP_NAME_CAP = 8;
52
+ // ---------------------------------------------------------------------------
53
+ // Reserved cluster IDs
54
+ //
55
+ // Inlined as string literals at the three fallback sites instead of named
56
+ // constants -- the IDs are part of the user-visible config schema in
57
+ // `packages/core/src/config/schema-tooling.ts`. Renaming them is intentionally
58
+ // not supported -- the cluster ID itself is fixed.
59
+ // ---------------------------------------------------------------------------
60
+ // ---------------------------------------------------------------------------
61
+ // Public renderer
62
+ // ---------------------------------------------------------------------------
63
+ /**
64
+ * Build the per-turn capability-index render result.
65
+ *
66
+ * Behavior:
67
+ * - Gate respect: returns {@link EMPTY} when `port.isCapabilityIndexEnabled()` is false.
68
+ * - Empty-input fast path: returns {@link EMPTY} when all three surface counts are zero.
69
+ * - Cluster bucketing: builtins -> `getBuiltinCluster()` (or `"other-tools"`),
70
+ * MCP -> `getMcpServerHint().cluster` (or `"external-integrations"`),
71
+ * skills -> `skill.cluster` (or `"prompt-skills"`).
72
+ * - Orphan-drop: deferred MCP tools whose server is not in the live
73
+ * `getConnectedMcpServers()` snapshot are dropped silently.
74
+ * - Sort: `(priority asc, clusterId asc)` for clusters; `TOOL_ORDER` for
75
+ * builtins (alphabetical fallback for unknowns); alphabetical for MCP
76
+ * servers and skills.
77
+ * - Per-group cap: 8 names + `+N more`.
78
+ * - >32 elision: drop ALL per-cluster name lists; keep headers + counts only.
79
+ * - Forbidden-literal discipline: the rendered text names neither the
80
+ * client-side discovery tool nor the server-side tool search regex tool.
81
+ * The deferred-tools preamble bullet uses the mechanism-neutral
82
+ * `"discovery mechanism available in your active toolspace"` wording.
83
+ * An architecture-grep enforces the file-level invariant.
84
+ *
85
+ * Restart-required note: `tooling.capabilityIndex.enabled` requires a daemon
86
+ * restart to take effect. The renderer respects the port's reported value at
87
+ * render time but does not enforce the restart constraint.
88
+ *
89
+ * @param deferralResult - Output of `applyToolDeferral` (active + deferred tool partition).
90
+ * @param port - The capability port (gate flag, cluster/skill resolution, live runtime view).
91
+ * @returns Frozen {@link CapabilityIndexRenderResult}; identity-stable {@link EMPTY} for the no-op path.
92
+ */
93
+ export function buildCapabilityIndexContext(deferralResult, port) {
94
+ // Gate (restart-required).
95
+ if (!port.isCapabilityIndexEnabled())
96
+ return EMPTY;
97
+ // Snapshot the live runtime view ONCE per render. Re-querying the port
98
+ // mid-render would risk inconsistent state if a server connect/disconnect
99
+ // happens between two reads.
100
+ const connectedServers = new Set(port.getConnectedMcpServers());
101
+ const visibleSkills = port.getPromptSkillCapabilities();
102
+ // Bucket every input source into a clusterId -> ClusterRender map.
103
+ const clusterMap = new Map();
104
+ // Active builtin / non-MCP tools.
105
+ for (const tool of deferralResult.activeTools) {
106
+ if (extractMcpServerName(tool.name) !== undefined)
107
+ continue;
108
+ const clusterId = port.getBuiltinCluster(tool.name) ?? "other-tools";
109
+ const cluster = ensureCluster(clusterMap, clusterId, port);
110
+ cluster.builtins.push(tool.name);
111
+ }
112
+ // Active MCP tools. Group by server within their cluster.
113
+ for (const tool of deferralResult.activeTools) {
114
+ const server = extractMcpServerName(tool.name);
115
+ if (server === undefined)
116
+ continue;
117
+ const clusterId = port.getMcpServerHint(server)?.cluster ?? "external-integrations";
118
+ const cluster = ensureCluster(clusterMap, clusterId, port);
119
+ const bucket = ensureServerBucket(cluster, server);
120
+ bucket.activeTools.push(tool.name);
121
+ }
122
+ // Deferred MCP tools (with orphan-drop). Non-MCP deferred entries are
123
+ // dropped entirely -- a header-only shell would be misleading because the
124
+ // renderer cannot teach what to do with a non-MCP deferred name.
125
+ let deferredToolCount = 0;
126
+ for (const entry of deferralResult.deferredEntries) {
127
+ const server = extractMcpServerName(entry.name);
128
+ if (server === undefined)
129
+ continue;
130
+ if (!connectedServers.has(server))
131
+ continue; // orphan-drop
132
+ const clusterId = port.getMcpServerHint(server)?.cluster ?? "external-integrations";
133
+ const cluster = ensureCluster(clusterMap, clusterId, port);
134
+ const bucket = ensureServerBucket(cluster, server);
135
+ bucket.deferredCount += 1;
136
+ deferredToolCount += 1;
137
+ }
138
+ // Visible prompt skills. The port has already merged
139
+ // operator > comis.capability > fallback; we only resolve cluster.
140
+ for (const skill of visibleSkills) {
141
+ const clusterId = skill.cluster ?? "prompt-skills";
142
+ const cluster = ensureCluster(clusterMap, clusterId, port);
143
+ cluster.skills.push(skill);
144
+ }
145
+ // Compute totals.
146
+ let activeToolCount = 0;
147
+ for (const cluster of clusterMap.values()) {
148
+ activeToolCount += cluster.builtins.length;
149
+ for (const bucket of cluster.mcpServers.values()) {
150
+ activeToolCount += bucket.activeTools.length;
151
+ }
152
+ }
153
+ // Empty-input fast path.
154
+ if (activeToolCount + deferredToolCount + visibleSkills.length === 0) {
155
+ return EMPTY;
156
+ }
157
+ // Sort clusters: (priority asc, clusterId asc).
158
+ const orderedClusters = [...clusterMap.values()].sort((a, b) => a.config.priority - b.config.priority || a.id.localeCompare(b.id));
159
+ // Determine elision: when total active exceeds 32, drop all per-cluster
160
+ // name lists (cluster headers + `(N tools)` counts remain).
161
+ const eliminateNameLists = activeToolCount > ELISION_THRESHOLD;
162
+ // Render the text envelope.
163
+ const lines = [];
164
+ lines.push("## Capabilities");
165
+ lines.push("");
166
+ lines.push("Map the task to one of these connected capabilities before using exec to install libraries.");
167
+ lines.push("");
168
+ lines.push("- Active tools: callable now.");
169
+ lines.push("- Deferred tools: connected, but load them through the discovery mechanism available in your active toolspace before invoking them.");
170
+ lines.push("- Prompt skills: available instructions/workflows; use the existing skill-loading mechanism when the task matches.");
171
+ for (const cluster of orderedClusters) {
172
+ lines.push("");
173
+ lines.push(`### ${cluster.config.label}`);
174
+ if (cluster.config.preferOverInstalls) {
175
+ lines.push("Prefer connected tools and available skills over installing equivalent libraries.");
176
+ }
177
+ if (eliminateNameLists) {
178
+ // Headers + count-only.
179
+ const tools = cluster.builtins.length + sumActiveServerTools(cluster);
180
+ lines.push(`(${tools} tools)`);
181
+ const deferredHere = sumDeferredServerTools(cluster);
182
+ if (deferredHere > 0) {
183
+ lines.push(`(${deferredHere} deferred tools)`);
184
+ }
185
+ if (cluster.skills.length > 0) {
186
+ lines.push(`(${cluster.skills.length} skills)`);
187
+ }
188
+ continue;
189
+ }
190
+ appendClusterBody(lines, cluster);
191
+ }
192
+ const text = lines.join("\n");
193
+ const clusterCount = orderedClusters.length;
194
+ return Object.freeze({
195
+ text,
196
+ capabilityIndexTokens: Math.ceil(text.length / CHARS_PER_TOKEN_RATIO),
197
+ clusterCount,
198
+ activeToolCount,
199
+ deferredToolCount,
200
+ promptSkillCount: visibleSkills.length,
201
+ });
202
+ }
203
+ // ---------------------------------------------------------------------------
204
+ // Internal helpers (file-scoped; not exported)
205
+ // ---------------------------------------------------------------------------
206
+ /**
207
+ * Lookup-or-create a {@link ClusterRender} bucket for a cluster ID. The
208
+ * cluster's {@link ClusterConfig} resolves through the port; missing config
209
+ * for a non-reserved ID falls back to a synthesized default labelled by the
210
+ * cluster ID itself. The wiring layer emits a WARN for missing configs; the
211
+ * renderer only renders.
212
+ */
213
+ function ensureCluster(map, clusterId, port) {
214
+ const existing = map.get(clusterId);
215
+ if (existing)
216
+ return existing;
217
+ const config = port.getClusterConfig(clusterId) ?? synthesizeClusterConfig(clusterId);
218
+ const cluster = {
219
+ id: clusterId,
220
+ config,
221
+ builtins: [],
222
+ mcpServers: new Map(),
223
+ skills: [],
224
+ };
225
+ map.set(clusterId, cluster);
226
+ return cluster;
227
+ }
228
+ /**
229
+ * Synthesize a {@link ClusterConfig} for a cluster ID the port does not
230
+ * recognize. This keeps the renderer total pure -- it never throws on
231
+ * misconfiguration. The wiring layer owns the WARN path; here we render
232
+ * with the cluster ID as both label and a sentinel `9999` priority (sorts
233
+ * last) and `preferOverInstalls: false`.
234
+ */
235
+ function synthesizeClusterConfig(clusterId) {
236
+ return Object.freeze({
237
+ label: clusterId,
238
+ priority: 9999,
239
+ preferOverInstalls: false,
240
+ });
241
+ }
242
+ function ensureServerBucket(cluster, server) {
243
+ const existing = cluster.mcpServers.get(server);
244
+ if (existing)
245
+ return existing;
246
+ const bucket = { activeTools: [], deferredCount: 0 };
247
+ cluster.mcpServers.set(server, bucket);
248
+ return bucket;
249
+ }
250
+ function sumActiveServerTools(cluster) {
251
+ let total = 0;
252
+ for (const bucket of cluster.mcpServers.values())
253
+ total += bucket.activeTools.length;
254
+ return total;
255
+ }
256
+ function sumDeferredServerTools(cluster) {
257
+ let total = 0;
258
+ for (const bucket of cluster.mcpServers.values())
259
+ total += bucket.deferredCount;
260
+ return total;
261
+ }
262
+ /**
263
+ * Sort builtin/non-MCP tool names within a cluster: known names follow
264
+ * {@link TOOL_ORDER}; unknown names fall through to alphabetical via
265
+ * `localeCompare`.
266
+ */
267
+ function sortBuiltinsInCluster(builtins) {
268
+ const orderIndex = (name) => {
269
+ const idx = TOOL_ORDER.indexOf(name);
270
+ return idx === -1 ? Number.MAX_SAFE_INTEGER : idx;
271
+ };
272
+ return builtins.slice().sort((a, b) => {
273
+ const diff = orderIndex(a) - orderIndex(b);
274
+ if (diff !== 0)
275
+ return diff;
276
+ return a.localeCompare(b);
277
+ });
278
+ }
279
+ /**
280
+ * Append the body of a single cluster (after its `### Label` heading and
281
+ * optional callout have already been pushed) to the line buffer. Renders:
282
+ * 1. Builtin/non-MCP tool names (TOOL_ORDER sort, alphabetical fallback).
283
+ * 2. MCP servers alphabetical, each with capped `+N more` tool list and
284
+ * optional `(N deferred)` suffix when deferred entries exist.
285
+ * 3. Prompt skills alphabetical, capped at 8 + `+N more`.
286
+ *
287
+ * Elision is handled by the caller (skip this body, emit count-only lines
288
+ * instead).
289
+ */
290
+ function appendClusterBody(lines, cluster) {
291
+ if (cluster.builtins.length > 0) {
292
+ const sorted = sortBuiltinsInCluster(cluster.builtins);
293
+ lines.push(`- ${sorted.join(", ")}`);
294
+ }
295
+ const sortedServers = [...cluster.mcpServers.entries()].sort(([a], [b]) => a.localeCompare(b));
296
+ for (const [server, bucket] of sortedServers) {
297
+ const sortedTools = bucket.activeTools.slice().sort((a, b) => a.localeCompare(b));
298
+ const shortNames = sortedTools.map((full) => stripServerPrefix(full, server));
299
+ const head = shortNames.slice(0, PER_GROUP_NAME_CAP);
300
+ const overflow = shortNames.length - head.length;
301
+ const namesPart = head.length === 0
302
+ ? ""
303
+ : `: ${head.join(", ")}${overflow > 0 ? `, +${overflow} more` : ""}`;
304
+ const deferredPart = bucket.deferredCount > 0
305
+ ? ` (${bucket.deferredCount} deferred)`
306
+ : "";
307
+ lines.push(`- [${server}] (${bucket.activeTools.length} tools${deferredPart})${namesPart}`);
308
+ }
309
+ if (cluster.skills.length > 0) {
310
+ const sortedSkills = cluster.skills
311
+ .slice()
312
+ .sort((a, b) => a.name.localeCompare(b.name));
313
+ const head = sortedSkills.slice(0, PER_GROUP_NAME_CAP);
314
+ const overflow = sortedSkills.length - head.length;
315
+ const names = head.map((s) => s.name).join(", ");
316
+ const overflowText = overflow > 0 ? `, +${overflow} more` : "";
317
+ lines.push(`- skills: ${names}${overflowText}`);
318
+ }
319
+ }
320
+ /**
321
+ * Strip the `mcp__<server>--` prefix from a sanitized MCP tool name so the
322
+ * cluster body shows compact short names. Falls back to the full name if
323
+ * the prefix does not match (defensive -- the upstream parser already
324
+ * validated the shape).
325
+ */
326
+ function stripServerPrefix(toolName, server) {
327
+ const prefix = `mcp__${server}--`;
328
+ return toolName.startsWith(prefix) ? toolName.slice(prefix.length) : toolName;
329
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Inline-consumption drain seams.
3
+ *
4
+ * The drain trigger lives at the BRIDGE call site (`tool_execution_end` for
5
+ * `message` send/reply/attach) -- NOT in pi-executor.ts. The helpers in
6
+ * this module are what the bridge invokes:
7
+ *
8
+ * - markRead(key): mark inbound messages for the composite key as read.
9
+ * Reads tool context via `tryGetContext()` so the
10
+ * function does NOT take a passed-in deps object.
11
+ * No-op outside an AsyncLocalStorage scope.
12
+ *
13
+ * - markConsumed(key): mark inbound messages for the composite key as
14
+ * consumed by the agent's response. Same context
15
+ * contract as markRead.
16
+ *
17
+ * - drainAt(key): orchestrator. Runs markRead + markConsumed under
18
+ * a per-composite-key single-tick inflight gate
19
+ * (`drainInflightByKey: Map<string, Promise<void>>`).
20
+ * Concurrent calls for the same composite key
21
+ * return immediately; concurrent calls for
22
+ * DIFFERENT composite keys (different agentId /
23
+ * channelType / channelId) drain independently.
24
+ * Failures are non-fatal: suppressError +
25
+ * structured WARN log. The drainInflightByKey state
26
+ * is owned by the bridge (BridgeMetricsState) so
27
+ * the bridge threads it into drainAt at each call
28
+ * site.
29
+ *
30
+ * The actual inline-consumption queue does not exist as a concrete data
31
+ * structure today -- this module provides the structural seam. Future
32
+ * work plugs queue/state into `tryGetContext()` so markRead / markConsumed
33
+ * read it without re-threading through every caller. Today the helpers
34
+ * are observability-only stubs that log at DEBUG when context is present
35
+ * and fall through silently when outside any request scope.
36
+ *
37
+ * This module lives in `packages/agent/src/executor/` (not in the bridge)
38
+ * so executor-post-execution.ts can re-export the helpers for source-grep
39
+ * tests, while the bridge imports from here directly. This avoids a
40
+ * circular import (executor-post-execution -> pi-executor -> bridge ->
41
+ * executor-post-execution).
42
+ *
43
+ * @module
44
+ */
45
+ import type { ComisLogger } from "@comis/infra";
46
+ /**
47
+ * Composite drain key uniquely identifies the inline-consumption queue
48
+ * partition for a single (agent, channel, channel-id) triple.
49
+ *
50
+ * Same shape as `BackgroundSessionResolver.ActiveSessionKey` so a single
51
+ * triple is reusable across the bridge / resolver / drain surface.
52
+ */
53
+ export interface DrainKey {
54
+ agentId: string;
55
+ channelType: string;
56
+ channelId: string;
57
+ }
58
+ /**
59
+ * State container for the per-composite-key drain inflight gate.
60
+ *
61
+ * Owned by the bridge (`BridgeMetricsState.drainInflightByKey`) and
62
+ * threaded into `drainAt` at each call site. A `Map` (rather than a single
63
+ * `drainInflight: Promise`) is required so concurrent drains for DIFFERENT
64
+ * composite keys can run independently (multi-agent isolation).
65
+ */
66
+ export interface DrainInflightState {
67
+ drainInflightByKey: Map<string, Promise<void>>;
68
+ }
69
+ /**
70
+ * Format a composite drain key into a deterministic string used as the
71
+ * inflight-gate Map key. Mirrors the resolver's composite-key shape so the
72
+ * gate keys are interchangeable with resolver keys (no parallel formatting
73
+ * surfaces to drift).
74
+ */
75
+ export declare function formatDrainKey(key: DrainKey): string;
76
+ /**
77
+ * Mark inbound messages for the composite drain key as read.
78
+ *
79
+ * Reads tool context via `tryGetContext()` -- when called outside any
80
+ * AsyncLocalStorage scope (test fixture, sub-agent path), this is a silent
81
+ * no-op. Otherwise emits a DEBUG-level observability log so operators can
82
+ * correlate drain activity with ALS context propagation.
83
+ *
84
+ * @param key - Composite drain key (agentId, channelType, channelId).
85
+ * @param logger - Logger for the (rare) DEBUG observability path.
86
+ */
87
+ export declare function markRead(key: DrainKey, logger: ComisLogger): void;
88
+ /**
89
+ * Mark inbound messages for the composite drain key as consumed.
90
+ *
91
+ * Same context contract as `markRead`. No-op outside AsyncLocalStorage
92
+ * scope.
93
+ *
94
+ * @param key - Composite drain key (agentId, channelType, channelId).
95
+ * @param logger - Logger for the (rare) DEBUG observability path.
96
+ */
97
+ export declare function markConsumed(key: DrainKey, logger: ComisLogger): void;
98
+ /**
99
+ * drainAt: composite-keyed inline-consumption drain with single-tick gate.
100
+ *
101
+ * Invoked by the bridge on `tool_execution_end` for successful
102
+ * `message(send|reply|attach)` calls. Runs `markRead` + `markConsumed`
103
+ * under a per-composite-key inflight gate so:
104
+ * - Concurrent drains for the SAME composite key return immediately
105
+ * (lock-safe drain).
106
+ * - Concurrent drains for DIFFERENT composite keys (different
107
+ * agentId / channelType / channelId) run independently (multi-agent
108
+ * isolation).
109
+ *
110
+ * Failures are non-fatal: a per-event `.catch(...)` logs WARN with `hint`
111
+ * + `errorKind`, and the outer `suppressError` ensures the bridge's
112
+ * `tool_execution_end` propagation is never aborted by drain misbehavior.
113
+ *
114
+ * Map-entry cleanup (`.delete(formatted)` in `.finally(...)`) is required
115
+ * to prevent unbounded growth across long-running sessions; the entry is
116
+ * removed within one event-loop tick of the drain promise settling.
117
+ *
118
+ * @param key - Composite drain key (agentId, channelType, channelId).
119
+ * @param state - Bridge-owned inflight-gate Map (drainInflightByKey).
120
+ * @param logger - Logger for the WARN failure log + DEBUG observability.
121
+ */
122
+ export declare function drainAt(key: DrainKey, state: DrainInflightState, logger: ComisLogger): void;
@@ -0,0 +1,173 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Inline-consumption drain seams.
4
+ *
5
+ * The drain trigger lives at the BRIDGE call site (`tool_execution_end` for
6
+ * `message` send/reply/attach) -- NOT in pi-executor.ts. The helpers in
7
+ * this module are what the bridge invokes:
8
+ *
9
+ * - markRead(key): mark inbound messages for the composite key as read.
10
+ * Reads tool context via `tryGetContext()` so the
11
+ * function does NOT take a passed-in deps object.
12
+ * No-op outside an AsyncLocalStorage scope.
13
+ *
14
+ * - markConsumed(key): mark inbound messages for the composite key as
15
+ * consumed by the agent's response. Same context
16
+ * contract as markRead.
17
+ *
18
+ * - drainAt(key): orchestrator. Runs markRead + markConsumed under
19
+ * a per-composite-key single-tick inflight gate
20
+ * (`drainInflightByKey: Map<string, Promise<void>>`).
21
+ * Concurrent calls for the same composite key
22
+ * return immediately; concurrent calls for
23
+ * DIFFERENT composite keys (different agentId /
24
+ * channelType / channelId) drain independently.
25
+ * Failures are non-fatal: suppressError +
26
+ * structured WARN log. The drainInflightByKey state
27
+ * is owned by the bridge (BridgeMetricsState) so
28
+ * the bridge threads it into drainAt at each call
29
+ * site.
30
+ *
31
+ * The actual inline-consumption queue does not exist as a concrete data
32
+ * structure today -- this module provides the structural seam. Future
33
+ * work plugs queue/state into `tryGetContext()` so markRead / markConsumed
34
+ * read it without re-threading through every caller. Today the helpers
35
+ * are observability-only stubs that log at DEBUG when context is present
36
+ * and fall through silently when outside any request scope.
37
+ *
38
+ * This module lives in `packages/agent/src/executor/` (not in the bridge)
39
+ * so executor-post-execution.ts can re-export the helpers for source-grep
40
+ * tests, while the bridge imports from here directly. This avoids a
41
+ * circular import (executor-post-execution -> pi-executor -> bridge ->
42
+ * executor-post-execution).
43
+ *
44
+ * @module
45
+ */
46
+ import { tryGetContext } from "@comis/core";
47
+ import { suppressError } from "@comis/shared";
48
+ // ---------------------------------------------------------------------------
49
+ // Helpers
50
+ // ---------------------------------------------------------------------------
51
+ /**
52
+ * Format a composite drain key into a deterministic string used as the
53
+ * inflight-gate Map key. Mirrors the resolver's composite-key shape so the
54
+ * gate keys are interchangeable with resolver keys (no parallel formatting
55
+ * surfaces to drift).
56
+ */
57
+ export function formatDrainKey(key) {
58
+ return `${key.agentId}:${key.channelType}:${key.channelId}`;
59
+ }
60
+ /**
61
+ * Mark inbound messages for the composite drain key as read.
62
+ *
63
+ * Reads tool context via `tryGetContext()` -- when called outside any
64
+ * AsyncLocalStorage scope (test fixture, sub-agent path), this is a silent
65
+ * no-op. Otherwise emits a DEBUG-level observability log so operators can
66
+ * correlate drain activity with ALS context propagation.
67
+ *
68
+ * @param key - Composite drain key (agentId, channelType, channelId).
69
+ * @param logger - Logger for the (rare) DEBUG observability path.
70
+ */
71
+ export function markRead(key, logger) {
72
+ const ctx = tryGetContext();
73
+ if (!ctx) {
74
+ // No AsyncLocalStorage scope: markRead is a no-op outside a request-
75
+ // scoped context. The bridge's `drainAt` is invoked from inside the
76
+ // request scope, but tests / sub-agent paths may invoke directly.
77
+ return;
78
+ }
79
+ // Future: read the inline-consumption queue partition for `key` from
80
+ // `ctx` and flip status. Today: structural seam + observability.
81
+ logger.debug({
82
+ submodule: "drain.markRead",
83
+ agentId: key.agentId,
84
+ channelType: key.channelType,
85
+ channelId: key.channelId,
86
+ traceId: ctx.traceId,
87
+ }, "markRead");
88
+ }
89
+ /**
90
+ * Mark inbound messages for the composite drain key as consumed.
91
+ *
92
+ * Same context contract as `markRead`. No-op outside AsyncLocalStorage
93
+ * scope.
94
+ *
95
+ * @param key - Composite drain key (agentId, channelType, channelId).
96
+ * @param logger - Logger for the (rare) DEBUG observability path.
97
+ */
98
+ export function markConsumed(key, logger) {
99
+ const ctx = tryGetContext();
100
+ if (!ctx) {
101
+ return;
102
+ }
103
+ logger.debug({
104
+ submodule: "drain.markConsumed",
105
+ agentId: key.agentId,
106
+ channelType: key.channelType,
107
+ channelId: key.channelId,
108
+ traceId: ctx.traceId,
109
+ }, "markConsumed");
110
+ }
111
+ /**
112
+ * Run a single drain pass for the composite key.
113
+ *
114
+ * Calls `markRead` + `markConsumed` sequentially. Both helpers no-op
115
+ * outside an AsyncLocalStorage scope, so this function is safe to invoke
116
+ * from the bridge's event handler without wrapping in `runWithContext`.
117
+ */
118
+ async function runOneDrainPass(key, logger) {
119
+ markRead(key, logger);
120
+ markConsumed(key, logger);
121
+ }
122
+ /**
123
+ * drainAt: composite-keyed inline-consumption drain with single-tick gate.
124
+ *
125
+ * Invoked by the bridge on `tool_execution_end` for successful
126
+ * `message(send|reply|attach)` calls. Runs `markRead` + `markConsumed`
127
+ * under a per-composite-key inflight gate so:
128
+ * - Concurrent drains for the SAME composite key return immediately
129
+ * (lock-safe drain).
130
+ * - Concurrent drains for DIFFERENT composite keys (different
131
+ * agentId / channelType / channelId) run independently (multi-agent
132
+ * isolation).
133
+ *
134
+ * Failures are non-fatal: a per-event `.catch(...)` logs WARN with `hint`
135
+ * + `errorKind`, and the outer `suppressError` ensures the bridge's
136
+ * `tool_execution_end` propagation is never aborted by drain misbehavior.
137
+ *
138
+ * Map-entry cleanup (`.delete(formatted)` in `.finally(...)`) is required
139
+ * to prevent unbounded growth across long-running sessions; the entry is
140
+ * removed within one event-loop tick of the drain promise settling.
141
+ *
142
+ * @param key - Composite drain key (agentId, channelType, channelId).
143
+ * @param state - Bridge-owned inflight-gate Map (drainInflightByKey).
144
+ * @param logger - Logger for the WARN failure log + DEBUG observability.
145
+ */
146
+ export function drainAt(key, state, logger) {
147
+ const formatted = formatDrainKey(key);
148
+ if (state.drainInflightByKey.has(formatted)) {
149
+ // Single-tick gate: a drain is already in flight for this composite
150
+ // key; second concurrent call returns immediately.
151
+ return;
152
+ }
153
+ const draining = runOneDrainPass(key, logger)
154
+ .catch((err) => {
155
+ logger.warn({
156
+ submodule: "drain.drainAt",
157
+ agentId: key.agentId,
158
+ channelType: key.channelType,
159
+ channelId: key.channelId,
160
+ err,
161
+ hint: "drainAt failed; will retry on next tool_execution_end. Investigate when this fires repeatedly without recovery.",
162
+ errorKind: "internal",
163
+ }, "drainAt failed");
164
+ })
165
+ .finally(() => {
166
+ state.drainInflightByKey.delete(formatted);
167
+ });
168
+ state.drainInflightByKey.set(formatted, draining);
169
+ // Belt-and-braces: outer suppressError ensures the bridge's
170
+ // tool_execution_end propagation is NEVER aborted by drain misbehavior
171
+ // (fire-and-forget contract).
172
+ suppressError(draining, "drain at bridge call site (B15)");
173
+ }
@@ -13,9 +13,9 @@ import { isSignedReplayError } from "./signed-replay-detector.js";
13
13
  const ERROR_PATTERNS = [
14
14
  // Billing / credits
15
15
  {
16
- test: /credit balance is too low|billing|purchase credits|insufficient.?funds|payment.?required/i,
16
+ test: /credit balance is too low|billing|purchase credits|insufficient.?funds|payment.?required|usage.?limits?|regain.?access|spend.?(cap|limit)/i,
17
17
  category: "credit_exhausted",
18
- userMessage: "The AI service is currently unavailable due to a billing issue. Please notify the system administrator.",
18
+ userMessage: "The AI service is currently unavailable due to a billing or usage-cap issue. Please notify the system administrator.",
19
19
  retryable: false,
20
20
  },
21
21
  // Rate limiting (429)