@desplega.ai/agent-swarm 1.172.0 → 1.173.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 (214) hide show
  1. package/dist/{acp-adapter-fpecx1g1.js → acp-adapter-ccm9xrep.js} +8 -8
  2. package/dist/{actions-pw87a1a8.js → actions-sgeyv08k.js} +44 -19
  3. package/dist/{amp-adapter-8jem2bc0.js → amp-adapter-gpxp3kc9.js} +1 -1
  4. package/dist/{anthropic-messages-8ee2f22q.js → anthropic-messages-r6czkjv1.js} +7 -7
  5. package/dist/{app-wmf0m843.js → app-km9qnv2f.js} +6 -4
  6. package/dist/{assistant-emp1e108.js → assistant-5r8tcvvd.js} +44 -22
  7. package/dist/{boot-reembed-4ymn9atn.js → boot-reembed-16rv9843.js} +4 -3
  8. package/dist/{boot-reembed-1de4f4aj.js → boot-reembed-swwzdgd7.js} +4 -3
  9. package/dist/{boot-scrub-sweep-adhk1s8c.js → boot-scrub-sweep-zkr6vkqb.js} +5 -4
  10. package/dist/{boot-seeds-8q637aew.js → boot-seeds-d8sp49t2.js} +7 -6
  11. package/dist/{claude-adapter-t26cv8a4.js → claude-adapter-yzzzh8jp.js} +9 -9
  12. package/dist/{claude-sdk-session-gsfsh662.js → claude-sdk-session-4whxere4.js} +9458 -9164
  13. package/dist/{cli-g4f5pj26.js → cli-0a7mm3zp.js} +92 -10
  14. package/dist/{cli-pj6x9qce.js → cli-0kxf81jj.js} +1 -1
  15. package/dist/{cli-kb4ehzx5.js → cli-1w0vj3gw.js} +4 -4
  16. package/dist/{cli-e0a9sk00.js → cli-3nxdmjrc.js} +1 -1
  17. package/dist/{cli-6jb2rf77.js → cli-42qhyvwx.js} +3 -3
  18. package/dist/{cli-esnpehf5.js → cli-5er5gjbm.js} +4 -4
  19. package/dist/{cli-04emfe2q.js → cli-5wpw4rab.js} +1 -1
  20. package/dist/{cli-aspcaef3.js → cli-6r2f7edk.js} +4 -4
  21. package/dist/{cli-xczpdp9e.js → cli-6thk4jz3.js} +1 -1
  22. package/dist/{cli-j6kpj9az.js → cli-7a93cv8c.js} +1458 -48
  23. package/dist/{cli-k46r9hmk.js → cli-94b48jhk.js} +2 -2
  24. package/dist/{cli-gp1rtyvk.js → cli-9xf7fdtp.js} +2 -2
  25. package/dist/{cli-qsztpfry.js → cli-anzdk0z4.js} +1 -1
  26. package/dist/{cli-cceskcvt.js → cli-b07hr3bt.js} +8 -8
  27. package/dist/{cli-c2pq2rjb.js → cli-bv2etw2y.js} +2 -2
  28. package/dist/{cli-hwwvy36w.js → cli-c3rk1v0x.js} +3 -3
  29. package/dist/{cli-tgpfnm6r.js → cli-fmhj1ehj.js} +53 -13
  30. package/dist/{cli-r8smj8b8.js → cli-jpc4r224.js} +1 -1
  31. package/dist/{cli-4nz8p1p5.js → cli-m89t9jdf.js} +5 -5
  32. package/dist/{cli-bf7485pm.js → cli-na2pxe4f.js} +1 -1
  33. package/dist/{cli-prrt9fa0.js → cli-ne7smj1b.js} +4 -4
  34. package/dist/{cli-cf69s3ew.js → cli-nwrdx840.js} +2 -2
  35. package/dist/cli-p0sg24zp.js +16 -0
  36. package/dist/{cli-gj81e47t.js → cli-phcqp1b9.js} +1 -1
  37. package/dist/{cli-5rv1j9ga.js → cli-qm1448f9.js} +51 -5
  38. package/dist/{cli-8hzfb6xz.js → cli-r0byd61h.js} +7 -7
  39. package/dist/{cli-nyhb1r6x.js → cli-r4fqfpwf.js} +3 -3
  40. package/dist/{cli-1hgat6pa.js → cli-rfs4vddv.js} +5 -5
  41. package/dist/{cli-ztrv9y21.js → cli-rfz2ve55.js} +861 -246
  42. package/dist/{cli-pstq9888.js → cli-s8z6x4b4.js} +22 -21
  43. package/dist/{cli-j1pmc07z.js → cli-saqyage0.js} +92 -16
  44. package/dist/{cli-zxm9p3x4.js → cli-srqq3gc5.js} +1 -1
  45. package/dist/{cli-9xsmdpdt.js → cli-tcc4wd5d.js} +136 -42
  46. package/dist/cli-v3zhnxsb.js +263 -0
  47. package/dist/{cli-388egyf6.js → cli-w1apjecb.js} +1 -1
  48. package/dist/{cli-g6t9texm.js → cli-wrh08zjd.js} +4 -4
  49. package/dist/{cli-8vs9trd4.js → cli-x119kdba.js} +1 -1
  50. package/dist/{cli-nv4zd0b9.js → cli-xc7p7s3g.js} +1 -1
  51. package/dist/cli.js +16 -16
  52. package/dist/{codex-adapter-m4tgdysy.js → codex-adapter-r3dn4fp5.js} +6 -6
  53. package/dist/{codex-session-runner-ymwzm6ea.js → codex-session-runner-fwt793h4.js} +6 -6
  54. package/dist/{commands-n0hshcvs.js → commands-z8fq9qna.js} +14 -4
  55. package/dist/{controllers-connect-y07cqcvy.js → controllers-connect-3mf2btyj.js} +21 -19
  56. package/dist/{db-4w7tmt1g.js → db-qfpq4bt7.js} +4 -3
  57. package/dist/{db-hkqvrdcb.js → db-yyhqqrbf.js} +5 -4
  58. package/dist/{dispatcher-5sd69y14.js → dispatcher-x0x7qfcq.js} +20 -18
  59. package/dist/{dsh-adapter-2h8m4cab.js → dsh-adapter-34etvb5m.js} +1 -1
  60. package/dist/external-channel-h1fjecwe.js +51 -0
  61. package/dist/{grok-adapter-8m9yqzbq.js → grok-adapter-e7mr287e.js} +9 -9
  62. package/dist/{handlers-s20xr10r.js → handlers-znb9n7d4.js} +23 -21
  63. package/dist/{heartbeat-3k31q4g2.js → heartbeat-bm4qkn8d.js} +22 -20
  64. package/dist/{hook-gdn6a2fn.js → hook-254r4mza.js} +7 -7
  65. package/dist/{hook-ygwerrdh.js → hook-xaa3q80s.js} +8 -8
  66. package/dist/{http-jdteeg81.js → http-3dgrgp8y.js} +135 -64
  67. package/dist/{index-6ksewd5y.js → index-2ybk7khy.js} +618 -120
  68. package/dist/{index-tsszt7g9.js → index-bdsqa799.js} +22 -20
  69. package/dist/{index-e0snym4t.js → index-fx3jqq1e.js} +20 -18
  70. package/dist/{index-vm3bm9t7.js → index-g2byveez.js} +20 -18
  71. package/dist/{index-ffqzqgwb.js → index-jd7sejm2.js} +4 -3
  72. package/dist/{keepalive-jekc896t.js → keepalive-6ywhpja1.js} +6 -5
  73. package/dist/{lead-4k9s9szx.js → lead-cw45n21s.js} +32 -30
  74. package/dist/{maintenance-shazch4n.js → maintenance-ss1a1ge9.js} +4 -3
  75. package/dist/node-c9we95xf.js +1 -1
  76. package/dist/{oauth-refresh-sweep-5gb36kn8.js → oauth-refresh-sweep-3c6f8vyy.js} +5 -4
  77. package/dist/{onboard-ydrbnvww.js → onboard-8hzdhc1k.js} +2 -2
  78. package/dist/{opencode-adapter-5zh0he20.js → opencode-adapter-qbchbcfz.js} +6 -6
  79. package/dist/{otel-impl-6pf5cv86.js → otel-impl-e6mnvc4v.js} +7 -2
  80. package/dist/{pi-mono-adapter-ef9wwx9m.js → pi-mono-adapter-3c9g9snh.js} +6 -6
  81. package/dist/{pricing-refresh-s52nsa5q.js → pricing-refresh-gedkjdw0.js} +6 -5
  82. package/dist/{rbac-roles-aq3msh4e.js → rbac-roles-dt4vh1yy.js} +4 -3
  83. package/dist/{rbac-roles-q5gb5e8v.js → rbac-roles-eszdg6pm.js} +5 -4
  84. package/dist/{render-v2-pd1rg54q.js → render-v2-907cy6sa.js} +6 -4
  85. package/dist/{resume-gw4a2c6v.js → resume-42stcaz0.js} +11 -9
  86. package/dist/{setup-jda1ksdh.js → setup-adbc1c10.js} +2 -2
  87. package/dist/{virtual-modules-b3ga2m65.js → virtual-modules-rtmt494q.js} +3 -3
  88. package/dist/{work-objects-tb7ycye8.js → work-objects-wpj8ck95.js} +28 -21
  89. package/dist/{worker-njjccz5d.js → worker-bzdra9rf.js} +32 -30
  90. package/openapi.json +423 -7
  91. package/package.json +4 -4
  92. package/src/apps/catalog.generated.json +1309 -53
  93. package/src/apps/page-validator.ts +93 -31
  94. package/src/azure-devops/api.ts +94 -18
  95. package/src/azure-devops/auth.ts +32 -0
  96. package/src/azure-devops/boards-sync.ts +708 -0
  97. package/src/azure-devops/handlers.ts +52 -8
  98. package/src/azure-devops/html.ts +26 -0
  99. package/src/azure-devops/index.ts +15 -0
  100. package/src/azure-devops/templates.ts +86 -0
  101. package/src/azure-devops/types.ts +65 -1
  102. package/src/be/db.ts +22 -6
  103. package/src/be/migrations/213_script_connections_sql.sql +79 -0
  104. package/src/be/script-connections.ts +47 -8
  105. package/src/be/seed-skills/bundled-files.generated.json +10 -0
  106. package/src/be/seed-skills/index.ts +3 -0
  107. package/src/be/sql-connections.ts +118 -0
  108. package/src/be/swarm-config-guard.ts +1 -0
  109. package/src/commands/profile-sync.ts +5 -3
  110. package/src/commands/runner.ts +6 -2
  111. package/src/http/mcp-oauth.ts +6 -13
  112. package/src/http/pages-public.ts +3 -2
  113. package/src/http/pages.ts +2 -1
  114. package/src/http/route-def.ts +7 -6
  115. package/src/http/script-connection-proxy.ts +51 -4
  116. package/src/http/script-connections.ts +5 -2
  117. package/src/http/session-cost-recompute.ts +12 -2
  118. package/src/http/session-data.ts +4 -0
  119. package/src/http/webhooks.ts +24 -1
  120. package/src/jira/adf.ts +3 -1
  121. package/src/kv-reserved-namespaces.ts +2 -0
  122. package/src/oauth/mcp-wrapper.ts +10 -6
  123. package/src/otel-impl.ts +2 -1
  124. package/src/prompts/base-prompt.ts +3 -3
  125. package/src/providers/acp-targets.ts +1 -1
  126. package/src/providers/amp-adapter.ts +1 -1
  127. package/src/providers/claude-adapter.ts +27 -2
  128. package/src/providers/claude-flag-settings.ts +121 -0
  129. package/src/providers/claude-sdk-session.ts +7 -0
  130. package/src/providers/dsh-adapter.ts +1 -1
  131. package/src/providers/opencode-adapter.ts +26 -12
  132. package/src/providers/opencode-billing.ts +86 -0
  133. package/src/providers/opencode-swarm-plugin.ts +20 -0
  134. package/src/providers/pricing-sources.md +4 -1
  135. package/src/providers/types.ts +5 -0
  136. package/src/scripts-runtime/api-client.ts +15 -1
  137. package/src/scripts-runtime/api-types.ts +9 -1
  138. package/src/scripts-runtime/ctx.ts +1 -1
  139. package/src/scripts-runtime/mcp-client.ts +30 -19
  140. package/src/slack/actions.ts +29 -1
  141. package/src/slack/app.ts +14 -2
  142. package/src/slack/assistant.ts +36 -2
  143. package/src/slack/channel-join.ts +3 -16
  144. package/src/slack/commands.ts +9 -1
  145. package/src/slack/external-channel-egress.ts +113 -0
  146. package/src/slack/external-channel.ts +309 -0
  147. package/src/slack/handlers.ts +61 -0
  148. package/src/slack/render-v2.ts +40 -47
  149. package/src/slack/work-objects.ts +5 -0
  150. package/src/tasks/context-key.ts +34 -1
  151. package/src/telemetry.ts +2 -1
  152. package/src/tests/apps-catalog-feedback.test.ts +310 -0
  153. package/src/tests/apps-catalog-inputs.test.ts +70 -0
  154. package/src/tests/apps-catalog-table.test.ts +113 -0
  155. package/src/tests/azure-devops-boards-sync.test.ts +584 -0
  156. package/src/tests/azure-devops-html.test.ts +13 -0
  157. package/src/tests/azure-devops-webhook-malformed.test.ts +4 -0
  158. package/src/tests/claude-adapter-binary.test.ts +139 -0
  159. package/src/tests/claude-flag-settings.test.ts +106 -0
  160. package/src/tests/claude-sdk-lifecycle.test.ts +142 -1
  161. package/src/tests/claude-sdk-process.fixture.ts +7 -0
  162. package/src/tests/context-key.test.ts +32 -0
  163. package/src/tests/deferred-task-wake.test.ts +52 -4
  164. package/src/tests/docs-index-footprint.test.js +3 -3
  165. package/src/tests/dsh-adapter.test.ts +1 -1
  166. package/src/tests/fixtures/azure-boards/workitem.commented.json +89 -0
  167. package/src/tests/fixtures/azure-boards/workitem.created.json +81 -0
  168. package/src/tests/fixtures/azure-boards/workitem.deleted.json +70 -0
  169. package/src/tests/fixtures/azure-boards/workitem.restored.json +82 -0
  170. package/src/tests/fixtures/azure-boards/workitem.updated.json +130 -0
  171. package/src/tests/fixtures/catalog-feedback-definition.json.txt +860 -0
  172. package/src/tests/fixtures/catalog-inputs-definition.json.txt +910 -0
  173. package/src/tests/fixtures/catalog-table-definition.json.txt +468 -0
  174. package/src/tests/fixtures/runner-identity-refresh.ts +3 -0
  175. package/src/tests/node-env-runtime.test.ts +98 -0
  176. package/src/tests/opencode-adapter.test.ts +132 -1
  177. package/src/tests/opencode-billing.test.ts +28 -0
  178. package/src/tests/runner-identity-refresh.test.ts +3 -0
  179. package/src/tests/script-connections-sql.test.ts +675 -0
  180. package/src/tests/script-connections.test.ts +5 -1
  181. package/src/tests/session-cost-recompute-unit.test.ts +48 -0
  182. package/src/tests/slack-channel-join.test.ts +4 -1
  183. package/src/tests/slack-external-channel-config.test.ts +65 -0
  184. package/src/tests/slack-external-channel-egress.test.ts +237 -0
  185. package/src/tests/slack-external-channel-guard.test.ts +280 -0
  186. package/src/tests/slack-external-channel-tools.test.ts +173 -0
  187. package/src/tests/slack-external-channel.test.ts +370 -0
  188. package/src/tests/slack-render-v2.test.ts +86 -10
  189. package/src/tests/spec-base-url.test.ts +16 -1
  190. package/src/tests/vendored-openapi.test.ts +16 -1
  191. package/src/tools/defer-task.ts +17 -0
  192. package/src/tools/slack-post.ts +6 -0
  193. package/src/tools/slack-read.ts +8 -0
  194. package/src/tools/slack-reply.ts +6 -0
  195. package/src/tools/slack-start-thread.ts +6 -0
  196. package/src/tools/slack-update.ts +6 -0
  197. package/src/tools/slack-upload-file.ts +6 -0
  198. package/src/utils/node-env.ts +24 -0
  199. package/templates/skills/apps/SKILL.md +742 -0
  200. package/templates/skills/apps/content.md +52 -21
  201. package/templates/skills/qa-loom/config.json +14 -0
  202. package/templates/skills/qa-loom/content.md +187 -0
  203. package/templates/skills/qa-loom/files/build-loom.mjs +221 -0
  204. package/templates/skills/qa-loom/files/run-qa.sh +57 -0
  205. package/templates/skills/slack-interaction/content.md +4 -0
  206. package/dist/{azure-openai-responses-ap1gwxgr.js → azure-openai-responses-ygfreq4t.js} +5 -5
  207. package/dist/{cli-4fnb6884.js → cli-h7k0xtry.js} +3 -3
  208. package/dist/{cli-5rqrj7q0.js → cli-pw7ebjdt.js} +0 -0
  209. package/dist/{google-generative-ai-7pavrbv7.js → google-generative-ai-szdxbpqg.js} +1 -1
  210. package/dist/{google-vertex-3f1jk41f.js → google-vertex-qzw3p36d.js} +1 -1
  211. package/dist/{openai-codex-responses-33qhkkdy.js → openai-codex-responses-3bk0grnr.js} +4 -4
  212. package/dist/{openai-completions-s88xm3ns.js → openai-completions-5d4jpakc.js} +9 -9
  213. package/dist/{openai-responses-7xd2f2e2.js → openai-responses-krb51f59.js} +5 -5
  214. package/dist/{pi-messages-xszk613n.js → pi-messages-x9gzynm1.js} +3 -3
