@desplega.ai/agent-swarm 1.156.0 → 1.158.0

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 (263) hide show
  1. package/dist/{acp-adapter-c1msptrw.js → acp-adapter-xaq4n6pc.js} +1 -1
  2. package/dist/{actions-wd41qtdc.js → actions-p9hz5yat.js} +15 -15
  3. package/dist/{app-tz2qrnah.js → app-g26wdcz6.js} +4 -4
  4. package/dist/{artifact-td4q4dch.js → artifact-3v5t9jtg.js} +1 -1
  5. package/dist/{assistant-c3q1zj2t.js → assistant-wydfqkb3.js} +16 -16
  6. package/dist/{boot-reembed-0be1krkm.js → boot-reembed-g5ckg9yb.js} +6 -6
  7. package/dist/{boot-reembed-r00j03f3.js → boot-reembed-se8zmdcp.js} +5 -5
  8. package/dist/{boot-scrub-logs-9j4hd0vw.js → boot-scrub-logs-wfxqan61.js} +3 -3
  9. package/dist/{seed-pricing-ncgxz1r2.js → boot-seeds-s3q01xj3.js} +10 -10
  10. package/dist/{claude-adapter-112w28qc.js → claude-adapter-tv2x4f36.js} +7 -7
  11. package/dist/{claude-managed-adapter-ea2aga6w.js → claude-managed-adapter-41b2s7fd.js} +2 -2
  12. package/dist/{claude-sdk-session-k7pb5kfz.js → claude-sdk-session-t442qz6f.js} +8 -8
  13. package/dist/{cli-e781c6f9.js → cli-0xjkj1j7.js} +1 -0
  14. package/dist/{cli-58rgvkt4.js → cli-1hkrch3k.js} +2 -2
  15. package/dist/{cli-mx3bzbv8.js → cli-20cj37vt.js} +1968 -44
  16. package/dist/{cli-h0wdadsx.js → cli-4198qc9v.js} +7 -3
  17. package/dist/{cli-24w39x6n.js → cli-47m6dkd3.js} +362 -126
  18. package/dist/{cli-90x9ewbz.js → cli-53sz8gkz.js} +1 -1
  19. package/dist/{cli-005pze6s.js → cli-5d10vd2m.js} +1 -1
  20. package/dist/{cli-08n8jxcw.js → cli-65hjmev5.js} +57 -17
  21. package/dist/{cli-scwq51gn.js → cli-68w09sjw.js} +7 -1
  22. package/dist/{cli-7j2098n5.js → cli-7gtvvpbh.js} +4 -4
  23. package/dist/{cli-b05d7szr.js → cli-7s3syk22.js} +18 -11
  24. package/dist/{cli-9w4ex8vh.js → cli-8qmme6vd.js} +2 -2
  25. package/dist/{cli-28wkjpdp.js → cli-987hg0aa.js} +2 -2
  26. package/dist/{cli-7pgcpcrg.js → cli-9httdpyq.js} +1 -1
  27. package/dist/{cli-jk26c1sz.js → cli-aky8tctm.js} +26 -93
  28. package/dist/{cli-865swy5p.js → cli-c07r9a2m.js} +3 -2
  29. package/dist/{cli-nyqza443.js → cli-fgqxwrpv.js} +291 -12
  30. package/dist/{cli-9pyah9c4.js → cli-ftfvggzx.js} +633 -166
  31. package/dist/{cli-ytxs7mn1.js → cli-g8djp6qe.js} +17 -0
  32. package/dist/{cli-avkd6mqv.js → cli-h967nb6r.js} +551 -0
  33. package/dist/{cli-y8bfxjtw.js → cli-j55n64b6.js} +23 -22
  34. package/dist/{cli-7vj52450.js → cli-j57qkrpq.js} +86 -28
  35. package/dist/{cli-tjmghmm5.js → cli-q67y4wzm.js} +6 -3
  36. package/dist/{cli-n6zq3kbd.js → cli-rj19hc34.js} +1 -1
  37. package/dist/{cli-tp0d7vzb.js → cli-rjehvrts.js} +4 -4
  38. package/dist/{cli-s8hpxeg4.js → cli-rqz8qbw7.js} +245 -75
  39. package/dist/{cli-p37sg0a9.js → cli-s8eet254.js} +1 -1
  40. package/dist/cli-t1ze64h3.js +811 -0
  41. package/dist/{cli-hc2fkhdv.js → cli-ws6rqycm.js} +3 -2
  42. package/dist/{cli-t1cd3mhp.js → cli-wscdvcqz.js} +13 -5
  43. package/dist/{cli-xa4j64zh.js → cli-x24pshtq.js} +5 -5
  44. package/dist/{cli-nbp6m6gp.js → cli-xberxa1r.js} +51 -26
  45. package/dist/{cli-szcyv1mm.js → cli-xhaeg2nc.js} +1 -1
  46. package/dist/cli.js +30 -16
  47. package/dist/{codex-adapter-g4tj1f2g.js → codex-adapter-cnm8wk11.js} +5 -5
  48. package/dist/{codex-session-runner-ad8ha7zt.js → codex-session-runner-gfg547zj.js} +5 -5
  49. package/dist/{commands-z3gd8ppc.js → commands-e3bn0p75.js} +3 -3
  50. package/dist/{db-bezm97r9.js → db-03v9kzr2.js} +17 -3
  51. package/dist/{dispatcher-a97hc33y.js → dispatcher-nrmg8yht.js} +14 -14
  52. package/dist/{dsh-adapter-1dwg97zv.js → dsh-adapter-k03nfqa6.js} +2 -2
  53. package/dist/{handlers-wtdy1x02.js → handlers-aqcedbyb.js} +17 -17
  54. package/dist/{hook-7edgmzng.js → hook-7vkskkre.js} +19 -11
  55. package/dist/{hook-fvs58a5b.js → hook-ybvmrmhq.js} +4 -4
  56. package/dist/{http-phnf93hp.js → http-2eqa8t24.js} +432 -195
  57. package/dist/{index-7x131mmf.js → index-1yks3qcr.js} +14 -14
  58. package/dist/{index-1fktvwxc.js → index-a3vn96n8.js} +31 -28
  59. package/dist/{index-yp8rasxk.js → index-c6zsv1bd.js} +5 -5
  60. package/dist/{index-50jxgw0d.js → index-chgdkq36.js} +14 -14
  61. package/dist/{index-7jwk7nsm.js → index-x71bvq0p.js} +15 -15
  62. package/dist/{keepalive-nnz07ebd.js → keepalive-vtkf5n99.js} +5 -5
  63. package/dist/{lead-mg7qcgww.js → lead-h3sp357x.js} +26 -26
  64. package/dist/{maintenance-ywmnqrpx.js → maintenance-jzeqtk3q.js} +6 -6
  65. package/dist/{oauth-refresh-sweep-c63sqfs1.js → oauth-refresh-sweep-qp3dzmdp.js} +4 -4
  66. package/dist/{onboard-2zj8sqkq.js → onboard-tmsp0qbz.js} +278 -81
  67. package/dist/{opencode-adapter-z64xjzec.js → opencode-adapter-x6x2da1z.js} +3 -3
  68. package/dist/{otel-impl-jgdfm26f.js → otel-impl-9mz73q65.js} +416 -301
  69. package/dist/{pi-mono-adapter-2b7mw16r.js → pi-mono-adapter-m6dvv0dz.js} +2 -2
  70. package/dist/{pricing-refresh-3whntb69.js → pricing-refresh-v1cbr9te.js} +5 -5
  71. package/dist/{rbac-roles-3zptcc3p.js → rbac-roles-c9wqz83x.js} +4 -4
  72. package/dist/{render-v2-dbvpyysc.js → render-v2-gryrh9h8.js} +4 -4
  73. package/dist/{setup-qwx0j8z9.js → setup-q9720bt6.js} +2 -2
  74. package/dist/{virtual-modules-8qheft18.js → virtual-modules-3htpgn8x.js} +1 -1
  75. package/dist/{work-objects-sjbcmvep.js → work-objects-b76x7deb.js} +17 -17
  76. package/dist/{worker-xezqdkb7.js → worker-6vrxbv41.js} +26 -26
  77. package/openapi.json +1626 -629
  78. package/package.json +3 -2
  79. package/src/apps/catalog.generated.json +229 -0
  80. package/src/apps/page-validator.ts +71 -16
  81. package/src/apps/row-store.ts +66 -11
  82. package/src/apps/sync.ts +59 -17
  83. package/src/artifact-sdk/browser-sdk.ts +436 -2
  84. package/src/be/approval-sweeps.ts +109 -0
  85. package/src/be/boot-seeds.ts +40 -0
  86. package/src/be/chunking.ts +1 -1
  87. package/src/be/db/agents.ts +20 -0
  88. package/src/be/db/approvals.ts +78 -0
  89. package/src/be/db/tasks/read.ts +197 -39
  90. package/src/be/db/tasks/write.ts +41 -9
  91. package/src/be/db-client.ts +107 -48
  92. package/src/be/db.ts +219 -76
  93. package/src/be/events.ts +33 -1
  94. package/src/be/memory/graph-expansion.ts +10 -3
  95. package/src/be/memory/providers/sqlite-store.ts +55 -13
  96. package/src/be/memory/raters/retrieval.ts +5 -2
  97. package/src/be/memory/reranker.ts +26 -2
  98. package/src/be/memory/types.ts +2 -0
  99. package/src/be/migrations/166_approval_request_auto_cancellation.sql +9 -0
  100. package/src/be/migrations/171_memory_retrieval_relevance.sql +8 -0
  101. package/src/be/migrations/175_workflow_runs_list_index.sql +11 -0
  102. package/src/be/migrations/177_events_event_agent_created_index.sql +11 -0
  103. package/src/be/models-catalog.ts +2 -0
  104. package/src/be/modelsdev-cache.json +1857 -0
  105. package/src/be/modelsdev-cache.ts +14 -1
  106. package/src/be/pricing-normalize.ts +2 -0
  107. package/src/be/scripts/typecheck.ts +1 -0
  108. package/src/be/seed-pricing.ts +3 -1
  109. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  110. package/src/be/steering.ts +53 -3
  111. package/src/be/swarm-config-guard.ts +3 -0
  112. package/src/be/task-citations.ts +29 -0
  113. package/src/cli.tsx +14 -1
  114. package/src/commands/onboard/compose-generator.ts +8 -3
  115. package/src/commands/onboard/container-engine.ts +220 -0
  116. package/src/commands/onboard/manifest.ts +1 -0
  117. package/src/commands/onboard/steps/health-check.tsx +4 -1
  118. package/src/commands/onboard/steps/prereq-check.tsx +50 -54
  119. package/src/commands/onboard/steps/start.tsx +31 -16
  120. package/src/commands/onboard/types.ts +7 -2
  121. package/src/commands/onboard.tsx +39 -4
  122. package/src/commands/profile-sync.ts +79 -26
  123. package/src/commands/runner.ts +8 -1
  124. package/src/extensions/catalog.generated.json +17 -0
  125. package/src/extensions/contract-types.generated.ts +1 -1
  126. package/src/heartbeat/heartbeat.ts +263 -103
  127. package/src/hooks/hook.ts +21 -12
  128. package/src/http/agents.ts +35 -0
  129. package/src/http/approval-requests.ts +136 -64
  130. package/src/http/apps.ts +14 -3
  131. package/src/http/config.ts +15 -1
  132. package/src/http/events.ts +62 -2
  133. package/src/http/index.ts +6 -4
  134. package/src/http/memory.ts +21 -7
  135. package/src/http/schedules.ts +1 -1
  136. package/src/http/session-data.ts +9 -4
  137. package/src/http/sessions.ts +25 -18
  138. package/src/http/stats.ts +19 -7
  139. package/src/http/status.ts +11 -1
  140. package/src/http/tasks.ts +40 -8
  141. package/src/http/webhooks.ts +67 -46
  142. package/src/http/workflows.ts +15 -20
  143. package/src/linear/outbound.ts +37 -15
  144. package/src/otel-impl.ts +151 -1
  145. package/src/otel.ts +14 -3
  146. package/src/prompts/memories.ts +19 -2
  147. package/src/providers/claude-managed-models.ts +8 -0
  148. package/src/providers/codex-agents-md.ts +140 -17
  149. package/src/providers/modelsdev-reasoning.json +551 -0
  150. package/src/rbac/legacy-policy.ts +16 -0
  151. package/src/rbac/permissions.ts +4 -0
  152. package/src/scheduler/deferred-task-waits.ts +28 -0
  153. package/src/scheduler/schedule-task.ts +3 -0
  154. package/src/scheduler/scheduler.ts +14 -0
  155. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  156. package/src/scripts-runtime/swarm-sdk.ts +9 -0
  157. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  158. package/src/scripts-runtime/types/swarm-extension.d.ts +1 -0
  159. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  160. package/src/server.ts +12 -7
  161. package/src/slack/app.ts +35 -4
  162. package/src/slack/connection-state.ts +17 -0
  163. package/src/tasks/context-key.ts +1 -0
  164. package/src/tasks/worker-follow-up.ts +6 -2
  165. package/src/tests/agent-task-activity.test.ts +74 -0
  166. package/src/tests/agents-list-model-display.test.ts +6 -1
  167. package/src/tests/approval-auto-cancellation-migration.test.ts +104 -0
  168. package/src/tests/approval-requests.test.ts +395 -0
  169. package/src/tests/approval-sweeps-config.test.ts +47 -0
  170. package/src/tests/approval-sweeps.test.ts +508 -0
  171. package/src/tests/apps-kibo-blocks.test.ts +77 -0
  172. package/src/tests/apps-spike.test.ts +56 -1
  173. package/src/tests/apps-sync-engine.test.ts +20 -9
  174. package/src/tests/boot-seeds.test.ts +170 -0
  175. package/src/tests/cancel-approval-request-tool.test.ts +115 -0
  176. package/src/tests/claude-managed-adapter.test.ts +6 -0
  177. package/src/tests/codex-adapter.test.ts +126 -1
  178. package/src/tests/config-key-filter.test.ts +80 -0
  179. package/src/tests/context-window.test.ts +1 -0
  180. package/src/tests/db-client-busy-retry.test.ts +96 -0
  181. package/src/tests/db-client.test.ts +60 -1
  182. package/src/tests/db-task-read-contract.test.ts +10 -0
  183. package/src/tests/db-task-write-contract.test.ts +9 -0
  184. package/src/tests/deferred-schedule-reaper.test.ts +193 -0
  185. package/src/tests/docs-index-footprint.test.js +3 -3
  186. package/src/tests/events-http.test.ts +103 -0
  187. package/src/tests/extensions-catalog.test.ts +22 -17
  188. package/src/tests/extensions-pre-task.test.ts +107 -0
  189. package/src/tests/fixtures/kibo-blocks-definition.json.txt +105 -0
  190. package/src/tests/follow-up-config-inheritance.test.ts +260 -0
  191. package/src/tests/heartbeat-orphan-resume.test.ts +84 -0
  192. package/src/tests/heartbeat-reboot-live.test.ts +95 -0
  193. package/src/tests/heartbeat-stall-cas.test.ts +80 -0
  194. package/src/tests/heartbeat.test.ts +99 -3
  195. package/src/tests/hook-mcp-config.test.ts +29 -0
  196. package/src/tests/http-api-integration.test.ts +16 -2
  197. package/src/tests/kv-storage.test.ts +62 -0
  198. package/src/tests/linear-outbound-sync.test.ts +120 -5
  199. package/src/tests/list-endpoint-slimming.test.ts +91 -0
  200. package/src/tests/memory-graph-organic-route.test.ts +155 -0
  201. package/src/tests/memory-http-recall-gating.test.ts +14 -4
  202. package/src/tests/memory-hybrid.test.ts +161 -1
  203. package/src/tests/memory-prompt-recall-crowdout.test.ts +297 -0
  204. package/src/tests/memory.test.ts +22 -0
  205. package/src/tests/onboard-container-engine.test.ts +240 -0
  206. package/src/tests/otel-event-loop-and-logs.test.ts +99 -0
  207. package/src/tests/pricing-routes.test.ts +14 -0
  208. package/src/tests/profile-sync-warning.test.ts +8 -4
  209. package/src/tests/promote-draft-task-route.test.ts +19 -0
  210. package/src/tests/provider-registration.test.ts +296 -42
  211. package/src/tests/rbac-engine.test.ts +30 -0
  212. package/src/tests/reasoning-effort.test.ts +8 -0
  213. package/src/tests/script-connections.test.ts +7 -0
  214. package/src/tests/sdk-allowlist.test.ts +39 -0
  215. package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
  216. package/src/tests/sessions-detail-batch.test.ts +250 -0
  217. package/src/tests/sessions.test.ts +118 -0
  218. package/src/tests/slack-config.test.ts +22 -0
  219. package/src/tests/sonnet-5-5-registry.test.ts +44 -0
  220. package/src/tests/status.test.ts +36 -1
  221. package/src/tests/swarm-diff.test.ts +316 -10
  222. package/src/tests/task-completion-idempotency.test.ts +2 -1
  223. package/src/tests/tasks-requested-by-filter.test.ts +3 -1
  224. package/src/tests/tool-annotations.test.ts +1 -1
  225. package/src/tests/ui-list-slim-projections.test.ts +158 -0
  226. package/src/tests/webhooks-task-blocked.test.ts +127 -0
  227. package/src/tests/workflow-approval-recovery.test.ts +203 -0
  228. package/src/tests/workflow-hitl-routing.test.ts +22 -1
  229. package/src/tests/workflow-hitl-slack-card.test.ts +52 -0
  230. package/src/tests/workflow-http-v2.test.ts +99 -8
  231. package/src/tests/workflow-retry-validation.test.ts +56 -0
  232. package/src/tests/workflow-tla-races.test.ts +389 -0
  233. package/src/tests/workflow-validation-port-routing.test.ts +113 -1
  234. package/src/tools/cancel-approval-request.ts +57 -0
  235. package/src/tools/memory-search.ts +1 -0
  236. package/src/tools/request-human-input.ts +4 -2
  237. package/src/tools/schedules/list-schedules.ts +4 -2
  238. package/src/tools/send-task.ts +4 -0
  239. package/src/tools/templates.ts +19 -0
  240. package/src/tools/tool-config.ts +2 -1
  241. package/src/tools/workflows/list-workflow-runs.ts +1 -0
  242. package/src/types.ts +38 -0
  243. package/src/utils/context-window.ts +1 -0
  244. package/src/workflows/approval-cancel.ts +152 -0
  245. package/src/workflows/approval-notifications.ts +84 -0
  246. package/src/workflows/checkpoint.ts +10 -5
  247. package/src/workflows/definition.ts +21 -0
  248. package/src/workflows/engine.ts +152 -21
  249. package/src/workflows/executors/human-in-the-loop.ts +1 -1
  250. package/src/workflows/recovery.ts +100 -45
  251. package/src/workflows/resume.ts +49 -18
  252. package/src/workflows/retry-poller.ts +71 -21
  253. package/templates/extensions/README.md +1 -0
  254. package/templates/extensions/github-sender-allowlist/README.md +18 -0
  255. package/templates/extensions/github-sender-allowlist/hooks.ts +55 -0
  256. package/templates/extensions/github-sender-allowlist/manifest.json +8 -0
  257. package/templates/skills/apps/content.md +20 -1
  258. package/templates/skills/code-quality/content.md +26 -1
  259. package/templates/skills/pages/content.md +16 -4
  260. package/templates/skills/pages/files/examples/multiplayer-board.html +3 -12
  261. package/templates/skills/scheduling/content.md +1 -1
  262. package/dist/cli-2daksqps.js +0 -378
  263. package/dist/rbac-roles-3zw68x72.js +0 -37
