@desplega.ai/agent-swarm 2.2.0 → 2.4.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 (266) hide show
  1. package/README.md +1 -0
  2. package/dist/{acp-adapter-k0vwfyvn.js → acp-adapter-kedstxx6.js} +6 -6
  3. package/dist/{actions-w4pnb8vz.js → actions-vget6r1k.js} +18 -16
  4. package/dist/{amp-adapter-0cmxb2t5.js → amp-adapter-xeb5mktv.js} +2 -2
  5. package/dist/{app-zneem7g9.js → app-mqap8t8m.js} +3 -3
  6. package/dist/{assistant-wbzd2rnr.js → assistant-h0gv97mv.js} +20 -18
  7. package/dist/{boot-reembed-4kx69j3g.js → boot-reembed-qr8a894z.js} +1 -1
  8. package/dist/{boot-reembed-db0wwgy1.js → boot-reembed-rj85jjhw.js} +1 -1
  9. package/dist/{boot-scrub-sweep-tfv4d3mm.js → boot-scrub-sweep-eyfvn6v9.js} +3 -3
  10. package/dist/{boot-seeds-w2hmkxn5.js → boot-seeds-xs9a8dx1.js} +4 -4
  11. package/dist/{claude-adapter-5qrc2paq.js → claude-adapter-bm23cye7.js} +5 -5
  12. package/dist/{claude-managed-adapter-n7ahjyx6.js → claude-managed-adapter-1mwrsm9d.js} +2 -2
  13. package/dist/{claude-managed-setup-7ra32em4.js → claude-managed-setup-gkfgks4j.js} +1 -1
  14. package/dist/{claude-sdk-session-cd02gc76.js → claude-sdk-session-apb4ypz3.js} +5 -5
  15. package/dist/cli-02vhhssk.js +848 -0
  16. package/dist/{cli-4ync7m60.js → cli-0ebey9hg.js} +1 -1
  17. package/dist/{cli-665j3feb.js → cli-0xk505e8.js} +1 -1
  18. package/dist/{cli-8c1m9mhy.js → cli-1cevz46c.js} +5 -5
  19. package/dist/{cli-gzvb3d4a.js → cli-1jx0srjt.js} +3 -3
  20. package/dist/{cli-zj4d6afs.js → cli-1pa3rz69.js} +21 -6
  21. package/dist/{cli-vmbakbq0.js → cli-2rwet3r7.js} +1 -1
  22. package/dist/{cli-azs9bhc5.js → cli-3wjs22x3.js} +2 -2
  23. package/dist/{cli-tba8fxf9.js → cli-4fhyq711.js} +45 -36
  24. package/dist/{cli-ak4c12mh.js → cli-5ff2sjyt.js} +1 -1
  25. package/dist/{cli-6q7pyd3k.js → cli-5s5hx57y.js} +5 -3
  26. package/dist/{cli-h23mr2s6.js → cli-6ftrrwm1.js} +9 -371
  27. package/dist/{cli-9ac0mkg5.js → cli-8b9wvjps.js} +1 -1
  28. package/dist/cli-8cxkw1f5.js +382 -0
  29. package/dist/{cli-xhgtay7w.js → cli-8rwvksbc.js} +2 -2
  30. package/dist/{cli-e09hj50q.js → cli-8wkb09q5.js} +11 -11
  31. package/dist/{cli-ksrphveb.js → cli-a1esmmee.js} +14 -2
  32. package/dist/{cli-xnp2vkx7.js → cli-avmqhatn.js} +7 -7
  33. package/dist/{cli-xx13gqf3.js → cli-b80a1100.js} +1 -1
  34. package/dist/{cli-z1hh99j0.js → cli-bfc3k1x5.js} +3 -3
  35. package/dist/{cli-0k9rcwfy.js → cli-bhwa8rzc.js} +33 -15
  36. package/dist/{cli-8cvvytad.js → cli-bn26xbcs.js} +2 -2
  37. package/dist/{cli-k28p6xh7.js → cli-c8fwpgqb.js} +1 -1
  38. package/dist/{cli-rc3r0bnh.js → cli-ccn0eybf.js} +1 -1
  39. package/dist/{cli-9td5ss65.js → cli-dsgj3e7a.js} +2 -2
  40. package/dist/{cli-jrk8mj0t.js → cli-ecshf504.js} +2 -2
  41. package/dist/{cli-raejy0mp.js → cli-eqgfs202.js} +1 -1
  42. package/dist/{cli-ppapy34w.js → cli-f3nx1h8z.js} +2 -2
  43. package/dist/{cli-c9vrwcpb.js → cli-ffbrkh1r.js} +74 -35
  44. package/dist/{cli-mnry16kh.js → cli-kk2r80n5.js} +4 -4
  45. package/dist/{cli-c0dpzwnx.js → cli-kz116kvw.js} +3 -3
  46. package/dist/{cli-e5y285rg.js → cli-mvg2hf8p.js} +4 -4
  47. package/dist/{cli-2jm992rm.js → cli-mz1ezzzh.js} +2 -2
  48. package/dist/{cli-dw0njd9q.js → cli-nme7wery.js} +6 -6
  49. package/dist/{cli-mvsfbbgq.js → cli-nrrg0fvy.js} +18 -4
  50. package/dist/{cli-8jtrekpy.js → cli-p1m2b6gc.js} +7 -7
  51. package/dist/{cli-4c7k75rq.js → cli-rg7ntypm.js} +378 -84
  52. package/dist/{cli-z3dk189k.js → cli-rsd7ga7v.js} +10 -8
  53. package/dist/{cli-etvn5ej2.js → cli-sppc0cmw.js} +1 -1
  54. package/dist/{cli-8w65vwee.js → cli-tg9ksbn8.js} +1 -1
  55. package/dist/{cli-th8krqbf.js → cli-tgvpmnwz.js} +2 -2
  56. package/dist/{cli-hwc4a9xm.js → cli-tj4j2ts4.js} +6 -6
  57. package/dist/{cli-jhhb2v9n.js → cli-ttaz6c7m.js} +2 -2
  58. package/dist/{cli-w7z94e6c.js → cli-tx00c0h6.js} +4 -2
  59. package/dist/{cli-4g629sgr.js → cli-wrv09v7q.js} +20 -1
  60. package/dist/{cli-cby329ny.js → cli-wyrr38xm.js} +2 -2
  61. package/dist/{cli-5ks6x36q.js → cli-x1p5dhq2.js} +3 -3
  62. package/dist/{cli-bddxamg7.js → cli-x719j3vs.js} +220 -629
  63. package/dist/{cli-ztcxv5nv.js → cli-xfe63zx3.js} +1 -1
  64. package/dist/{cli-q6gw45wj.js → cli-ywyrffff.js} +1 -1
  65. package/dist/{cli-p46xdr1m.js → cli-zvze9rbm.js} +1 -1
  66. package/dist/cli.js +15 -15
  67. package/dist/{codex-adapter-m0fx3v3s.js → codex-adapter-pp2gq8gr.js} +3 -3
  68. package/dist/{codex-hook-x6tm6y5m.js → codex-hook-qwzccd64.js} +1 -1
  69. package/dist/{codex-session-runner-j9m2gfyy.js → codex-session-runner-0vzn09t8.js} +3 -3
  70. package/dist/{commands-ktnqwrv4.js → commands-psqhkttb.js} +2 -2
  71. package/dist/{controller-nex2he8v.js → controller-t20vz2k8.js} +2 -2
  72. package/dist/{controllers-connect-6yhwqv9v.js → controllers-connect-gkxmqw9d.js} +24 -20
  73. package/dist/{cursor-adapter-02s5ekva.js → cursor-adapter-3v542qe5.js} +1 -1
  74. package/dist/{db-jvr86v7f.js → db-2xbyenjr.js} +2 -2
  75. package/dist/{db-19tw9r5s.js → db-nc1rd869.js} +3 -2
  76. package/dist/{dispatcher-d3mnsg2y.js → dispatcher-ewceb8ts.js} +17 -15
  77. package/dist/{dsh-adapter-63rvej88.js → dsh-adapter-75xwh78t.js} +1 -1
  78. package/dist/{easter-egg-3d-6hg2r942.js → easter-egg-3d-xsc47jwf.js} +1 -1
  79. package/dist/{execute-bcqc69a5.js → execute-gzmw1mnw.js} +1 -1
  80. package/dist/{external-channel-44cwer32.js → external-channel-a5dsmxn4.js} +2 -2
  81. package/dist/{google-generative-ai-phpmwxvg.js → google-generative-ai-bmq8gkf3.js} +1 -1
  82. package/dist/{google-vertex-es9ybgdy.js → google-vertex-b1rvp52s.js} +1 -1
  83. package/dist/{grok-adapter-ysp907f6.js → grok-adapter-vj88xr23.js} +7 -7
  84. package/dist/handlers-zbs90bny.js +39 -0
  85. package/dist/{heartbeat-2etacsqq.js → heartbeat-t2m9r3wa.js} +20 -18
  86. package/dist/{hook-mzxxs5n2.js → hook-8femx1bg.js} +4 -4
  87. package/dist/{hook-3kvmax6d.js → hook-xa677f8r.js} +5 -5
  88. package/dist/{http-fn978m0g.js → http-xpmmq028.js} +940 -269
  89. package/dist/{index-e4tsa1h7.js → index-8pwc1jc8.js} +17 -15
  90. package/dist/{index-w2wr89xd.js → index-cdzyfrsp.js} +1 -1
  91. package/dist/{index-1r8hw4vd.js → index-dg2tmj99.js} +17 -15
  92. package/dist/index-nxa6me0q.js +41 -0
  93. package/dist/{index-039x2sag.js → index-s7ckdajq.js} +42 -14
  94. package/dist/{index-wgsw9xcz.js → index-syztqdt6.js} +1 -1
  95. package/dist/{index-x0q2a4f9.js → index-ysd45kgc.js} +1 -1
  96. package/dist/{index-g909y089.js → index-zgvqzjsw.js} +8 -8
  97. package/dist/{keepalive-d4nzs76x.js → keepalive-3nr4st3r.js} +3 -3
  98. package/dist/{lead-8h5n9k5f.js → lead-td8h2sfn.js} +33 -31
  99. package/dist/{maintenance-ea6jfw42.js → maintenance-verwpe2c.js} +1 -1
  100. package/dist/{oauth-refresh-sweep-3f25djq3.js → oauth-refresh-sweep-9q2t0y3q.js} +2 -2
  101. package/dist/{onboard-pvk7hdb2.js → onboard-aytqedmq.js} +2 -2
  102. package/dist/{opencode-adapter-eeesgfqk.js → opencode-adapter-f4hryjny.js} +4 -4
  103. package/dist/{otel-impl-qb7nhgdp.js → otel-impl-hckw4wt1.js} +1 -1
  104. package/dist/{pi-mono-adapter-zasp3ze3.js → pi-mono-adapter-hxby0ce1.js} +6 -6
  105. package/dist/{pricing-refresh-6ehwkcmp.js → pricing-refresh-5nvme3sm.js} +3 -3
  106. package/dist/{rbac-roles-km0f9cmm.js → rbac-roles-pd8c26m1.js} +5 -3
  107. package/dist/{rbac-roles-9648nh00.js → rbac-roles-vbpy21mg.js} +2 -2
  108. package/dist/{render-v2-xkze1wef.js → render-v2-7nqgvk2q.js} +3 -3
  109. package/dist/{resume-tq0bbvt5.js → resume-b6djcj3c.js} +6 -6
  110. package/dist/{runtime-ar8310ba.js → runtime-txrxc9za.js} +1 -1
  111. package/dist/session-access-fc3f9y12.js +25 -0
  112. package/dist/{setup-rnrazxvd.js → setup-n39szm6b.js} +1 -1
  113. package/dist/{virtual-modules-wzk6jf3a.js → virtual-modules-wyp2k6z3.js} +2 -2
  114. package/dist/{work-objects-e75q2rqs.js → work-objects-jem2fre0.js} +20 -18
  115. package/dist/{worker-4ww4hjef.js → worker-wr6xgfr2.js} +33 -31
  116. package/openapi.json +1011 -115
  117. package/package.json +2 -2
  118. package/src/apps/action-caller.ts +6 -0
  119. package/src/apps/script-types.ts +16 -3
  120. package/src/be/asset-key-audit.ts +4 -13
  121. package/src/be/db/active-sessions.ts +5 -0
  122. package/src/be/db/session-visibility.ts +29 -0
  123. package/src/be/db/tasks/read.ts +61 -3
  124. package/src/be/db/tasks/write.ts +15 -6
  125. package/src/be/db.ts +149 -66
  126. package/src/be/events.ts +11 -0
  127. package/src/be/migrations/227_inbox_item_state_last_read_at.sql +1 -0
  128. package/src/be/migrations/230_session_shares.sql +23 -0
  129. package/src/be/rbac-roles.ts +2 -0
  130. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +22 -4
  131. package/src/be/session-shares.ts +244 -0
  132. package/src/be/swarm-config-guard.ts +23 -2
  133. package/src/commands/runner.ts +11 -0
  134. package/src/heartbeat/heartbeat.ts +25 -7
  135. package/src/http/active-sessions.ts +13 -2
  136. package/src/http/agents.ts +10 -3
  137. package/src/http/all-routes.ts +1 -0
  138. package/src/http/api-keys.ts +3 -1
  139. package/src/http/approval-requests.ts +2 -1
  140. package/src/http/apps.ts +36 -11
  141. package/src/http/assets.ts +26 -4
  142. package/src/http/config.ts +3 -0
  143. package/src/http/context.ts +2 -0
  144. package/src/http/core.ts +39 -2
  145. package/src/http/events.ts +6 -2
  146. package/src/http/favorite-owner.ts +1 -1
  147. package/src/http/fs.ts +38 -3
  148. package/src/http/inbox-state.ts +58 -8
  149. package/src/http/index.ts +3 -1
  150. package/src/http/memory.ts +3 -1
  151. package/src/http/ranking.ts +367 -0
  152. package/src/http/realtime.ts +26 -21
  153. package/src/http/route-def.ts +77 -11
  154. package/src/http/script-runs.ts +2 -1
  155. package/src/http/scripts.ts +8 -0
  156. package/src/http/session-access.ts +179 -0
  157. package/src/http/session-data.ts +10 -2
  158. package/src/http/sessions.ts +212 -12
  159. package/src/http/stats.ts +7 -3
  160. package/src/http/tasks.ts +83 -7
  161. package/src/http/used-by.ts +11 -3
  162. package/src/kv-reserved-namespaces.ts +1 -0
  163. package/src/prompts/session-templates.ts +3 -1
  164. package/src/providers/amp-adapter.ts +1 -1
  165. package/src/ranking/telemetry-ranking.ts +147 -0
  166. package/src/ranking/usage.ts +57 -0
  167. package/src/rbac/admission.ts +22 -7
  168. package/src/rbac/legacy-policy.ts +2 -0
  169. package/src/rbac/permissions.ts +9 -0
  170. package/src/realtime/auth.ts +69 -0
  171. package/src/realtime/tickets.ts +4 -1
  172. package/src/realtime/transport.ts +77 -9
  173. package/src/slack/blocks.ts +18 -0
  174. package/src/slack/dm.ts +29 -0
  175. package/src/telemetry-contract/EVENTS.md +3 -0
  176. package/src/telemetry-contract/SOURCE.json +37 -4
  177. package/src/telemetry-contract/catalog.json +69 -0
  178. package/src/telemetry-contract/ranking/fixtures/expected.json +30 -0
  179. package/src/telemetry-contract/ranking/fixtures/invalid/bad-category.json +11 -0
  180. package/src/telemetry-contract/ranking/fixtures/invalid/bad-claim.json +11 -0
  181. package/src/telemetry-contract/ranking/fixtures/invalid/blank-description.json +11 -0
  182. package/src/telemetry-contract/ranking/fixtures/invalid/blank-name.json +11 -0
  183. package/src/telemetry-contract/ranking/fixtures/invalid/description-too-long.json +11 -0
  184. package/src/telemetry-contract/ranking/fixtures/invalid/domain-uppercase.json +11 -0
  185. package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-scheme.json +11 -0
  186. package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-www.json +11 -0
  187. package/src/telemetry-contract/ranking/fixtures/invalid/http-source-url.json +11 -0
  188. package/src/telemetry-contract/ranking/fixtures/invalid/missing-description.json +10 -0
  189. package/src/telemetry-contract/ranking/fixtures/invalid/missing-version.json +10 -0
  190. package/src/telemetry-contract/ranking/fixtures/invalid/name-not-string.json +11 -0
  191. package/src/telemetry-contract/ranking/fixtures/invalid/name-too-long.json +11 -0
  192. package/src/telemetry-contract/ranking/fixtures/invalid/not-object.json +1 -0
  193. package/src/telemetry-contract/ranking/fixtures/invalid/null.json +1 -0
  194. package/src/telemetry-contract/ranking/fixtures/invalid/other-with-subcategory.json +11 -0
  195. package/src/telemetry-contract/ranking/fixtures/invalid/oversize.json +1 -0
  196. package/src/telemetry-contract/ranking/fixtures/invalid/short-claim.json +11 -0
  197. package/src/telemetry-contract/ranking/fixtures/invalid/subcategory-other-category.json +11 -0
  198. package/src/telemetry-contract/ranking/fixtures/invalid/trailing-data.json +1 -0
  199. package/src/telemetry-contract/ranking/fixtures/invalid/unknown-field.json +12 -0
  200. package/src/telemetry-contract/ranking/fixtures/invalid/unsupported-version.json +11 -0
  201. package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-not-date.json +11 -0
  202. package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-offset.json +11 -0
  203. package/src/telemetry-contract/ranking/fixtures/invalid/wrong-case-field.json +11 -0
  204. package/src/telemetry-contract/ranking/fixtures/valid/category-without-subcategory.json +11 -0
  205. package/src/telemetry-contract/ranking/fixtures/valid/full.json +11 -0
  206. package/src/telemetry-contract/ranking/fixtures/valid/minimal.json +11 -0
  207. package/src/telemetry-contract/ranking/fixtures/valid/padded-text.json +11 -0
  208. package/src/telemetry-contract/ranking/fixtures/valid/unicode-name.json +11 -0
  209. package/src/telemetry-contract/ranking/listing.v1.schema.json +61 -0
  210. package/src/telemetry-contract/ranking/taxonomy.json +8 -0
  211. package/src/telemetry-contract/ranking-taxonomy.gen.ts +11 -0
  212. package/src/telemetry-contract/types.gen.ts +708 -1
  213. package/src/telemetry.ts +26 -2
  214. package/src/tests/apps-rbac.test.ts +189 -2
  215. package/src/tests/apps-script-types.test.ts +16 -3
  216. package/src/tests/asset-key-api.test.ts +128 -0
  217. package/src/tests/asset-key-invariants.test.ts +179 -3
  218. package/src/tests/asset-key-migration.test.ts +35 -0
  219. package/src/tests/check-pr-body.test.ts +23 -0
  220. package/src/tests/check-ranking-entry.test.ts +146 -0
  221. package/src/tests/db-extraction-contract.test.ts +1 -0
  222. package/src/tests/db-task-read-contract.test.ts +1 -0
  223. package/src/tests/docs-index-footprint.test.js +2 -2
  224. package/src/tests/heartbeat-checklist.test.ts +88 -0
  225. package/src/tests/heartbeat-jev-gate.test.ts +394 -10
  226. package/src/tests/inbox-state.test.ts +167 -0
  227. package/src/tests/kv-tool.test.ts +15 -0
  228. package/src/tests/org-identity.test.ts +44 -0
  229. package/src/tests/ranking-listing.test.ts +112 -0
  230. package/src/tests/ranking-route.test.ts +319 -0
  231. package/src/tests/ranking-usage.test.ts +50 -0
  232. package/src/tests/rbac-admission-e2e.test.ts +8 -0
  233. package/src/tests/rbac-admission.test.ts +38 -0
  234. package/src/tests/rbac-charact-http.test.ts +4 -3
  235. package/src/tests/rbac-engine.test.ts +2 -0
  236. package/src/tests/rbac-roles.test.ts +2 -0
  237. package/src/tests/realtime-room-surfaces.test.ts +3 -1
  238. package/src/tests/realtime-session-join-race.test.ts +142 -0
  239. package/src/tests/realtime-tickets.test.ts +24 -17
  240. package/src/tests/realtime-transport.test.ts +91 -0
  241. package/src/tests/script-connections.test.ts +2 -0
  242. package/src/tests/session-route-posture.test.ts +639 -0
  243. package/src/tests/session-sharing-routes.test.ts +282 -0
  244. package/src/tests/session-sharing.test.ts +526 -0
  245. package/src/tests/sessions-detail-batch.test.ts +7 -1
  246. package/src/tests/slack-tool-blocks.test.ts +101 -0
  247. package/src/tests/slack-update.test.ts +55 -0
  248. package/src/tests/slack-user-dm.test.ts +177 -0
  249. package/src/tests/used-by-route.test.ts +10 -0
  250. package/src/tools/slack-post.ts +18 -12
  251. package/src/tools/slack-read.ts +13 -1
  252. package/src/tools/slack-reply.ts +7 -10
  253. package/src/tools/slack-start-thread.ts +18 -11
  254. package/src/tools/slack-update.ts +2 -9
  255. package/src/types.ts +37 -0
  256. package/src/utils/org-identity.ts +33 -0
  257. package/templates/skills/apps/SKILL.md +17 -0
  258. package/templates/skills/apps/content.md +17 -0
  259. package/templates/skills/code-quality/content.md +2 -2
  260. package/templates/skills/pages/content.md +4 -0
  261. package/templates/skills/swarm-scripts/SKILL.md +6 -0
  262. package/templates/skills/swarm-scripts/content.md +6 -0
  263. package/dist/handlers-t5pbabwf.js +0 -37
  264. package/dist/index-qyjbb7br.js +0 -39
  265. package/dist/{cli-n5mrsg0f.js → cli-60cgh1rv.js} +3 -3
  266. /package/dist/{cli-j0v75y13.js → cli-y83zk5sb.js} +0 -0