@@ -63,6 +63,8 @@ type FakeHostOptions = {
63
63
  mcpStatuses?: Array<McpStatus | null>;
64
64
  /** Registered tool ids the plugin's tool lister returns. */
65
65
  tools?: string[];
66
+ /** Model responses handed to the plugin's `http.response` hook at startup. */
67
+ responses?: Response[];
66
68
  };
67
69
 
68
70
  /**
@@ -83,6 +85,11 @@ function makeFakeHost(events: OpenCodeEvent[], opts: FakeHostOptions = {}) {
83
85
  interrupts: [] as unknown[],
84
86
  closeCalls: 0,
85
87
  mcpListCalls: 0,
88
+ /** Session hooks the plugins registered, with their options. */
89
+ sessionHooks: {} as Record<
90
+ string,
91
+ { fn: (event: any) => unknown; options?: { providerID?: string } }
92
+ >,
86
93
  mcp: {
87
94
  list: async () => {
88
95
  const status = statuses[Math.min(host.mcpListCalls, statuses.length - 1)];
@@ -91,6 +98,7 @@ function makeFakeHost(events: OpenCodeEvent[], opts: FakeHostOptions = {}) {
91
98
  },
92
99
  },
93
100
  tools: opts.tools ?? ["read", "swarm_store-progress"],
101
+ responses: opts.responses ?? [],
94
102
  sessions: {
95
103
  create: async (input: unknown) => {
96
104
  host.created.push(input);
@@ -135,9 +143,20 @@ function installHost(host: FakeHost) {
135
143
  hook: async () => {},
136
144
  list: async () => host.tools.map((id) => ({ id })),
137
145
  },
138
- session: { hook: async () => {} },
146
+ session: {
147
+ hook: async (
148
+ name: string,
149
+ fn: (event: unknown) => unknown,
150
+ options?: { providerID?: string },
151
+ ) => {
152
+ host.sessionHooks[name] = { fn, options };
153
+ },
154
+ },
139
155
  });
140
156
  }