package/src/otel-impl.ts CHANGED
@@ -1,8 +1,12 @@
1
+ import { monitorEventLoopDelay } from "node:perf_hooks";
2
+ import { format } from "node:util";
1
3
  import {
4
+ type BatchObservableResult,
2
5
  type Counter,
3
6
  context,
4
7
  type Gauge,
5
8
  type Histogram,
9
+ type Meter,
6
10
  metrics,
7
11
  propagation,
8
12
  ROOT_CONTEXT,
@@ -12,6 +16,7 @@ import {
12
16
  type Tracer,
13
17
  trace,
14
18
  } from "@opentelemetry/api";
19
+ import { logs, SeverityNumber } from "@opentelemetry/api-logs";
15
20
  import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
16
21
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
17
22
  import {
@@ -149,7 +154,12 @@ export function resolveServiceName(serviceRole: string): string {
149
154
  return serviceRole === "api" ? `${baseServiceName}-api` : baseServiceName;
150
155
  }
151
156
 
152
- export async function boot(serviceRole: string): Promise<void> {
157
+ export interface BootOptions {
158
+ /** Mirror console output into OTel log records (API role only). */
159
+ exportConsoleLogs?: boolean;
160
+ }
161
+
162
+ export async function boot(serviceRole: string, options: BootOptions = {}): Promise<void> {
153
163
  if (sdk) return;
154
164
 
155
165
  const configuredResourceAttributes = parseResourceAttributes();
@@ -191,6 +201,14 @@ export async function boot(serviceRole: string): Promise<void> {
191
201
 
192
202
  sdk.start();
193
203
 
204
+ if (serviceRole === "api") {
205
+ startEventLoopDelayMetrics(metrics.getMeter(METER_NAME));
206
+ // NodeSDK already registers a global LoggerProvider exporting over OTLP
207
+ // to the same endpoint and resource as traces (OTEL_LOGS_EXPORTER
208
+ // defaults to otlp); nothing feeds it until the bridge is installed.
209
+ if (options.exportConsoleLogs) installConsoleLogBridge();
210
+ }
211
+
194
212
  const shutdown = async () => {
195
213
  try {
196
214
  await sdk?.shutdown();
@@ -204,8 +222,140 @@ export async function boot(serviceRole: string): Promise<void> {
204
222
  }
205
223
 
206
224
  export async function shutdown(): Promise<void> {
225
+ // SDK first: its final flush still reads the event-loop gauges and drains
226
+ // bridged log records.
207
227
  await sdk?.shutdown();
208
228
  sdk = undefined;
229
+ stopEventLoopDelayMetrics();
230
+ uninstallConsoleLogBridge();
231
+ }
232
+
233
+ let stopEventLoopDelay: (() => void) | undefined;
234
+
235
+ /**
236
+ * Event-loop delay from `perf_hooks.monitorEventLoopDelay`, exported as the
237
+ * semconv `nodejs.eventloop.delay.*` gauges (seconds). Each collection reads
238
+ * the histogram and resets it, so `max` is the worst stall since the last
239
+ * export. Server spans only start once the loop frees, so a stall never shows
240
+ * in span durations: a request queued behind a 3s synchronous query still
241
+ * reports a 14ms span. This metric is what sees the stall.
242
+ */
243
+ export function startEventLoopDelayMetrics(meter: Meter): void {
244
+ if (stopEventLoopDelay) return;
245
+ const histogram = monitorEventLoopDelay({ resolution: 10 });
246
+ histogram.enable();
247
+ const gauges = {
248
+ min: meter.createObservableGauge("nodejs.eventloop.delay.min", {
249
+ description: "Minimum event loop delay since the last export",
250
+ unit: "s",
251
+ }),
252
+ max: meter.createObservableGauge("nodejs.eventloop.delay.max", {
253
+ description: "Maximum event loop delay since the last export: the longest stall",
254
+ unit: "s",
255
+ }),
256
+ mean: meter.createObservableGauge("nodejs.eventloop.delay.mean", {
257
+ description: "Mean event loop delay since the last export",
258
+ unit: "s",
259
+ }),
260
+ p50: meter.createObservableGauge("nodejs.eventloop.delay.p50", {
261
+ description: "50th percentile event loop delay since the last export",
262
+ unit: "s",
263
+ }),
264
+ p90: meter.createObservableGauge("nodejs.eventloop.delay.p90", {
265
+ description: "90th percentile event loop delay since the last export",
266
+ unit: "s",
267
+ }),
268
+ p99: meter.createObservableGauge("nodejs.eventloop.delay.p99", {
269
+ description: "99th percentile event loop delay since the last export",
270
+ unit: "s",
271
+ }),
272
+ };
273
+ const seconds = (ns: number) => ns / 1e9;
274
+ const collect = (observer: BatchObservableResult) => {
275
+ // No sample yet (first interval shorter than the resolution): the
276
+ // histogram's min is a sentinel then, so report nothing.
277
+ if (histogram.count === 0) return;
278
+ observer.observe(gauges.min, seconds(histogram.min));
279
+ observer.observe(gauges.max, seconds(histogram.max));
280
+ observer.observe(gauges.mean, seconds(histogram.mean));
281
+ observer.observe(gauges.p50, seconds(histogram.percentile(50)));
282
+ observer.observe(gauges.p90, seconds(histogram.percentile(90)));
283
+ observer.observe(gauges.p99, seconds(histogram.percentile(99)));
284
+ histogram.reset();
285
+ };
286
+ const observables = Object.values(gauges);
287
+ meter.addBatchObservableCallback(collect, observables);
288
+ stopEventLoopDelay = () => {
289
+ meter.removeBatchObservableCallback(collect, observables);
290
+ histogram.disable();
291
+ stopEventLoopDelay = undefined;
292
+ };
293
+ }
294
+
295
+ export function stopEventLoopDelayMetrics(): void {
296
+ stopEventLoopDelay?.();
297
+ }
298
+
299
+ const CONSOLE_LOG_BODY_MAX_CHARS = 16_384;
300
+ const CONSOLE_SEVERITY = {
301
+ debug: { number: SeverityNumber.DEBUG, text: "DEBUG" },
302
+ log: { number: SeverityNumber.INFO, text: "INFO" },
303
+ info: { number: SeverityNumber.INFO, text: "INFO" },
304
+ warn: { number: SeverityNumber.WARN, text: "WARN" },
305
+ error: { number: SeverityNumber.ERROR, text: "ERROR" },
306
+ } as const;
307
+ type ConsoleMethod = keyof typeof CONSOLE_SEVERITY;
308
+
309
+ let restoreConsole: (() => void) | undefined;
310
+
311
+ /**
312
+ * Mirror console output into OTel log records (opt-in: OTEL_EXPORT_API_LOGS).
313
+ * stdout/stderr stay the primary sink; each line is also emitted to the
314
+ * global LoggerProvider, scrubbed at this egress point and correlated with
315
+ * the active span.
316
+ */
317
+ export function installConsoleLogBridge(): void {
318
+ if (restoreConsole) return;
319
+ const logger = logs.getLogger(METER_NAME);
320
+ const originals = new Map<ConsoleMethod, (...args: unknown[]) => void>();
321
+ let emitting = false;
322
+ for (const method of Object.keys(CONSOLE_SEVERITY) as ConsoleMethod[]) {
323
+ const original = console[method].bind(console);
324
+ originals.set(method, console[method]);
325
+ console[method] = (...args: unknown[]) => {
326
+ original(...args);
327
+ // An exporter or scrubber that logs must not recurse into itself.
328
+ if (emitting) return;
329
+ emitting = true;
330
+ try {
331
+ // Scrub before truncating so a cut can never split a secret into an
332
+ // unrecognizable fragment.
333
+ const text = scrubSecrets(format(...args));
334
+ const body =
335
+ text.length > CONSOLE_LOG_BODY_MAX_CHARS
336
+ ? `${text.slice(0, CONSOLE_LOG_BODY_MAX_CHARS)}…`
337
+ : text;
338
+ logger.emit({
339
+ severityNumber: CONSOLE_SEVERITY[method].number,
340
+ severityText: CONSOLE_SEVERITY[method].text,
341
+ body,
342
+ attributes: { "log.source": "console" },
343
+ });
344
+ } catch {
345
+ // Log export is best-effort; never break the caller's console call.
346
+ } finally {
347
+ emitting = false;
348
+ }
349
+ };
350
+ }
351
+ restoreConsole = () => {
352
+ for (const [method, original] of originals) console[method] = original;
353
+ restoreConsole = undefined;
354
+ };
355
+ }
356
+
357
+ export function uninstallConsoleLogBridge(): void {
358
+ restoreConsole?.();
209
359
  }
210
360
 
211
361
  export async function withSpan<T>(
package/src/otel.ts CHANGED
@@ -94,18 +94,29 @@ export function isOtelEnabled(): boolean {
94
94
  return otelConfigured();
95
95
  }
96
96
 
97
- export function isPollTracingEnabled(): boolean {
98
- const v = (process.env.OTEL_TRACE_POLL ?? "").trim().toLowerCase();
97
+ function envFlagEnabled(name: string): boolean {
98
+ const v = (process.env[name] ?? "").trim().toLowerCase();
99
99
  return v === "1" || v === "true" || v === "yes" || v === "on";
100
100
  }
101
101
 
102
+ export function isPollTracingEnabled(): boolean {
103
+ return envFlagEnabled("OTEL_TRACE_POLL");
104
+ }
105
+
106
+ /** Mirror the API's console output to the OTLP collector as log records. Off by default. */
107
+ export function isApiLogExportEnabled(): boolean {
108
+ return envFlagEnabled("OTEL_EXPORT_API_LOGS");
109
+ }
110
+
102
111
  export async function initOtel(serviceRole = process.env.AGENT_ROLE || "api"): Promise<void> {
103
112
  if (!otelConfigured() || initialized) return;
104
113
  initialized = true;
105
114
 
106
115
  try {
107
116
  const impl = await import("./otel-impl");
108
- await impl.boot(serviceRole);
117
+ await impl.boot(serviceRole, {
118
+ exportConsoleLogs: serviceRole === "api" && isApiLogExportEnabled(),
119
+ });
109
120
  realWithSpan = impl.withSpan;
110
121
  realStartSpan = impl.startSpan;
111
122
  realWithRemoteContext = impl.withRemoteContext;
@@ -18,9 +18,26 @@ export type RelevantMemory = {
18
18
  name: string;
19
19
  content: string;
20
20
  similarity: number;
21
+ rawSimilarity?: number;
21
22
  };
22
23
 
23
- export const SIMILARITY_THRESHOLD = 0.4;
24
+ /**
25
+ * Minimum pre-boost relevance for a memory to be injected into a task prompt.
26
+ *
27
+ * Compared against `rawSimilarity`, the [0,1] match score every retrieval arm
28
+ * emits before rerank (vec cosine, hybrid fused cosine, graph-derived), never
29
+ * against the composite `similarity`: the reranker's access, source-quality
30
+ * and usefulness multipliers can lift a score up to 4.5x, so a composite
31
+ * threshold admits unrelated rows. 0.55 sits between an unrelated query's best
32
+ * cosine (~0.33 on the production embedding model) and an exact-name hybrid
33
+ * hit (~0.65 fused).
34
+ */
35
+ export const SIMILARITY_THRESHOLD = 0.55;
36
+
37
+ /** The score the injection threshold applies to. Older servers omit `rawSimilarity`. */
38
+ export function memoryRelevance(memory: { similarity: number; rawSimilarity?: number }): number {
39
+ return memory.rawSimilarity ?? memory.similarity;
40
+ }
24
41
 
25
42
  const RATE_TOOL_HINT = `
26
43
 
@@ -35,7 +52,7 @@ This trains the swarm to surface better memories next time. Use sparingly:
35
52
  * append entirely (matching pre-step-5 behaviour).
36
53
  */
37
54
  export function renderMemoriesPrompt(memories: RelevantMemory[]): string | null {
38
- const useful = memories.filter((m) => m.similarity > SIMILARITY_THRESHOLD);
55
+ const useful = memories.filter((m) => memoryRelevance(m) > SIMILARITY_THRESHOLD);
39
56
  if (useful.length === 0) return null;
40
57
 
41
58
  const memoryContext = useful
@@ -31,6 +31,7 @@ export const CLAUDE_MANAGED_MODELS = [
31
31
  "claude-opus-5",
32
32
  "claude-fable-5",
33
33
  "claude-mythos-5",
34
+ "claude-sonnet-5-5",
34
35
  "claude-sonnet-5",
35
36
  "claude-sonnet-4-6",
36
37
  "claude-opus-4-8",
@@ -72,6 +73,7 @@ export interface ClaudeManagedModelPricing extends ClaudeManagedTokenPricing {
72
73
  * - claude-mythos-5: $10 / $50 / $1.00 / $12.50 (limited availability, verified 2026-06-10)
73
74
  * - claude-opus-5-5: $4 / $20 / $0.20 / $5.00 (verified 2026-09-22)
74
75
  * - claude-opus-5: $5 / $25 / $0.50 / $6.25 (verified 2026-07-25)
76
+ * - claude-sonnet-5-5: $2 / $10 / $0.20 / $2.50 (verified 2026-09-28)
75
77
  * - claude-sonnet-5: $2 / $10 / $0.20 / $2.50 (standard rate, verified 2026-08-29)
76
78
  * - claude-sonnet-4-6: $3 / $15 / $0.30 / $3.75 (in / out / cache-read / cache-write)
77
79
  * - claude-opus-4-8: $5 / $25 / $0.50 / $6.25 (verified 2026-05-28)
@@ -116,6 +118,12 @@ export const CLAUDE_MANAGED_MODEL_PRICING: Record<ClaudeManagedModel, ClaudeMana
116
118
  cacheReadPerMillion: 1.0,
117
119
  cacheWritePerMillion: 12.5,
118
120
  },
121
+ "claude-sonnet-5-5": {
122
+ inputPerMillion: 2.0,
123
+ outputPerMillion: 10.0,
124
+ cacheReadPerMillion: 0.2,
125
+ cacheWritePerMillion: 2.5,
126
+ },
119
127
  "claude-sonnet-5": {
120
128
  inputPerMillion: 2.0,
121
129
  outputPerMillion: 10.0,
@@ -21,32 +21,121 @@
21
21
  * instructions.
22
22
  * - Otherwise, write just the block.
23
23
  * - Mark `createdFresh: true` so cleanup removes the file entirely.
24
+ * - `AGENTS.md` is a symlink (many repos ship `AGENTS.md -> CLAUDE.md`):
25
+ * never write through it. Swap in a real file holding the block plus the
26
+ * link target's contents, and restore the symlink on cleanup. The target is
27
+ * only read, and edits only carried back to it, when its real path is a
28
+ * regular file inside the cwd. A crash-recovery record (cwd + link target)
29
+ * is kept OUTSIDE the repo, so the next session can restore the link if
30
+ * this one crashed before cleanup. Repo content never names a path.
24
31
  * - Existing `AGENTS.md` already contains the block: replace the block with
25
32
  * the fresh contents.
26
33
  * - Existing `AGENTS.md` without the block: prepend the block.
27
34
  *
28
35
  * Cleanup mirrors the creation logic — if we created the file fresh, delete
29
- * it; otherwise re-read the current AGENTS.md and strip just the managed
30
- * block so anything the agent appended during the session is preserved.
36
+ * it; if we swapped out a symlink, restore it; otherwise re-read the current
37
+ * AGENTS.md and strip just the managed block so anything the agent appended
38
+ * during the session is preserved.
31
39
  *
32
40
  * The helper is deliberately isolated from the adapter so it can be
33
41
  * unit-tested without pulling in the Codex SDK.
34
42
  */
35
43
 
36
- import { join } from "node:path";
44
+ import { lstat, mkdir, readlink, realpath, symlink, unlink } from "node:fs/promises";
45
+ import { homedir } from "node:os";
46
+ import { dirname, join, resolve, sep } from "node:path";
37
47
 
38
48
  const BLOCK_OPEN = "<swarm_system_prompt>";
39
49
  const BLOCK_CLOSE = "</swarm_system_prompt>";
40
50
  const BLOCK_REGEX = /<swarm_system_prompt>[\s\S]*?<\/swarm_system_prompt>\n?/;
51
+ const DEFAULT_STATE_DIR = join(homedir(), ".cache", "agent-swarm", "codex-agents-md");
41
52
 
42
53
  export interface CodexAgentsMdHandle {
43
54
  cleanup(): Promise<void>;
44
55
  }
45
56
 
57
+ export interface CodexAgentsMdOptions {
58
+ /** Where symlink crash-recovery records live. Must be outside any repo. */
59
+ stateDir?: string;
60
+ }
61
+
62
+ interface SymlinkRecord {
63
+ cwd: string;
64
+ linkTarget: string;
65
+ }
66
+
46
67
  const NOOP_HANDLE: CodexAgentsMdHandle = {
47
68
  cleanup: async () => {},
48
69
  };
49
70
 
71
+ async function readIfExists(path: string): Promise<string | null> {
72
+ const file = Bun.file(path);
73
+ return (await file.exists()) ? await file.text() : null;
74
+ }
75
+
76
+ function recordPathFor(stateDir: string, realCwd: string): string {
77
+ const key = new Bun.CryptoHasher("sha256").update(realCwd).digest("hex");
78
+ return join(stateDir, `${key}.json`);
79
+ }
80
+
81
+ async function readRecord(recordPath: string, realCwd: string): Promise<SymlinkRecord | null> {
82
+ try {
83
+ const record = JSON.parse(await Bun.file(recordPath).text()) as Partial<SymlinkRecord>;
84
+ if (record.cwd === realCwd && typeof record.linkTarget === "string") {
85
+ return { cwd: record.cwd, linkTarget: record.linkTarget };
86
+ }
87
+ } catch {
88
+ // Missing or unreadable record: nothing to recover.
89
+ }
90
+ return null;
91
+ }
92
+
93
+ /**
94
+ * Real path of the symlink target when it is a regular file inside the cwd,
95
+ * otherwise null. Out-of-tree and dangling targets are never read or written.
96
+ */
97
+ async function inTreeTarget(
98
+ realCwd: string,
99
+ agentsMdPath: string,
100
+ linkTarget: string,
101
+ ): Promise<string | null> {
102
+ const target = await realpath(resolve(dirname(agentsMdPath), linkTarget)).catch(() => null);
103
+ const prefix = realCwd.endsWith(sep) ? realCwd : `${realCwd}${sep}`;
104
+ if (target === null || !target.startsWith(prefix)) {
105
+ return null;
106
+ }
107
+ const stat = await lstat(target).catch(() => null);
108
+ return stat?.isFile() ? target : null;
109
+ }
110
+
111
+ /**
112
+ * Put the `AGENTS.md -> linkTarget` symlink back in place of our swapped-in
113
+ * real file. Edits the agent made to AGENTS.md during the session would have
114
+ * landed in the link target had the link been in place, so carry them over
115
+ * (block stripped) when the target is in-tree and its contents differ.
116
+ */
117
+ async function restoreSymlink(
118
+ agentsMdPath: string,
119
+ realCwd: string,
120
+ linkTarget: string,
121
+ ): Promise<void> {
122
+ const stat = await lstat(agentsMdPath).catch(() => null);
123
+ const current = stat?.isFile() ? await Bun.file(agentsMdPath).text() : null;
124
+ await unlink(agentsMdPath).catch(() => {});
125
+ await symlink(linkTarget, agentsMdPath);
126
+ if (current === null) {
127
+ return;
128
+ }
129
+ const target = await inTreeTarget(realCwd, agentsMdPath, linkTarget);
130
+ if (target === null) {
131
+ return;
132
+ }
133
+ const stripped = current.replace(BLOCK_REGEX, "");
134
+ if (stripped !== (await Bun.file(target).text())) {
135
+ await Bun.write(target, stripped);
136
+ }
137
+ }
138
+
50
139
  /**
51
140
  * Write (or refresh) a managed `<swarm_system_prompt>` block inside
52
141
  * `${cwd}/AGENTS.md`. Returns a handle whose `cleanup()` reverses the edit.
@@ -56,6 +145,7 @@ const NOOP_HANDLE: CodexAgentsMdHandle = {
56
145
  export async function writeCodexAgentsMd(
57
146
  cwd: string | undefined,
58
147
  systemPrompt: string | undefined,
148
+ options: CodexAgentsMdOptions = {},
59
149
  ): Promise<CodexAgentsMdHandle> {
60
150
  if (!cwd || !systemPrompt) {
61
151
  return NOOP_HANDLE;
@@ -63,33 +153,66 @@ export async function writeCodexAgentsMd(
63
153
 
64
154
  const agentsMdPath = join(cwd, "AGENTS.md");
65
155
  const claudeMdPath = join(cwd, "CLAUDE.md");
156
+ const stateDir = options.stateDir ?? DEFAULT_STATE_DIR;
157
+ const realCwd = await realpath(cwd).catch(() => resolve(cwd));
158
+ const recordPath = recordPathFor(stateDir, realCwd);
66
159
  const block = `${BLOCK_OPEN}\n${systemPrompt}\n${BLOCK_CLOSE}`;
67
160
 
68
- const agentsMdFile = Bun.file(agentsMdPath);
69
- const existingAgentsMdExists = await agentsMdFile.exists();
161
+ let stat = await lstat(agentsMdPath).catch(() => null);
162
+
163
+ // A previous session swapped a symlink out and crashed before cleanup. Only
164
+ // our own out-of-repo record can trigger a restore, and only while AGENTS.md
165
+ // is still the real file holding our block.
166
+ const record = await readRecord(recordPath, realCwd);
167
+ if (record) {
168
+ if (stat?.isFile() && BLOCK_REGEX.test(await Bun.file(agentsMdPath).text())) {
169
+ await restoreSymlink(agentsMdPath, realCwd, record.linkTarget);
170
+ stat = await lstat(agentsMdPath);
171
+ }
172
+ await unlink(recordPath).catch(() => {});
173
+ }
174
+
175
+ if (stat?.isSymbolicLink()) {
176
+ const linkTarget = await readlink(agentsMdPath);
177
+ const target = await inTreeTarget(realCwd, agentsMdPath, linkTarget);
178
+ const targetContent = target !== null ? await Bun.file(target).text() : "";
179
+ // Record before swapping, so a crash at any later point stays recoverable.
180
+ await mkdir(stateDir, { recursive: true });
181
+ await Bun.write(recordPath, JSON.stringify({ cwd: realCwd, linkTarget }));
182
+ // Never write through the link: replace it with a real file for the session.
183
+ await unlink(agentsMdPath);
184
+ await Bun.write(agentsMdPath, `${block}\n${targetContent.replace(BLOCK_REGEX, "")}`);
185
+
186
+ return {
187
+ async cleanup(): Promise<void> {
188
+ try {
189
+ await restoreSymlink(agentsMdPath, realCwd, linkTarget);
190
+ await unlink(recordPath).catch(() => {});
191
+ } catch {
192
+ // Cleanup is best-effort; the record lets the next session finish
193
+ // the restore.
194
+ }
195
+ },
196
+ };
197
+ }
70
198
 
71
199
  let createdFresh = false;
72
200
  let newContent: string;
73
201
 
74
- if (!existingAgentsMdExists) {
202
+ if (!stat) {
75
203
  // No AGENTS.md yet — prefer CLAUDE.md content as a base if present.
76
- const claudeMdFile = Bun.file(claudeMdPath);
77
- const claudeMdExists = await claudeMdFile.exists();
78
- if (claudeMdExists) {
79
- const claudeContent = await claudeMdFile.text();
80
- newContent = `${block}\n\n${claudeContent}`;
81
- } else {
82
- newContent = `${block}\n`;
83
- }
204
+ const claudeContent = await readIfExists(claudeMdPath);
205
+ newContent = claudeContent !== null ? `${block}\n\n${claudeContent}` : `${block}\n`;
84
206
  createdFresh = true;
85
207
  } else {
86
- const existingContent = await agentsMdFile.text();
208
+ const existingContent = await Bun.file(agentsMdPath).text();
87
209
  if (BLOCK_REGEX.test(existingContent)) {
88
210
  // Replace the stale block in place.
89
211
  newContent = existingContent.replace(BLOCK_REGEX, `${block}\n`);
90
212
  } else {
91
- // Prepend the block, keeping existing content intact.
92
- newContent = `${block}\n\n${existingContent}`;
213
+ // Prepend the block with a single newline so cleanup's strip restores
214
+ // the original bytes exactly.
215
+ newContent = `${block}\n${existingContent}`;
93
216
  }
94
217
  }
95
218