@desplega.ai/agent-swarm 1.145.0 → 1.147.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 (221) hide show
  1. package/README.md +2 -1
  2. package/dist/{acp-adapter-580t41c0.js → acp-adapter-9n319wqc.js} +61 -6
  3. package/dist/{actions-m662tjng.js → actions-2vxqvpr9.js} +9 -8
  4. package/dist/{app-zb3rndm8.js → app-w066xfy0.js} +6 -5
  5. package/dist/assistant-kt31dj2h.js +236 -0
  6. package/dist/{boot-reembed-072x00vn.js → boot-reembed-az5rassp.js} +7 -6
  7. package/dist/{boot-reembed-65zz2smt.js → boot-reembed-j3mm3rfz.js} +8 -7
  8. package/dist/{boot-scrub-logs-yz0ams04.js → boot-scrub-logs-b6h54817.js} +4 -4
  9. package/dist/{claude-adapter-paj270y2.js → claude-adapter-w1z04ann.js} +8 -6
  10. package/dist/{claude-managed-setup-w619jncn.js → claude-managed-setup-f5pmsyrp.js} +24 -3
  11. package/dist/{claude-sdk-session-swt80j9z.js → claude-sdk-session-f0cc1kec.js} +8 -6
  12. package/dist/{cli-dt553ktb.js → cli-0b6y7kxm.js} +1 -1
  13. package/dist/{cli-bxdhw615.js → cli-12fz972k.js} +4 -4
  14. package/dist/{cli-s2f20r5w.js → cli-1th1728g.js} +2 -2
  15. package/dist/{cli-rs6ec3b9.js → cli-3tgwnf43.js} +3 -3
  16. package/dist/{cli-gxnzc6wh.js → cli-4nr988h6.js} +1 -1
  17. package/dist/{cli-axs8e6d4.js → cli-53s590z8.js} +1 -1
  18. package/dist/{cli-ygbjp3z8.js → cli-54wwve05.js} +5 -4
  19. package/dist/{cli-v0asr5yv.js → cli-719k9j2c.js} +2 -2
  20. package/dist/{cli-anktweqr.js → cli-89xfd7f9.js} +21 -47
  21. package/dist/{cli-q34a0e10.js → cli-8v0g1yc8.js} +1 -1
  22. package/dist/cli-9d8ntetj.js +42 -0
  23. package/dist/{cli-z9fprcrf.js → cli-atcve9y8.js} +881 -738
  24. package/dist/{cli-jmb7g47h.js → cli-b5bq9crk.js} +14 -31
  25. package/dist/cli-d8brzxjb.js +190 -0
  26. package/dist/{cli-gzky0gkr.js → cli-de14znh8.js} +2 -2
  27. package/dist/{cli-xytwff7f.js → cli-dttph1bw.js} +2 -2
  28. package/dist/cli-ep19k3mb.js +19 -0
  29. package/dist/{cli-m1xa8be7.js → cli-f146mn5s.js} +7 -50
  30. package/dist/{cli-1fj0chdk.js → cli-f7dsy61y.js} +5 -3
  31. package/dist/{cli-f567d5c1.js → cli-ftht3zzj.js} +2782 -2589
  32. package/dist/{cli-a0g1gbt5.js → cli-hbv0kq6w.js} +2 -2
  33. package/dist/{cli-fhsz3twr.js → cli-jsp14b6c.js} +171 -1
  34. package/dist/{cli-7fnkj1hj.js → cli-kpv03zhj.js} +21 -17
  35. package/dist/{cli-m4j6trn4.js → cli-m8yrsg97.js} +2 -2
  36. package/dist/cli-m9sxbkhm.js +779 -0
  37. package/dist/cli-mmemxxdc.js +823 -0
  38. package/dist/{cli-tqtvj5pt.js → cli-n8508nre.js} +1 -1
  39. package/dist/{cli-z96t0thj.js → cli-p1d5b073.js} +37 -2
  40. package/dist/{cli-gh8y8f36.js → cli-rw51bq3j.js} +13 -10
  41. package/dist/{cli-59ej1c07.js → cli-smby5m17.js} +7 -6
  42. package/dist/{cli-65vqzz3v.js → cli-tzvk9haz.js} +94 -372
  43. package/dist/{cli-dc9k2ba9.js → cli-w9kb3bk1.js} +1 -1
  44. package/dist/{cli-kpev657q.js → cli-y6b6fb76.js} +25 -3
  45. package/dist/{cli-123q2get.js → cli-yz3djrm0.js} +9 -9
  46. package/dist/{cli-wvp6bvy4.js → cli-zhjmqxzh.js} +1 -1
  47. package/dist/cli.js +13 -13
  48. package/dist/{codex-adapter-k98vaw2r.js → codex-adapter-zw4dwsx7.js} +2 -2
  49. package/dist/{codex-hook-9pf19cks.js → codex-hook-bw8p5nh4.js} +2 -2
  50. package/dist/{codex-session-runner-tngtphx7.js → codex-session-runner-k981s65v.js} +2 -2
  51. package/dist/{commands-y8nbr3fv.js → commands-sykrxx7e.js} +4 -4
  52. package/dist/{db-30p4h2t1.js → db-sfbsc8wt.js} +6 -4
  53. package/dist/handlers-0aspcejm.js +603 -0
  54. package/dist/{hook-dcw4jfbj.js → hook-4nvqj2sb.js} +7 -5
  55. package/dist/{hook-21jexvfx.js → hook-hxccs7m5.js} +24 -49
  56. package/dist/{http-r4kq5g8d.js → http-y06z517d.js} +230 -94
  57. package/dist/{index-q3qja6ta.js → index-hssww7kj.js} +15 -14
  58. package/dist/{index-gay66b9w.js → index-k8znfx0z.js} +13 -12
  59. package/dist/{index-53zyht4z.js → index-tce1rss4.js} +371 -110
  60. package/dist/{index-nqvf26za.js → index-x5j894zf.js} +14 -13
  61. package/dist/{keepalive-xs8fa55y.js → keepalive-w733ax66.js} +6 -6
  62. package/dist/{lead-d9m2t4s0.js → lead-75bjqa9d.js} +29 -27
  63. package/dist/{maintenance-3zh82g6z.js → maintenance-w4f1zjck.js} +8 -7
  64. package/dist/{oauth-refresh-sweep-03snsye5.js → oauth-refresh-sweep-wdetky26.js} +5 -5
  65. package/dist/{onboard-0cg1925x.js → onboard-bb5net8s.js} +2 -2
  66. package/dist/{otel-impl-0ab95z27.js → otel-impl-0w7c14ap.js} +1 -1
  67. package/dist/{pi-mono-adapter-dtkz4e7j.js → pi-mono-adapter-2has4gnp.js} +12 -2
  68. package/dist/{pricing-refresh-e5jhzz4j.js → pricing-refresh-9nzsrgcq.js} +6 -6
  69. package/dist/{rbac-roles-p1mf40z3.js → rbac-roles-r3hqjp09.js} +5 -5
  70. package/dist/{rbac-roles-2w3cmrkc.js → rbac-roles-z1jrffbt.js} +4 -4
  71. package/dist/{registry-9kdck5ye.js → registry-v5wytg0s.js} +1 -2
  72. package/dist/{render-v2-xhawry3k.js → render-v2-vbr89fnb.js} +6 -5
  73. package/dist/{seed-pricing-t223sw91.js → seed-pricing-g62hy1pk.js} +7 -5
  74. package/dist/{setup-zcs0rce7.js → setup-gnfqnptp.js} +2 -2
  75. package/dist/{worker-qnp2ka62.js → worker-kznxp4qm.js} +29 -27
  76. package/openapi.json +263 -12
  77. package/package.json +1 -1
  78. package/plugin/commands/start-worker.md +4 -9
  79. package/plugin/pi-skills/start-worker/SKILL.md +4 -9
  80. package/src/be/db/agents.ts +556 -0
  81. package/src/be/db/context-versions.ts +117 -0
  82. package/src/be/db/runtime.ts +406 -0
  83. package/src/be/db/tasks/read.ts +1167 -0
  84. package/src/be/db/tasks/write.ts +953 -0
  85. package/src/be/db.ts +584 -3429
  86. package/src/be/migrations/147_scheduled_tasks_parent_task.sql +7 -0
  87. package/src/be/migrations/148_pages_svg.sql +123 -0
  88. package/src/be/migrations/149_session_tokens.sql +24 -0
  89. package/src/be/scripts/typecheck.ts +1 -0
  90. package/src/be/seed-pricing.ts +13 -8
  91. package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
  92. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
  93. package/src/be/seed-scripts/catalog/star-history-refresh.ts +101 -0
  94. package/src/be/seed-scripts/catalog/star-history-renderer.ts +119 -0
  95. package/src/be/seed-scripts/index.ts +17 -0
  96. package/src/be/seed-skills/index.ts +14 -0
  97. package/src/be/swarm-config-guard.ts +5 -2
  98. package/src/be/task-attachment-store.ts +70 -0
  99. package/src/be/users.ts +77 -0
  100. package/src/commands/attachments-section.ts +37 -0
  101. package/src/commands/claude-managed-setup.ts +45 -3
  102. package/src/commands/claude-md-session.ts +408 -0
  103. package/src/commands/context-preamble.ts +15 -3
  104. package/src/commands/profile-sync.ts +205 -5
  105. package/src/commands/provider-credentials.ts +60 -27
  106. package/src/commands/runner.ts +63 -58
  107. package/src/commands/templates.ts +25 -2
  108. package/src/hooks/hook.ts +26 -67
  109. package/src/http/agents.ts +40 -1
  110. package/src/http/auth.ts +13 -1
  111. package/src/http/fs.ts +21 -51
  112. package/src/http/index.ts +2 -0
  113. package/src/http/mcp.ts +32 -0
  114. package/src/http/pages-public.ts +20 -4
  115. package/src/http/pages.ts +11 -3
  116. package/src/http/poll.ts +4 -0
  117. package/src/http/repos.ts +3 -3
  118. package/src/http/sessions.ts +82 -0
  119. package/src/http/tasks.ts +10 -2
  120. package/src/http/utils.ts +79 -1
  121. package/src/prompts/session-templates.ts +12 -8
  122. package/src/providers/acp-adapter.ts +62 -8
  123. package/src/providers/pi-mono-adapter.ts +23 -6
  124. package/src/providers/pricing-sources.md +11 -0
  125. package/src/scheduler/scheduler.test.ts +49 -2
  126. package/src/scheduler/scheduler.ts +4 -0
  127. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  128. package/src/scripts-runtime/types/stdlib.d.ts +8 -0
  129. package/src/scripts-runtime/types/swarm-sdk.d.ts +8 -0
  130. package/src/server.ts +2 -0
  131. package/src/slack/assistant.ts +87 -46
  132. package/src/slack/handlers.test.ts +1 -3
  133. package/src/slack/handlers.ts +102 -87
  134. package/src/slack/inbound-files.ts +466 -0
  135. package/src/slack/templates.ts +14 -0
  136. package/src/tasks/task-terminal-effects.ts +158 -0
  137. package/src/tests/acp-adapter.test.ts +369 -117
  138. package/src/tests/budget-claim-gate.test.ts +1 -0
  139. package/src/tests/build-pi-skills.test.ts +5 -7
  140. package/src/tests/claude-managed-setup.test.ts +52 -4
  141. package/src/tests/claude-md-session.test.ts +603 -0
  142. package/src/tests/codex-skill-resolver.test.ts +19 -0
  143. package/src/tests/context-versioning.test.ts +107 -0
  144. package/src/tests/cors-allowlist.test.ts +309 -0
  145. package/src/tests/credential-check.test.ts +157 -14
  146. package/src/tests/db-agents.test.ts +91 -0
  147. package/src/tests/db-context-versions.test.ts +37 -0
  148. package/src/tests/db-extraction-contract.test.ts +45 -0
  149. package/src/tests/db-runtime.test.ts +70 -0
  150. package/src/tests/db-task-read-contract.test.ts +624 -0
  151. package/src/tests/db-task-write-contract.test.ts +653 -0
  152. package/src/tests/db-tasks-read.test.ts +183 -0
  153. package/src/tests/db-tasks-write.test.ts +230 -0
  154. package/src/tests/defer-task.test.ts +389 -0
  155. package/src/tests/docs-index-footprint.test.js +2 -2
  156. package/src/tests/e2e-visuals-stage.test.ts +151 -0
  157. package/src/tests/file-lock.test.ts +144 -0
  158. package/src/tests/fixtures/file-lock-holder.ts +13 -0
  159. package/src/tests/fixtures/hold-file-lock.ts +25 -0
  160. package/src/tests/heartbeat-expiry.test.ts +188 -0
  161. package/src/tests/http-api-integration.test.ts +78 -1
  162. package/src/tests/pages-public-html.test.ts +43 -0
  163. package/src/tests/pages-svg-migration.test.ts +42 -0
  164. package/src/tests/preload.ts +4 -1
  165. package/src/tests/profile-sync-warning.test.ts +16 -0
  166. package/src/tests/profile-sync.test.ts +126 -0
  167. package/src/tests/prompt-template-session.test.ts +20 -4
  168. package/src/tests/provider-registration.test.ts +93 -0
  169. package/src/tests/rbac-e2e-helpers.test.ts +53 -0
  170. package/src/tests/rbac-e2e-helpers.ts +19 -2
  171. package/src/tests/reload-config.test.ts +26 -0
  172. package/src/tests/routing-affinity-inventory.test.ts +1 -1
  173. package/src/tests/runner-attachments-section.test.ts +22 -2
  174. package/src/tests/runner-context-preamble.test.ts +30 -0
  175. package/src/tests/runner-deployment-resume.test.ts +107 -0
  176. package/src/tests/runner-repo-autostash.test.ts +12 -0
  177. package/src/tests/script-connections.test.ts +4 -0
  178. package/src/tests/seed-scripts.test.ts +2 -2
  179. package/src/tests/session-tokens.test.ts +279 -0
  180. package/src/tests/slack-file-tools-attachments.test.ts +408 -0
  181. package/src/tests/slack-inbound-files.test.ts +847 -0
  182. package/src/tests/star-history-refresh.test.ts +102 -0
  183. package/src/tests/swarm-config-reserved-keys.test.ts +21 -0
  184. package/src/tests/swarm-repos.test.ts +20 -0
  185. package/src/tests/tool-annotations.test.ts +4 -3
  186. package/src/tests/ui-logs-parser.test.ts +176 -0
  187. package/src/tools/create-page.ts +4 -2
  188. package/src/tools/defer-task.ts +239 -0
  189. package/src/tools/repos/update-repo.ts +2 -2
  190. package/src/tools/schedules/create-schedule.ts +1 -0
  191. package/src/tools/schedules/list-schedules.ts +1 -0
  192. package/src/tools/schedules/patch-schedule.ts +1 -0
  193. package/src/tools/schedules/update-schedule.ts +1 -0
  194. package/src/tools/slack-download-file.ts +60 -29
  195. package/src/tools/slack-read.ts +73 -32
  196. package/src/tools/store-progress.ts +16 -146
  197. package/src/tools/tool-config.ts +1 -0
  198. package/src/tools/utils.ts +21 -0
  199. package/src/types.ts +37 -4
  200. package/src/utils/acp-session-token.ts +56 -0
  201. package/src/utils/file-lock.ts +111 -0
  202. package/src/utils/heartbeat-expiry.ts +78 -0
  203. package/src/utils/request-auth-context.ts +2 -0
  204. package/src/utils/task-attachment-links.ts +22 -0
  205. package/templates/skills/code-quality/content.md +8 -5
  206. package/templates/skills/download-task-attachment/content.md +2 -1
  207. package/{plugin/pi-skills → templates/skills}/review-offered-task/SKILL.md +5 -5
  208. package/templates/skills/review-offered-task/config.json +14 -0
  209. package/{plugin/commands/review-offered-task.md → templates/skills/review-offered-task/content.md} +3 -8
  210. package/templates/skills/scheduling/content.md +4 -0
  211. package/templates/skills/slack-interaction/content.md +2 -2
  212. package/templates/skills/work-on-task/SKILL.md +16 -0
  213. package/templates/skills/work-on-task/config.json +14 -0
  214. package/templates/skills/work-on-task/content.md +11 -0
  215. package/dist/assistant-0qt42nzg.js +0 -212
  216. package/dist/cli-fs1kkqgr.js +0 -317
  217. package/dist/cli-jnf5zghe.js +0 -269
  218. package/dist/cli-n1q8q14d.js +0 -177
  219. package/dist/handlers-yfp1qn81.js +0 -592
  220. package/plugin/commands/work-on-task.md +0 -39
  221. package/plugin/pi-skills/work-on-task/SKILL.md +0 -39