@@ -9,6 +9,8 @@ import {
9
9
  createTaskExtended,
10
10
  createUser,
11
11
  createWorkflow,
12
+ deleteTask,
13
+ deleteTaskAttachment,
12
14
  getAssetKeyMappingByProvider,
13
15
  getDb,
14
16
  getDbClient,
@@ -166,6 +168,11 @@ describe("cross-entity asset namespace invariants", () => {
166
168
  ).sourceEntityType,
167
169
  ).toBe("task-attachment");
168
170
 
171
+ const lastUpdatedAt = "2026-01-01T00:00:00.000Z";
172
+ await getDbClient().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
173
+ lastUpdatedAt,
174
+ task.id,
175
+ ]);
169
176
  expect(
170
177
  await moveAssetKey({
171
178
  entityType: "task",
@@ -173,7 +180,8 @@ describe("cross-entity asset namespace invariants", () => {
173
180
  key: "shared/archive/",
174
181
  changedBy: userId,
175
182
  }),
176
- ).toBe(true);
183
+ ).toEqual({ movedTaskIds: [task.id] });
184
+ expect((await getTaskById(task.id))?.lastUpdatedAt).toBe(lastUpdatedAt);
177
185
  expect((await getTaskById(task.id))?.key).toBe("shared/archive/");
178
186
  const after = await getAssetKeyMappingByProvider({
179
187
  providerId: "agent-fs",
@@ -251,7 +259,7 @@ describe("cross-entity asset namespace invariants", () => {
251
259
  key: "shared/products/",
252
260
  changedBy: userId,
253
261
  }),
254
- ).toBe(true);
262
+ ).toEqual({});
255
263
  expect(
256
264
  await moveAssetKey({
257
265
  entityType: "script",
@@ -259,7 +267,7 @@ describe("cross-entity asset namespace invariants", () => {
259
267
  key: "shared/automation/",
260
268
  changedBy: userId,
261
269
  }),
262
- ).toBe(true);
270
+ ).toEqual({});
263
271
 
264
272
  expect(
265
273
  await getDbClient().get<{ key: string }>('SELECT "key" AS key FROM apps WHERE id = ?', [
@@ -330,6 +338,174 @@ describe("cross-entity asset namespace invariants", () => {
330
338
  expect(auditAssetKeys(getDb()).warningCount).toBe(0);
331
339
  });
332
340
 
341
+ test("shared provider files allow moves and follow only their source task", async () => {
342
+ const first = await createTaskExtended("first attachment owner", {
343
+ agentId,
344
+ key: "shared/first/",
345
+ });
346
+ const source = await createTaskExtended("source attachment owner", {
347
+ agentId,
348
+ key: "shared/source/",
349
+ });
350
+ const provider = {
351
+ providerId: "agent-fs",
352
+ providerOrgId: "shared-org",
353
+ providerDriveId: "shared-drive",
354
+ providerKey: "shared-file.md",
355
+ };
356
+ for (const task of [first, source]) {
357
+ await insertTaskAttachment({
358
+ taskId: task.id,
359
+ name: "shared-file.md",
360
+ kind: "agent-fs",
361
+ providerId: provider.providerId,
362
+ providerKey: provider.providerKey,
363
+ orgId: provider.providerOrgId,
364
+ driveId: provider.providerDriveId,
365
+ });
366
+ }
367
+ const mapping = (await getAssetKeyMappingByProvider(provider))!;
368
+ expect(mapping.key).toBe(source.key);
369
+ const historyBefore = await getDbClient().query(
370
+ "SELECT id FROM asset_key_history WHERE entity_id = ? ORDER BY id",
371
+ [mapping.id],
372
+ );
373
+ expect(auditAssetKeys(getDb()).ok).toBe(true);
374
+
375
+ await moveAssetKey({ entityType: "task", id: first.id, key: "shared/first-moved/" });
376
+ expect(await getAssetKeyMappingByProvider(provider)).toEqual(mapping);
377
+ expect(auditAssetKeys(getDb()).ok).toBe(true);
378
+ expect(
379
+ await getDbClient().query(
380
+ "SELECT id FROM asset_key_history WHERE entity_id = ? ORDER BY id",
381
+ [mapping.id],
382
+ ),
383
+ ).toEqual(historyBefore);
384
+
385
+ await moveAssetKey({ entityType: "task", id: source.id, key: "shared/source-moved/" });
386
+ expect((await getAssetKeyMappingByProvider(provider))?.key).toBe("shared/source-moved/");
387
+ expect(auditAssetKeys(getDb()).ok).toBe(true);
388
+ expect(
389
+ await getDbClient().query("SELECT entity_type FROM asset_key_history WHERE entity_id = ?", [
390
+ mapping.id,
391
+ ]),
392
+ ).toHaveLength(historyBefore.length + 1);
393
+
394
+ await getDbClient().run(
395
+ `UPDATE asset_key_mappings SET "key" = 'shared/no-attaching-task/' WHERE id = ?`,
396
+ [mapping.id],
397
+ );
398
+ const drift = auditAssetKeys(getDb()).issues.filter(
399
+ (issue) => issue.code === "provider-mapping-drift",
400
+ );
401
+ expect(drift).toHaveLength(1);
402
+ expect(drift[0]?.entityId).toBe(mapping.id);
403
+ await expect(
404
+ moveAssetKey({ entityType: "task", id: first.id, key: "shared/blocked/" }),
405
+ ).rejects.toThrow("blocked until");
406
+ await getDbClient().run(
407
+ `UPDATE asset_key_mappings SET "key" = ?, source_entity_type = 'task-attachment', source_entity_id = ? WHERE id = ?`,
408
+ ["shared/source-moved/", mapping.sourceEntityId!, mapping.id],
409
+ );
410
+ expect(auditAssetKeys(getDb()).ok).toBe(true);
411
+ });
412
+
413
+ test("a task move takes only the descendants the caller passes", async () => {
414
+ const root = await createTaskExtended("partial root", { agentId, key: "shared/partial/" });
415
+ const checked = await createTaskExtended("checked child", { agentId, parentTaskId: root.id });
416
+ const unchecked = await createTaskExtended("unchecked child", {
417
+ agentId,
418
+ parentTaskId: root.id,
419
+ });
420
+
421
+ expect(
422
+ await moveAssetKey({
423
+ entityType: "task",
424
+ id: root.id,
425
+ key: "shared/partial-moved/",
426
+ descendantTaskIds: [checked.id],
427
+ }),
428
+ ).toEqual({ movedTaskIds: [root.id, checked.id] });
429
+ expect((await getTaskById(checked.id))?.key).toBe("shared/partial-moved/");
430
+ expect((await getTaskById(unchecked.id))?.key).toBe("shared/partial/");
431
+ });
432
+
433
+ test("deleting the source attachment of a shared file leaves moves open", async () => {
434
+ const [kept, deleted, removed] = await Promise.all(
435
+ ["shared/detach-a/", "shared/detach-b/", "shared/detach-c/"].map((key) =>
436
+ createTaskExtended(`detach ${key}`, { agentId, key }),
437
+ ),
438
+ );
439
+ const other = await createTaskExtended("unrelated move", { agentId, key: "shared/other/" });
440
+ const provider = {
441
+ providerId: "agent-fs",
442
+ providerOrgId: "shared-org",
443
+ providerDriveId: "shared-drive",
444
+ providerKey: "detach.md",
445
+ };
446
+ const attach = (taskId: string) =>
447
+ insertTaskAttachment({
448
+ taskId,
449
+ name: "detach.md",
450
+ kind: "agent-fs",
451
+ providerId: provider.providerId,
452
+ providerKey: provider.providerKey,
453
+ orgId: provider.providerOrgId,
454
+ driveId: provider.providerDriveId,
455
+ });
456
+ await attach(kept!.id);
457
+ const sourceAttachment = await attach(deleted!.id);
458
+ expect((await getAssetKeyMappingByProvider(provider))?.key).toBe("shared/detach-b/");
459
+
460
+ expect(await deleteTaskAttachment(sourceAttachment.id)).toBe(true);
461
+ expect(await getAssetKeyMappingByProvider(provider)).toMatchObject({
462
+ key: "shared/detach-b/",
463
+ sourceEntityType: "external",
464
+ });
465
+ expect(auditAssetKeys(getDb()).ok).toBe(true);
466
+ await moveAssetKey({ entityType: "task", id: other.id, key: "shared/other-moved/" });
467
+
468
+ await attach(removed!.id);
469
+ expect(await deleteTask(removed!.id)).toBe(true);
470
+ expect(await getAssetKeyMappingByProvider(provider)).toMatchObject({
471
+ key: "shared/detach-c/",
472
+ sourceEntityType: "external",
473
+ });
474
+ expect(auditAssetKeys(getDb()).ok).toBe(true);
475
+
476
+ await moveAssetKey({ entityType: "task", id: kept!.id, key: "shared/detach-moved/" });
477
+ expect((await getAssetKeyMappingByProvider(provider))?.key).toBe("shared/detach-moved/");
478
+ expect(auditAssetKeys(getDb()).ok).toBe(true);
479
+ });
480
+
481
+ test("an external mapping that a task still attaches follows that task's move", async () => {
482
+ const task = await createTaskExtended("external owner", { agentId, key: "shared/external/" });
483
+ const provider = {
484
+ providerId: "agent-fs",
485
+ providerOrgId: "shared-org",
486
+ providerDriveId: "shared-drive",
487
+ providerKey: "external.md",
488
+ };
489
+ await insertTaskAttachment({
490
+ taskId: task.id,
491
+ name: "external.md",
492
+ kind: "agent-fs",
493
+ providerId: provider.providerId,
494
+ providerKey: provider.providerKey,
495
+ orgId: provider.providerOrgId,
496
+ driveId: provider.providerDriveId,
497
+ });
498
+ const mapping = (await getAssetKeyMappingByProvider(provider))!;
499
+ await getDbClient().run(
500
+ "UPDATE asset_key_mappings SET source_entity_type = 'external', source_entity_id = NULL WHERE id = ?",
501
+ [mapping.id],
502
+ );
503
+
504
+ await moveAssetKey({ entityType: "task", id: task.id, key: "shared/external-moved/" });
505
+ expect((await getAssetKeyMappingByProvider(provider))?.key).toBe("shared/external-moved/");
506
+ expect(auditAssetKeys(getDb()).ok).toBe(true);
507
+ });
508
+
333
509
  test("personal namespace users are audited and missing users are repairable warnings", async () => {
334
510
  const task = await createTaskExtended("personal", {
335
511
  agentId,
@@ -4,6 +4,7 @@ import { closeDb, createTaskExtended, createUser, getDb, getDbClient, initDb } f
4
4
  const FRESH_DB = "./test-asset-key-migration-fresh.sqlite";
5
5
  const HISTORICAL_DB = "./test-asset-key-migration-historical.sqlite";
6
6
  const HISTORY_REBUILD_DB = "./test-asset-key-migration-history-rebuild.sqlite";
7
+ const INBOX_UPGRADE_DB = "./test-asset-key-migration-inbox-upgrade.sqlite";
7
8
  const LEGACY_STATUS_DB = "./test-asset-key-migration-legacy-status.sqlite";
8
9
 
9
10
  const globals = globalThis as typeof globalThis & {
@@ -97,6 +98,7 @@ beforeAll(async () => {
97
98
  await removeDb(HISTORICAL_DB);
98
99
  await removeDb(HISTORY_REBUILD_DB);
99
100
  await removeDb(LEGACY_STATUS_DB);
101
+ await removeDb(INBOX_UPGRADE_DB);
100
102
  initDb(FRESH_DB);
101
103
  });
102
104
 
@@ -108,6 +110,7 @@ afterAll(async () => {
108
110
  await removeDb(HISTORICAL_DB);
109
111
  await removeDb(HISTORY_REBUILD_DB);
110
112
  await removeDb(LEGACY_STATUS_DB);
113
+ await removeDb(INBOX_UPGRADE_DB);
111
114
  });
112
115
 
113
116
  describe("asset namespace key migrations", () => {
@@ -173,6 +176,38 @@ describe("asset namespace key migrations", () => {
173
176
  expect(historySchema).toContain("'script'");
174
177
  });
175
178
 
179
+ test("inbox read cursor is nullable on fresh databases and has no backfill on upgrade", async () => {
180
+ const column = await getDbClient().get<{ notnull: number; dflt_value: string | null }>(
181
+ "SELECT \"notnull\", dflt_value FROM pragma_table_info('inbox_item_state') WHERE name = 'lastReadAt'",
182
+ );
183
+ expect(column).toEqual({ notnull: 0, dflt_value: null });
184
+ closeDb();
185
+ initDb(INBOX_UPGRADE_DB);
186
+ try {
187
+ const user = await createUser({ name: "Upgrade User", email: "inbox-upgrade@example.com" });
188
+ const readAt = "2026-01-01T00:00:00.000Z";
189
+ await getDbClient().run(
190
+ `INSERT INTO inbox_item_state (userId, itemType, itemId, status, readAt)
191
+ VALUES (?, 'to_read', 'existing-item', 'done', ?)`,
192
+ [user.id, readAt],
193
+ );
194
+ await getDbClient().run("ALTER TABLE inbox_item_state DROP COLUMN lastReadAt");
195
+ await getDbClient().run("DELETE FROM _migrations WHERE version = 227");
196
+ closeDb();
197
+ initDb(INBOX_UPGRADE_DB);
198
+ expect(
199
+ await getDbClient().get(
200
+ "SELECT status, readAt, lastReadAt FROM inbox_item_state WHERE itemId = 'existing-item'",
201
+ ),
202
+ ).toEqual({ status: "done", readAt, lastReadAt: null });
203
+ closeDb();
204
+ expect(() => initDb(INBOX_UPGRADE_DB)).not.toThrow();
205
+ } finally {
206
+ closeDb();
207
+ initDb(FRESH_DB);
208
+ }
209
+ });
210
+
176
211
  test("old insert statements may omit key and repeated shared namespaces are valid", () => {
177
212
  const now = new Date().toISOString();
178
213
  const taskId = crypto.randomUUID();
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
2
2
  import {
3
3
  checkPrBody,
4
4
  isFixTitle,
5
+ isRankingEntryPr,
5
6
  pickedChoices,
6
7
  prBodyWarnings,
7
8
  proseWordCount,
@@ -34,6 +35,28 @@ const TEMPLATE = `<!-- top comment -->
34
35
 
35
36
  const URGENT = "## Urgency\n- [ ] asap\n- [x] nice to have\n";
36
37
 
38
+ describe("isRankingEntryPr", () => {
39
+ test("exempts only a one-file entry PR with the matching title", () => {
40
+ expect(
41
+ isRankingEntryPr("chore(ranking): add acme.dev", ["community/ranking/acme.dev.json"]),
42
+ ).toBe(true);
43
+ expect(
44
+ isRankingEntryPr("chore(ranking): remove acme.dev", ["community/ranking/acme.dev.json"]),
45
+ ).toBe(true);
46
+ expect(
47
+ isRankingEntryPr("chore(ranking): add acme.dev", ["community/ranking/acme.com.json"]),
48
+ ).toBe(false);
49
+ expect(
50
+ isRankingEntryPr("chore(ranking): add acme.dev", [
51
+ "community/ranking/acme.dev.json",
52
+ "src/server.ts",
53
+ ]),
54
+ ).toBe(false);
55
+ expect(isRankingEntryPr("feat: add acme.dev", ["community/ranking/acme.dev.json"])).toBe(false);
56
+ expect(isRankingEntryPr("chore(ranking): add acme.dev", undefined)).toBe(false);
57
+ });
58
+ });
59
+
37
60
  describe("check-pr-body", () => {
38
61
  test("template markers set when a section is required", () => {
39
62
  expect(templateSections(TEMPLATE)).toEqual([
@@ -0,0 +1,146 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ checkRankingEntries,
4
+ type Entry,
5
+ isEntryPath,
6
+ parseEntry,
7
+ type Verification,
8
+ } from "../../scripts/check-ranking-entry";
9
+
10
+ const CLAIM = "rk_3f9a1c0e7b2d4a6f8e1c3b5d7f9a2c4e";
11
+ const OTHER_CLAIM = "rk_00000000000000000000000000000001";
12
+ const file = (domain: string, claim = CLAIM) =>
13
+ JSON.stringify({ $schema: "./schema.json", domain, claim });
14
+
15
+ const VERIFIED: Verification = {
16
+ verified: true,
17
+ reasons: [],
18
+ telemetryActive7d: true,
19
+ websiteMatchesDomain: true,
20
+ listing: {
21
+ name: "Acme",
22
+ domain: "acme.dev",
23
+ category: "engineering",
24
+ subcategory: "qa-testing",
25
+ description: "QA | release automation",
26
+ sourceCodeUrl: null,
27
+ },
28
+ };
29
+
30
+ describe("parseEntry", () => {
31
+ test("accepts the opt-in file", () => {
32
+ expect(parseEntry("acme.dev.json", file("acme.dev"))).toEqual({
33
+ domain: "acme.dev",
34
+ claim: CLAIM,
35
+ });
36
+ });
37
+
38
+ test.each([
39
+ ["acme.dev.json", "{", "valid JSON"],
40
+ ["acme.dev.json", "[]", "JSON object"],
41
+ [
42
+ "acme.dev.json",
43
+ JSON.stringify({ domain: "acme.dev", claim: CLAIM, name: "Acme" }),
44
+ "unknown keys",
45
+ ],
46
+ ["www.acme.dev.json", file("www.acme.dev"), "bare lowercase hostname"],
47
+ ["Acme.ba.json", file("Acme.ba"), "bare lowercase hostname"],
48
+ ["other.ba.json", file("acme.dev"), "must be named acme.dev.json"],
49
+ ["acme.dev.json", file("acme.dev", "rk_short"), "claim"],
50
+ ])("rejects %s %s", (name, text, message) => {
51
+ expect(() => parseEntry(name, text)).toThrow(message);
52
+ });
53
+
54
+ test("README and schema are not entries", () => {
55
+ expect(isEntryPath("community/ranking/acme.dev.json")).toBe(true);
56
+ expect(isEntryPath("community/ranking/schema.json")).toBe(false);
57
+ expect(isEntryPath("community/ranking/README.md")).toBe(false);
58
+ expect(isEntryPath("community/ranking/nested/acme.dev.json")).toBe(false);
59
+ });
60
+ });
61
+
62
+ describe("checkRankingEntries", () => {
63
+ const files: Record<string, string> = {
64
+ "community/ranking/acme.dev.json": file("acme.dev"),
65
+ "community/ranking/acme.com.json": file("acme.com", CLAIM),
66
+ };
67
+ const run = (
68
+ paths: string[],
69
+ verify: (entry: Entry) => Promise<Verification>,
70
+ all = ["acme.dev.json"],
71
+ ) =>
72
+ checkRankingEntries({
73
+ changes: paths.map((path) => ({ status: "A", path })),
74
+ readEntry: (path) => files[path] ?? "",
75
+ allEntries: all.map((name) => [name, files[`community/ranking/${name}`] ?? ""]),
76
+ verify,
77
+ });
78
+
79
+ test("a verified entry passes and the summary shows the listing", async () => {
80
+ const asked: Entry[] = [];
81
+ const result = await run(["community/ranking/acme.dev.json"], async (entry) => {
82
+ asked.push(entry);
83
+ return VERIFIED;
84
+ });
85
+ expect(result.problems).toEqual([]);
86
+ expect(asked).toEqual([{ domain: "acme.dev", claim: CLAIM }]);
87
+ const summary = result.summary.join("\n");
88
+ expect(summary).toContain("### acme.dev: verified");
89
+ expect(summary).toContain(
90
+ "| Acme | engineering | qa-testing | QA \\| release automation | — |",
91
+ );
92
+ });
93
+
94
+ test("an unverified entry fails with the proxy's reasons", async () => {
95
+ const result = await run(["community/ranking/acme.dev.json"], async () => ({
96
+ ...VERIFIED,
97
+ verified: false,
98
+ reasons: ["telemetry_inactive_7d"],
99
+ }));
100
+ expect(result.problems).toHaveLength(1);
101
+ expect(result.problems[0]).toContain("telemetry_inactive_7d");
102
+ });
103
+
104
+ test("a proxy error fails without throwing", async () => {
105
+ const result = await run(["community/ranking/acme.dev.json"], async () => {
106
+ throw new Error("the proxy answered 503");
107
+ });
108
+ expect(result.problems[0]).toContain("503");
109
+ });
110
+
111
+ test("a claim shared with another entry fails before verify", async () => {
112
+ let called = false;
113
+ const result = await run(["community/ranking/acme.com.json"], async () => {
114
+ called = true;
115
+ return VERIFIED;
116
+ }, ["acme.dev.json", "acme.com.json"]);
117
+ expect(called).toBe(false);
118
+ expect(result.problems[0]).toContain("acme.dev.json");
119
+ });
120
+
121
+ test("a removal needs no proof, and README edits are ignored", async () => {
122
+ const result = await checkRankingEntries({
123
+ changes: [
124
+ { status: "D", path: "community/ranking/acme.dev.json" },
125
+ { status: "M", path: "community/ranking/README.md" },
126
+ ],
127
+ readEntry: () => "",
128
+ allEntries: [],
129
+ verify: async () => {
130
+ throw new Error("must not be called");
131
+ },
132
+ });
133
+ expect(result.problems).toEqual([]);
134
+ expect(result.summary).toEqual(["- Removed `acme.dev.json`."]);
135
+ });
136
+
137
+ test("the claim differs per entry in a valid two-entry PR", async () => {
138
+ files["community/ranking/acme.com.json"] = file("acme.com", OTHER_CLAIM);
139
+ const result = await run(
140
+ ["community/ranking/acme.com.json", "community/ranking/acme.dev.json"],
141
+ async () => VERIFIED,
142
+ ["acme.dev.json", "acme.com.json"],
143
+ );
144
+ expect(result.problems).toEqual([]);
145
+ });
146
+ });
@@ -15,6 +15,7 @@ test("facade exposes repository bindings without internal exports", () => {
15
15
  "rowToAgentTaskSummary",
16
16
  "configureTaskReadDependencies",
17
17
  "NEVER_STARTED_TASK_STATUSES",
18
+ "readableSessionCondition",
18
19
  ].includes(name)
19
20
  ) {
20
21
  expect(name in db).toBe(false);
@@ -660,6 +660,7 @@ const internal = [
660
660
  "type:AgentTaskRow",
661
661
  "value:configureTaskReadDependencies",
662
662
  "value:NEVER_STARTED_TASK_STATUSES",
663
+ "value:readableSessionCondition",
663
664
  "value:rowToAgentTask",
664
665
  "value:rowToAgentTaskSummary",
665
666
  ].sort();
@@ -103,8 +103,8 @@ describe("docs API index footprint", () => {
103
103
  expect(spec.paths["/api/agents/{id}/runtime"]?.get).toBeDefined();
104
104
 
105
105
  const generatedKeys = await generatedOperationKeys();
106
- expect(operationKeys).toHaveLength(430);
107
- expect(generatedKeys).toHaveLength(430);
106
+ expect(operationKeys).toHaveLength(436);
107
+ expect(generatedKeys).toHaveLength(436);
108
108
  expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
109
109
  });
110
110
 
@@ -13,10 +13,14 @@ import {
13
13
  closeDb,
14
14
  createAgent,
15
15
  createTaskExtended,
16
+ deletePromptTemplate,
16
17
  getDbClient,
18
+ getKv,
19
+ getRecentFailedTasks,
17
20
  initDb,
18
21
  startTask,
19
22
  updateAgentProfile,
23
+ upsertPromptTemplate,
20
24
  upsertSwarmConfig,
21
25
  } from "../be/db";
22
26
  import { getScript, upsertScriptByName } from "../be/scripts/db";
@@ -240,6 +244,89 @@ describe("Heartbeat Checklist", () => {
240
244
  expect(tasks[0]!.priority).toBe(60);
241
245
  });
242
246
 
247
+ test("receipt stores exactly the ten rendered full IDs from twelve failures", async () => {
248
+ const lead = await createAgent({ name: "lead", isLead: true, status: "idle" });
249
+ await updateAgentProfile(lead.id, { heartbeatMd: "- Check failures" });
250
+ for (let i = 0; i < 12; i++) {
251
+ const task = await createTaskExtended(`receipt failure ${i}`, { agentId: lead.id });
252
+ await getDbClient().run(
253
+ "UPDATE agent_tasks SET status = 'failed', failureReason = ?, finishedAt = ? WHERE id = ?",
254
+ [`receipt reason ${i}`, new Date(Date.now() - (i + 1) * 60e3).toISOString(), task.id],
255
+ );
256
+ }
257
+ const failures = await getRecentFailedTasks(6);
258
+ const before = Date.now();
259
+ await checkHeartbeatChecklist();
260
+ const checklist = await getDbClient().get<{ id: string; task: string }>(
261
+ "SELECT id, task FROM agent_tasks WHERE taskType = 'heartbeat-checklist'",
262
+ );
263
+ expect(checklist).toBeDefined();
264
+ const receipt = await getKv("heartbeat-checklist:reported-failures", checklist!.id);
265
+ expect(receipt?.value).toEqual(failures.slice(0, 10).map((task) => task.id));
266
+ expect(receipt?.expiresAt).toBeGreaterThanOrEqual(before + 7 * 24 * 3600e3);
267
+ expect(receipt?.expiresAt).toBeLessThanOrEqual(Date.now() + 7 * 24 * 3600e3);
268
+ for (const failure of failures.slice(0, 10)) {
269
+ expect(checklist!.task).toContain(`[${failure.id.slice(0, 8)}]`);
270
+ }
271
+ for (const failure of failures.slice(10)) {
272
+ expect(checklist!.task).not.toContain(`[${failure.id.slice(0, 8)}]`);
273
+ }
274
+ });
275
+
276
+ test("receipt excludes failures when the global checklist template omits system status", async () => {
277
+ const lead = await createAgent({ name: "lead", isLead: true, status: "idle" });
278
+ await updateAgentProfile(lead.id, { heartbeatMd: "- Check standing orders" });
279
+ const failure = await createTaskExtended("Unrendered failure", { agentId: lead.id });
280
+ await getDbClient().run(
281
+ "UPDATE agent_tasks SET status = 'failed', failureReason = ?, finishedAt = ? WHERE id = ?",
282
+ ["boom", new Date(Date.now() - 1000).toISOString(), failure.id],
283
+ );
284
+ const template = upsertPromptTemplate({
285
+ eventType: "heartbeat.checklist",
286
+ scope: "global",
287
+ state: "enabled",
288
+ body: "Review standing orders:\n{{heartbeat_content}}",
289
+ });
290
+ try {
291
+ await checkHeartbeatChecklist();
292
+ const checklist = await getDbClient().get<{ id: string; task: string }>(
293
+ "SELECT id, task FROM agent_tasks WHERE taskType = 'heartbeat-checklist'",
294
+ );
295
+ expect(checklist?.task).toBe("Review standing orders:\n- Check standing orders");
296
+ expect(
297
+ (await getKv("heartbeat-checklist:reported-failures", checklist!.id))?.value,
298
+ ).toEqual([]);
299
+ } finally {
300
+ await deletePromptTemplate(template.id);
301
+ }
302
+ });
303
+
304
+ test("receipt write errors log and preserve the created checklist", async () => {
305
+ const lead = await createAgent({ name: "lead", isLead: true, status: "idle" });
306
+ await updateAgentProfile(lead.id, { heartbeatMd: "- Check failures" });
307
+ await getDbClient().run(`CREATE TRIGGER reject_heartbeat_receipt
308
+ BEFORE INSERT ON kv_entries
309
+ WHEN NEW.namespace = 'heartbeat-checklist:reported-failures'
310
+ BEGIN SELECT RAISE(FAIL, 'receipt unavailable'); END`);
311
+ const log = spyOn(console, "error").mockImplementation(() => {});
312
+ try {
313
+ await checkHeartbeatChecklist();
314
+ const checklist = await getDbClient().get<{ id: string }>(
315
+ "SELECT id FROM agent_tasks WHERE taskType = 'heartbeat-checklist'",
316
+ );
317
+ expect(checklist).toBeDefined();
318
+ expect(log).toHaveBeenCalledWith(
319
+ expect.stringContaining(
320
+ `Failed to save reported failures for checklist ${checklist!.id}`,
321
+ ),
322
+ expect.any(Error),
323
+ );
324
+ } finally {
325
+ log.mockRestore();
326
+ await getDbClient().run("DROP TRIGGER reject_heartbeat_receipt");
327
+ }
328
+ });
329
+
243
330
  test("dedup: skips when active heartbeat-checklist task exists for lead", async () => {
244
331
  const lead = await createAgent({ name: "lead", isLead: true, status: "idle" });
245
332
  await updateAgentProfile(lead.id, {
@@ -725,6 +812,7 @@ describe("Heartbeat Checklist", () => {
725
812
  expect(tasks.length).toBe(1);
726
813
  expect(tasks[0]!.agentId).toBe(lead.id);
727
814
  expect(tasks[0]!.priority).toBe(70);
815
+ expect(await getKv("heartbeat-checklist:reported-failures", tasks[0]!.id)).toBeNull();
728
816
  });
729
817
 
730
818
  test("boot-triage task includes reboot context", async () => {