157
+ for (const response of host.responses) {
158
+ await host.sessionHooks["http.response"]?.fn({ response });
159
+ }
141
160
  return host;
142
161
  },
143
162
  },
@@ -445,6 +464,118 @@ describe("OpencodeSession — event mapping", () => {
445
464
  });
446
465
  });
447
466
 
467
+ describe("OpencodeSession — cost of an OpenRouter session", () => {
468
+ // Task 06c031dd (opencode 2.0.28, openrouter/deepseek/deepseek-v4.1-flash),
469
+ // verbatim from session_logs: a title call, then three agent steps.
470
+ // `session.usage.updated` is cumulative; `output` excludes reasoning.
471
+ const deepseek = testConfig({ model: "openrouter/deepseek/deepseek-v4.1-flash" });
472
+ const usage = (cost: number, input: number, output: number, reasoning: number, read: number) =>
473
+ ev("session.usage.updated", {
474
+ sessionID: SESSION_ID,
475
+ cost,
476
+ tokens: { input, output, reasoning, cache: { read, write: 0 } },
477
+ });
478
+ const step = (cost: number, input: number, output: number, reasoning: number, read: number) =>
479
+ ev("session.step.ended", {
480
+ sessionID: SESSION_ID,
481
+ finish: "tool-calls",
482
+ cost,
483
+ tokens: { input, output, reasoning, cache: { read, write: 0 } },
484
+ });
485
+ const v2Events = [
486
+ usage(0.00010640000000000001, 1019, 9, 0, 0),
487
+ step(0.00023894900000000002, 51770, 53, 26, 0),
488
+ usage(0.000345349, 52789, 62, 26, 0),
489
+ step(0.0005280831000000001, 12963, 102, 7, 51840),
490
+ usage(0.0008734321000000001, 65752, 164, 33, 51840),
491
+ step(0.0005287785, 165, 15, 0, 64896),
492
+ usage(0.0014022106, 65917, 179, 33, 116736),
493
+ succeeded,
494
+ ];
495
+
496
+ // OpenRouter's final SSE chunk for deepseek/deepseek-v4.1-flash, served by
497
+ // Together (live probe, 2026-10-14): `usage.cost` is the billed USD.
498
+ const openRouterSse = (cost: number, cached: number) =>
499
+ new Response(
500
+ [
501
+ ": OPENROUTER PROCESSING",
502
+ "",
503
+ `data: ${JSON.stringify({ id: "gen-1", choices: [{ index: 0, delta: { content: "pong" } }] })}`,
504
+ "",
505
+ `data: ${JSON.stringify({
506
+ id: "gen-1",
507
+ provider: "Together",
508
+ model: "deepseek/deepseek-v4.1-flash",
509
+ choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
510
+ usage: {
511
+ prompt_tokens: 26545,
512
+ completion_tokens: 15,
513
+ total_tokens: 26560,
514
+ cost,
515
+ is_byok: false,
516
+ prompt_tokens_details: { cached_tokens: cached, cache_write_tokens: 0 },
517
+ completion_tokens_details: { reasoning_tokens: 11 },
518
+ },
519
+ })}`,
520
+ "",
521
+ "data: [DONE]",
522
+ "",
523
+ ].join("\n"),
524
+ { headers: { "content-type": "text/event-stream" } },
525
+ );
526
+
527
+ test("reports v2 tokens with reasoning billed as output", async () => {
528
+ const { result } = await runSession(v2Events, deepseek);
529
+ expect(result.cost).toMatchObject({
530
+ totalCostUsd: 0.0014022106,
531
+ inputTokens: 65917,
532
+ outputTokens: 212,
533
+ reasoningOutputTokens: 33,
534
+ cacheReadTokens: 116736,
535
+ cacheWriteTokens: 0,
536
+ numTurns: 3,
537
+ });
538
+ expect(result.cost?.harnessCostBilled).toBeUndefined();
539
+ });
540
+
541
+ test("reports OpenRouter's billed cost when every response carried one", async () => {
542
+ const { result } = await runSession(v2Events, deepseek, {
543
+ responses: [
544
+ openRouterSse(0.0079815, 0),
545
+ openRouterSse(0.000191676, 26496),
546
+ new Response('{"error":{"message":"rate limited"}}', { status: 429 }),
547
+ ],
548
+ });
549
+ expect(result.cost?.harnessCostBilled).toBe(true);
550
+ expect(result.cost?.totalCostUsd).toBeCloseTo(0.0079815 + 0.000191676, 12);
551
+ expect(result.cost?.inputTokens).toBe(65917);
552
+ });
553
+
554
+ test("falls back to OpenCode's estimate when a response has no billed cost", async () => {
555
+ const { result } = await runSession(v2Events, deepseek, {
556
+ responses: [openRouterSse(0.0079815, 0), new Response("data: [DONE]\n\n")],
557
+ });
558
+ expect(result.cost?.harnessCostBilled).toBeUndefined();
559
+ expect(result.cost?.totalCostUsd).toBe(0.0014022106);
560
+ });
561
+
562
+ test("hooks only OpenRouter responses, and only for an OpenRouter model", async () => {
563
+ const openrouter = await runSession([succeeded], deepseek);
564
+ expect(openrouter.host.sessionHooks["http.response"]?.options).toEqual({
565
+ providerID: "openrouter",
566
+ });
567
+ const direct = await runSession([succeeded], testConfig({ model: "anthropic/claude-haiku" }));
568
+ expect(direct.host.sessionHooks["http.response"]).toBeUndefined();
569
+ });
570
+
571
+ test("skips title generation", async () => {
572
+ const { host } = await runSession([succeeded]);
573
+ const event: { result?: string } = {};
574
+ await host.sessionHooks.title?.fn(event);
575
+ expect(event.result).toBe("swarm task task-1");
576
+ });
577
+ });
578
+
448
579
  describe("OpencodeSession — persisted rows render in the dashboard", () => {
449
580
  test("text, tools and errors render once each, in order", async () => {
450
581
  const text = (ordinal: number, value: string, messageID = "msg_1") => [
@@ -0,0 +1,28 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { OpenRouterBilling, parseOpenRouterBilledCost } from "../providers/opencode-billing";
3
+
4
+ describe("parseOpenRouterBilledCost", () => {
5
+ test("reads usage.cost from a non-streamed JSON body", () => {
6
+ expect(parseOpenRouterBilledCost('{"id":"gen-1","usage":{"cost":0.0042}}')).toBe(0.0042);
7
+ });
8
+
9
+ test("a BYOK cost is only OpenRouter's fee, so it is not the bill", () => {
10
+ expect(parseOpenRouterBilledCost('{"usage":{"cost":0.0001,"is_byok":true}}')).toBeNull();
11
+ });
12
+
13
+ test("a stream without usage has no billed cost", () => {
14
+ expect(parseOpenRouterBilledCost('data: {"choices":[]}\n\ndata: [DONE]\n\n')).toBeNull();
15
+ });
16
+ });
17
+
18
+ describe("OpenRouterBilling", () => {
19
+ test("no observed request reports nothing", async () => {
20
+ expect(await new OpenRouterBilling().settle(10)).toBeNull();
21
+ });
22
+
23
+ test("a body still streaming at the deadline leaves the bill incomplete", async () => {
24
+ const billing = new OpenRouterBilling();
25
+ billing.observe(new Response(new ReadableStream({ start() {} })));
26
+ expect(await billing.settle(10)).toEqual({ costUsd: 0, complete: false });
27
+ });
28
+ });
@@ -13,6 +13,7 @@ interface FixtureResult {
13
13
  tools: string;
14
14
  baselineAtFirstSpawn: string;
15
15
  baselineAtSecondSpawn: string;
16
+ workspaceClaudeMdAtFirstSpawn: string;
16
17
  }
17
18
 
18
19
  async function runFixture(scenario: string): Promise<FixtureResult> {
@@ -63,6 +64,8 @@ describe("runner per-task identity refresh", () => {
63
64
  expect(result.captures[1]).toContain("Identity changed after task A");
64
65
  expect(result.captures[0]).toContain("Notes before boot refresh");
65
66
  expect(result.captures[1]).not.toContain("Notes before boot refresh");
67
+ // A non-Claude (pi) boot still materializes the workspace CLAUDE.md it reads and syncs.
68
+ expect(result.workspaceClaudeMdAtFirstSpawn).toBe("Notes before boot refresh");
66
69
  expect(result.captures[1]).toContain("Additional fixture instructions");
67
70
  },
68
71
  CHILD_PROCESS_TEST_BUDGET_MS,