@@ -0,0 +1,183 @@
1
+ import { afterEach, beforeEach, expect, spyOn, test } from "bun:test";
2
+ import * as db from "../be/db";
3
+
4
+ beforeEach(async () => {
5
+ db.initDb(":memory:");
6
+ await db
7
+ .getDbClient()
8
+ .run(
9
+ "INSERT INTO users (id, name, createdAt, lastUpdatedAt) VALUES ('requester', 'Requester', '2026-01-01', '2026-01-01')",
10
+ );
11
+ for (const [id, status, taskType, tags, requester, updated, priority] of [
12
+ ["a", "pending", null, "[]", null, "03", 10],
13
+ ["b", "completed", "feature", '["blue"]', "requester", "02", 50],
14
+ ["c", "pending", "feature", '["blue"]', "requester", "03", 90],
15
+ ["h", "pending", "heartbeat", "[]", null, "04", 50],
16
+ ["t", "pending", null, '["heartbeat"]', null, "05", 50],
17
+ ["k", "pending", "heartbeat-checklist", "[]", null, "06", 50],
18
+ ["z", "pending", "boot-triage", "[]", null, "07", 50],
19
+ ] as const) {
20
+ await db.getDbClient().run(
21
+ `INSERT INTO agent_tasks (id, task, status, source, taskType, tags, requestedByUserId,
22
+ createdAt, lastUpdatedAt, priority, "key") VALUES (?, ?, ?, 'api', ?, ?, ?, ?, ?, ?, 'shared/demo/')`,
23
+ [
24
+ id,
25
+ `needle ${id}`,
26
+ status,
27
+ taskType,
28
+ tags,
29
+ requester,
30
+ "2026-01-01",
31
+ `2026-01-${updated}`,
32
+ priority,
33
+ ],
34
+ );
35
+ }
36
+ });
37
+ afterEach(() => db.closeDb());
38
+
39
+ // Characterized against the pre-extraction facade at 8a8b7b53.
40
+ const cases: {
41
+ name: string;
42
+ filters: db.TaskFilters;
43
+ params: string[];
44
+ ids: string[];
45
+ total?: number;
46
+ }[] = [
47
+ { name: "defaults and priority tie", filters: {}, params: [], ids: ["c", "a", "b"] },
48
+ { name: "single status", filters: { status: "completed" }, params: ["completed"], ids: ["b"] },
49
+ { name: "empty statuses", filters: { status: [] }, params: [], ids: ["c", "a", "b"] },
50
+ {
51
+ name: "singleton statuses",
52
+ filters: { status: ["pending"] },
53
+ params: ["pending"],
54
+ ids: ["c", "a"],
55
+ },
56
+ {
57
+ name: "status array ordering",
58
+ filters: { status: ["completed", "pending"] },
59
+ params: ["completed", "pending"],
60
+ ids: ["c", "a", "b"],
61
+ },
62
+ {
63
+ name: "requester",
64
+ filters: { requestedByUserId: "requester" },
65
+ params: ["requester"],
66
+ ids: ["c", "b"],
67
+ },
68
+ {
69
+ name: "null wins",
70
+ filters: { requestedByUserId: "requester", requestedByUserIdIsNull: true },
71
+ params: [],
72
+ ids: ["a"],
73
+ },
74
+ { name: "search", filters: { search: "b" }, params: ["%b%", "%b%"], ids: ["b"] },
75
+ {
76
+ name: "combined ordering",
77
+ filters: {
78
+ status: ["pending", "completed"],
79
+ search: "needle",
80
+ taskType: "feature",
81
+ tags: ["blue"],
82
+ source: ["api"],
83
+ requestedByUserId: "requester",
84
+ },
85
+ params: [
86
+ "pending",
87
+ "completed",
88
+ "%needle%",
89
+ "%needle%",
90
+ "feature",
91
+ '%"blue"%',
92
+ "api",
93
+ "requester",
94
+ ],
95
+ ids: ["c", "b"],
96
+ },
97
+ {
98
+ name: "include heartbeat",
99
+ filters: { includeHeartbeat: true },
100
+ params: [],
101
+ ids: ["z", "k", "t", "h", "c", "a", "b"],
102
+ },
103
+ {
104
+ name: "created rowid tie",
105
+ filters: { orderBy: "createdAt" },
106
+ params: [],
107
+ ids: ["c", "b", "a"],
108
+ },
109
+ { name: "pagination", filters: { limit: 1, offset: 1 }, params: [], ids: ["a"], total: 3 },
110
+ { name: "zero limit", filters: { limit: 0 }, params: [], ids: [], total: 3 },
111
+ {
112
+ name: "namespace prefix",
113
+ filters: { keyPrefix: "shared/demo/" },
114
+ params: ["shared/demo/%"],
115
+ ids: ["c", "a", "b"],
116
+ },
117
+ ];
118
+
119
+ for (const { name, filters, params, ids, total } of cases) {
120
+ test(name, async () => {
121
+ const client = db.getDbClient();
122
+ const listSpy = spyOn(client, "query");
123
+ const countSpy = spyOn(client, "get");
124
+ try {
125
+ expect((await db.getAllTasks(filters)).map((task) => task.id)).toEqual(ids);
126
+ expect(listSpy.mock.calls[0]?.[1]).toEqual(params);
127
+ const sql = listSpy.mock.calls[0]?.[0] ?? "";
128
+ expect(sql).toContain(`LIMIT ${filters.limit ?? 25} OFFSET ${filters.offset ?? 0}`);
129
+ expect(sql).toContain(
130
+ filters.orderBy === "createdAt"
131
+ ? "ORDER BY createdAt DESC, rowid DESC"
132
+ : "ORDER BY lastUpdatedAt DESC, priority DESC",
133
+ );
134
+ expect(await db.getTasksCount(filters)).toBe(total ?? ids.length);
135
+ expect(countSpy.mock.calls[0]?.[1]).toEqual(params);
136
+ expect((await db.getAllTasks({ ...filters, limit: 100, offset: 0 })).length).toBe(
137
+ await db.getTasksCount(filters),
138
+ );
139
+ expect((await db.getAllTasks(filters, { slim: true })).map((task) => task.id)).toEqual(ids);
140
+ } finally {
141
+ listSpy.mockRestore();
142
+ countSpy.mockRestore();
143
+ }
144
+ });
145
+ }
146
+
147
+ test("facade dependency callback preserves post-pagination readiness and propagates errors", async () => {
148
+ await db.getDbClient().run("UPDATE agent_tasks SET dependsOn = '[\"a\"]' WHERE id = 'c'");
149
+ expect(await db.getAllTasks({ readyOnly: true, limit: 1 })).toEqual([]);
150
+ expect(await db.getAllTasks({ readyOnly: true, limit: 1 }, { slim: true })).toEqual([]);
151
+ await db.getDbClient().run("UPDATE agent_tasks SET status = 'completed' WHERE id = 'a'");
152
+ expect((await db.getAllTasks({ readyOnly: true, limit: 1 })).map((t) => t.id)).toEqual(["c"]);
153
+ await db.getDbClient().run("UPDATE agent_tasks SET tags = '{' WHERE id = 'a'");
154
+ await expect(db.getAllTasks({ readyOnly: true, limit: 1 })).rejects.toThrow();
155
+ });
156
+
157
+ test("row defaults, malformed guarded JSON and slim preview remain unchanged", async () => {
158
+ await db
159
+ .getDbClient()
160
+ .run(
161
+ "UPDATE agent_tasks SET task = ?, followUpConfig = '{', routingAffinity = '{' WHERE id = 'a'",
162
+ ["x".repeat(400)],
163
+ );
164
+ const warning = spyOn(console, "warn").mockImplementation(() => {});
165
+ try {
166
+ const task = await db.getTaskById("a");
167
+ expect(task).toMatchObject({
168
+ tags: [],
169
+ dependsOn: [],
170
+ slackReplySent: false,
171
+ wasPaused: false,
172
+ routingAffinityInvalid: true,
173
+ });
174
+ expect(task?.followUpConfig).toBeUndefined();
175
+ expect(task?.requestedByUserId).toBeUndefined();
176
+ const slim = (await db.getAllTasks({ search: "a" }, { slim: true }))[0];
177
+ expect(slim?.task.length).toBeLessThanOrEqual(301);
178
+ expect(slim).not.toHaveProperty("output");
179
+ expect(await db.getTaskById("missing")).toBeNull();
180
+ } finally {
181
+ warning.mockRestore();
182
+ }
183
+ });
@@ -0,0 +1,230 @@
1
+ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
2
+ import * as db from "../be/db";
3
+ import { telemetry } from "../telemetry";
4
+
5
+ const agentId = "bbbb0000-0000-4000-8000-000000000003";
6
+ const stamp = "2025-01-02T03:04:05.000Z";
7
+ let events: string[];
8
+ let taskEventSpy: ReturnType<typeof spyOn>;
9
+
10
+ async function flush(): Promise<void> {
11
+ for (let i = 0; i < 5; i++) await Bun.sleep(0);
12
+ }
13
+
14
+ beforeEach(async () => {
15
+ db.closeDb();
16
+ db.initDb(":memory:");
17
+ await db.createAgent({ id: agentId, name: "Write fixtures", isLead: false, status: "idle" });
18
+ events = [];
19
+ taskEventSpy = spyOn(telemetry, "taskEvent").mockImplementation((event) => {
20
+ events.push(event);
21
+ });
22
+ });
23
+ afterEach(async () => {
24
+ await flush();
25
+ taskEventSpy.mockRestore();
26
+ db.closeDb();
27
+ });
28
+
29
+ async function fixture() {
30
+ const task = await db.createTask(agentId, "write fixture");
31
+ await db.updateTaskClaudeSessionId(
32
+ task.id,
33
+ "session-03",
34
+ "codex",
35
+ { fixture: true },
36
+ "fixture-model",
37
+ "codex",
38
+ { version: 3 },
39
+ );
40
+ await db
41
+ .getDbClient()
42
+ .run("UPDATE agent_tasks SET createdAt = ?, lastUpdatedAt = ? WHERE id = ?", [
43
+ stamp,
44
+ stamp,
45
+ task.id,
46
+ ]);
47
+ return (await db.getTaskById(task.id))!;
48
+ }
49
+
50
+ // These same fixtures also run against the merged base before extraction.
51
+ describe("task write base behavior", () => {
52
+ test("create, start, pause and resume retain session identity and lifecycle timestamps", async () => {
53
+ const task = await fixture();
54
+ expect(task.status).toBe("pending");
55
+ expect(task.finishedAt).toBeUndefined();
56
+ expect(await db.getPendingTaskForAgent(agentId)).toEqual(task);
57
+ expect(await db.pauseTask(task.id)).toBeNull();
58
+ for (const [operation, status] of [
59
+ [db.startTask, "in_progress"],
60
+ [db.pauseTask, "paused"],
61
+ [db.resumeTask, "in_progress"],
62
+ ] as const) {
63
+ const result = (await operation(task.id))!;
64
+ expect(result.status).toBe(status);
65
+ expect(result.claudeSessionId).toBe("session-03");
66
+ expect(result.createdAt).toBe(stamp);
67
+ expect(result.lastUpdatedAt > stamp).toBe(true);
68
+ expect(result.finishedAt).toBeUndefined();
69
+ expect(result.provider).toBe("codex");
70
+ expect(result.harnessVariantMeta).toEqual({ version: 3 });
71
+ }
72
+ expect(await db.resumeTask(task.id)).toBeNull();
73
+ await flush();
74
+ expect(events).toEqual([]);
75
+ expect((await db.getLogsByTaskId(task.id)).map((log) => log.newValue).sort()).toEqual(
76
+ ["in_progress", "in_progress", "paused", "pending"].sort(),
77
+ );
78
+ });
79
+
80
+ for (const status of ["completed", "failed", "cancelled", "superseded"] as const) {
81
+ test(`${status} preserves timestamps and session, emits once and rejects repeated terminal transitions`, async () => {
82
+ const task = await fixture();
83
+ await db.startTask(task.id);
84
+ const terminalize =
85
+ status === "completed"
86
+ ? (id: string) => db.completeTask(id, "done")
87
+ : status === "failed"
88
+ ? (id: string) => db.failTask(id, "failed fixture")
89
+ : status === "cancelled"
90
+ ? (id: string) => db.cancelTask(id)
91
+ : (id: string) => db.supersedeTask(id, { reason: "restart", resumeTaskId: null });
92
+ const result = (await terminalize(task.id))!;
93
+ expect(result.status).toBe(status);
94
+ expect(result.createdAt).toBe(stamp);
95
+ expect(result.finishedAt! > stamp).toBe(true);
96
+ expect(result.lastUpdatedAt > stamp).toBe(true);
97
+ expect(result.claudeSessionId).toBe("session-03");
98
+ for (const operation of [
99
+ db.startTask,
100
+ db.pauseTask,
101
+ db.resumeTask,
102
+ db.completeTask,
103
+ (id: string) => db.failTask(id, "again"),
104
+ db.cancelTask,
105
+ (id: string) => db.supersedeTask(id, { reason: "again", resumeTaskId: null }),
106
+ ]) {
107
+ expect(await operation(task.id)).toBeNull();
108
+ }
109
+ expect(await db.getTaskById(task.id)).toEqual(result);
110
+ const updated = (await db.updateTaskProgress(task.id, "late progress"))!;
111
+ expect(updated.status).toBe(status);
112
+ expect(updated.finishedAt).toBe(result.finishedAt);
113
+ expect(updated.claudeSessionId).toBe("session-03");
114
+ await flush();
115
+ expect(events).toEqual([status]);
116
+ });
117
+ }
118
+
119
+ test("missing ids retain null, false and empty-list results", async () => {
120
+ for (const operation of [
121
+ db.startTask,
122
+ db.pauseTask,
123
+ db.resumeTask,
124
+ db.completeTask,
125
+ db.cancelTask,
126
+ (id: string) => db.failTask(id, "missing"),
127
+ (id: string) => db.supersedeTask(id, { reason: "missing", resumeTaskId: null }),
128
+ (id: string) => db.updateTaskClaudeSessionId(id, "missing"),
129
+ (id: string) => db.updateTaskTitle(id, "missing"),
130
+ (id: string) => db.updateTaskProgress(id, "missing"),
131
+ (id: string) => db.overwriteTerminalTaskResultText(id, { output: "missing" }),
132
+ (id: string) => db.assignUnassignedTaskPending(id, agentId),
133
+ ]) {
134
+ expect(await operation("missing")).toBeNull();
135
+ }
136
+ expect(await db.deleteTask("missing")).toBe(false);
137
+ expect(await db.backfillSupersedeTaskResumeTaskId("missing", "resume")).toBe(false);
138
+ expect(await db.getPendingTaskForAgent("missing")).toBeNull();
139
+ expect(await db.getPausedTasksForAgent("missing")).toEqual([]);
140
+ expect(await db.getOrphanedInProgressTasksForAgent("missing")).toEqual([]);
141
+ expect(await db.resetOrphanedInProgressTasksForAgent("missing")).toEqual([]);
142
+ expect(await db.getRecentlyCancelledTasksForAgent("missing")).toEqual([]);
143
+ await flush();
144
+ expect(events).toEqual([]);
145
+ });
146
+
147
+ test("rename and terminal text overwrite preserve activity timestamps", async () => {
148
+ const task = await fixture();
149
+ expect((await db.updateTaskTitle(task.id, " title "))?.title).toBe("title");
150
+ expect((await db.updateTaskTitle(task.id, " "))?.title).toBeUndefined();
151
+ expect((await db.getTaskById(task.id))?.lastUpdatedAt).toBe(stamp);
152
+ expect(await db.overwriteTerminalTaskResultText(task.id, { output: "early" })).toBeNull();
153
+ const completed = (await db.completeTask(task.id, "original"))!;
154
+ const edited = (await db.overwriteTerminalTaskResultText(task.id, { output: "edited" }))!;
155
+ expect(edited.output).toBe("edited");
156
+ expect(edited.lastUpdatedAt).toBe(completed.lastUpdatedAt);
157
+ expect(edited.finishedAt).toBe(completed.finishedAt);
158
+ await flush();
159
+ expect(events).toEqual(["completed"]);
160
+ });
161
+
162
+ test("assignment, orphan recovery, paused and cancelled queries, supersede backfill and deletion", async () => {
163
+ const task = await fixture();
164
+ await db
165
+ .getDbClient()
166
+ .run(
167
+ "UPDATE agent_tasks SET agentId = NULL, status = 'unassigned', claudeSessionId = NULL WHERE id = ?",
168
+ [task.id],
169
+ );
170
+ expect((await db.assignUnassignedTaskPending(task.id, agentId))?.status).toBe("pending");
171
+ expect(await db.assignUnassignedTaskPending(task.id, agentId)).toBeNull();
172
+ await db.startTask(task.id);
173
+ await db
174
+ .getDbClient()
175
+ .run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [stamp, task.id]);
176
+ expect((await db.getOrphanedInProgressTasksForAgent(agentId)).map((t) => t.id)).toEqual([
177
+ task.id,
178
+ ]);
179
+ expect((await db.resetOrphanedInProgressTasksForAgent(agentId)).map((t) => t.status)).toEqual([
180
+ "pending",
181
+ ]);
182
+ await db.startTask(task.id);
183
+ await db.pauseTask(task.id);
184
+ expect((await db.getPausedTasksForAgent(agentId)).map((t) => t.id)).toEqual([task.id]);
185
+ await db.resumeTask(task.id);
186
+ await db.cancelTask(task.id);
187
+ expect((await db.getRecentlyCancelledTasksForAgent(agentId)).map((t) => t.id)).toEqual([
188
+ task.id,
189
+ ]);
190
+ const superseded = await fixture();
191
+ await db.supersedeTask(superseded.id, { reason: "restart", resumeTaskId: null });
192
+ expect(await db.backfillSupersedeTaskResumeTaskId(superseded.id, "resume-id")).toBe(true);
193
+ const log = (await db.getLogsByTaskId(superseded.id)).find(
194
+ (l) => l.eventType === "task_superseded",
195
+ )!;
196
+ expect(JSON.parse(log.metadata!).resumeTaskId).toBe("resume-id");
197
+ await flush();
198
+ expect(await db.deleteTask(task.id)).toBe(true);
199
+ expect(await db.getTaskById(task.id)).toBeNull();
200
+ });
201
+ });
202
+
203
+ test("racing terminal transitions emit only for the winning write", async () => {
204
+ const task = await fixture();
205
+ const results = await Promise.all([
206
+ db.completeTask(task.id, "winner"),
207
+ db.failTask(task.id, "failure"),
208
+ db.cancelTask(task.id),
209
+ db.supersedeTask(task.id, { reason: "restart", resumeTaskId: null }),
210
+ ]);
211
+ const winners = results.filter((result) => result !== null);
212
+ expect(winners).toHaveLength(1);
213
+ expect((await db.getTaskById(task.id))?.status).toBe(winners[0]!.status);
214
+ await flush();
215
+ expect(events).toEqual([winners[0]!.status]);
216
+ });
217
+
218
+ test("outer rollback restores the task and discards terminal telemetry and logs", async () => {
219
+ const task = await fixture();
220
+ await expect(
221
+ db.getDbClient().transaction(async () => {
222
+ await db.completeTask(task.id, "rolled back");
223
+ throw new Error("rollback fixture");
224
+ }),
225
+ ).rejects.toThrow("rollback fixture");
226
+ await flush();
227
+ expect(await db.getTaskById(task.id)).toEqual(task);
228
+ expect(events).toEqual([]);
229
+ expect((await db.getLogsByTaskId(task.id)).map((log) => log.newValue)).toEqual(["pending"]);
230
+ });