@desplega.ai/agent-swarm 1.157.0 → 1.158.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/dist/{acp-adapter-qhqrk4q0.js → acp-adapter-xaq4n6pc.js} +1 -1
  2. package/dist/{actions-9rrg0zxm.js → actions-p9hz5yat.js} +15 -15
  3. package/dist/{app-enysy1zd.js → app-g26wdcz6.js} +4 -4
  4. package/dist/{artifact-td4q4dch.js → artifact-3v5t9jtg.js} +1 -1
  5. package/dist/{assistant-zegamy48.js → assistant-wydfqkb3.js} +16 -16
  6. package/dist/{boot-reembed-r6shx5qx.js → boot-reembed-g5ckg9yb.js} +6 -6
  7. package/dist/{boot-reembed-zfxb937k.js → boot-reembed-se8zmdcp.js} +5 -5
  8. package/dist/{boot-scrub-logs-k5y0se6r.js → boot-scrub-logs-wfxqan61.js} +3 -3
  9. package/dist/{seed-pricing-vqxa06qf.js → boot-seeds-s3q01xj3.js} +10 -10
  10. package/dist/{claude-adapter-5yc7hxwv.js → claude-adapter-tv2x4f36.js} +7 -7
  11. package/dist/{claude-managed-adapter-ea2aga6w.js → claude-managed-adapter-41b2s7fd.js} +2 -2
  12. package/dist/{claude-sdk-session-j4ywn3xf.js → claude-sdk-session-t442qz6f.js} +8 -8
  13. package/dist/{cli-e781c6f9.js → cli-0xjkj1j7.js} +1 -0
  14. package/dist/{cli-58rgvkt4.js → cli-1hkrch3k.js} +2 -2
  15. package/dist/{cli-yscm6cv8.js → cli-20cj37vt.js} +1866 -9
  16. package/dist/{cli-h0wdadsx.js → cli-4198qc9v.js} +7 -3
  17. package/dist/{cli-awwphhcs.js → cli-47m6dkd3.js} +362 -126
  18. package/dist/{cli-wb2zvbfg.js → cli-53sz8gkz.js} +1 -1
  19. package/dist/{cli-005pze6s.js → cli-5d10vd2m.js} +1 -1
  20. package/dist/{cli-v53rbqp3.js → cli-65hjmev5.js} +57 -17
  21. package/dist/{cli-scwq51gn.js → cli-68w09sjw.js} +7 -1
  22. package/dist/{cli-s98skd5r.js → cli-7gtvvpbh.js} +4 -4
  23. package/dist/{cli-y3nxwx0t.js → cli-7s3syk22.js} +18 -11
  24. package/dist/{cli-74h6f2bp.js → cli-8qmme6vd.js} +2 -2
  25. package/dist/{cli-wsed24mk.js → cli-987hg0aa.js} +2 -2
  26. package/dist/{cli-9n76jszt.js → cli-9httdpyq.js} +1 -1
  27. package/dist/{cli-f3va018d.js → cli-aky8tctm.js} +26 -93
  28. package/dist/{cli-sgyqyyxe.js → cli-c07r9a2m.js} +2 -1
  29. package/dist/{cli-c41wjx0r.js → cli-fgqxwrpv.js} +291 -12
  30. package/dist/{cli-01vfmq8c.js → cli-ftfvggzx.js} +607 -164
  31. package/dist/{cli-ytxs7mn1.js → cli-g8djp6qe.js} +17 -0
  32. package/dist/{cli-avkd6mqv.js → cli-h967nb6r.js} +551 -0
  33. package/dist/{cli-r9ee9c4w.js → cli-j55n64b6.js} +23 -22
  34. package/dist/{cli-9p33cz9h.js → cli-j57qkrpq.js} +86 -28
  35. package/dist/{cli-tjmghmm5.js → cli-q67y4wzm.js} +6 -3
  36. package/dist/{cli-n6zq3kbd.js → cli-rj19hc34.js} +1 -1
  37. package/dist/{cli-jytrw2na.js → cli-rjehvrts.js} +4 -4
  38. package/dist/{cli-tpv97m71.js → cli-rqz8qbw7.js} +245 -75
  39. package/dist/{cli-p37sg0a9.js → cli-s8eet254.js} +1 -1
  40. package/dist/cli-t1ze64h3.js +811 -0
  41. package/dist/{cli-6jjzxv3b.js → cli-ws6rqycm.js} +3 -2
  42. package/dist/{cli-xf5b7y1f.js → cli-wscdvcqz.js} +13 -5
  43. package/dist/{cli-5j7bm4tb.js → cli-x24pshtq.js} +5 -5
  44. package/dist/{cli-nbp6m6gp.js → cli-xberxa1r.js} +51 -26
  45. package/dist/{cli-vx9p3qdt.js → cli-xhaeg2nc.js} +1 -1
  46. package/dist/cli.js +29 -15
  47. package/dist/{codex-adapter-gns3s6c3.js → codex-adapter-cnm8wk11.js} +5 -5
  48. package/dist/{codex-session-runner-gqnxys5w.js → codex-session-runner-gfg547zj.js} +5 -5
  49. package/dist/{commands-hxy2y2sp.js → commands-e3bn0p75.js} +3 -3
  50. package/dist/{db-w8ghhsgf.js → db-03v9kzr2.js} +17 -3
  51. package/dist/{dispatcher-cp1rknjz.js → dispatcher-nrmg8yht.js} +14 -14
  52. package/dist/{dsh-adapter-1dwg97zv.js → dsh-adapter-k03nfqa6.js} +2 -2
  53. package/dist/{handlers-egfhr351.js → handlers-aqcedbyb.js} +17 -17
  54. package/dist/{hook-x4kftwzt.js → hook-7vkskkre.js} +19 -11
  55. package/dist/{hook-fbxjqfjz.js → hook-ybvmrmhq.js} +4 -4
  56. package/dist/{http-c7jfx990.js → http-2eqa8t24.js} +432 -195
  57. package/dist/{index-4k9hct21.js → index-1yks3qcr.js} +14 -14
  58. package/dist/{index-jtvpgb8f.js → index-a3vn96n8.js} +30 -27
  59. package/dist/{index-dc2ek6bw.js → index-c6zsv1bd.js} +5 -5
  60. package/dist/{index-yy07ef2s.js → index-chgdkq36.js} +14 -14
  61. package/dist/{index-r6bty6tb.js → index-x71bvq0p.js} +15 -15
  62. package/dist/{keepalive-f7ekefyc.js → keepalive-vtkf5n99.js} +5 -5
  63. package/dist/{lead-h053syts.js → lead-h3sp357x.js} +26 -26
  64. package/dist/{maintenance-a3w6zs54.js → maintenance-jzeqtk3q.js} +6 -6
  65. package/dist/{oauth-refresh-sweep-vra1dhkd.js → oauth-refresh-sweep-qp3dzmdp.js} +4 -4
  66. package/dist/{onboard-mfgzeqxs.js → onboard-tmsp0qbz.js} +278 -81
  67. package/dist/{opencode-adapter-z64xjzec.js → opencode-adapter-x6x2da1z.js} +3 -3
  68. package/dist/{otel-impl-2ez6xk57.js → otel-impl-9mz73q65.js} +416 -301
  69. package/dist/{pi-mono-adapter-2b7mw16r.js → pi-mono-adapter-m6dvv0dz.js} +2 -2
  70. package/dist/{pricing-refresh-6sw65tck.js → pricing-refresh-v1cbr9te.js} +5 -5
  71. package/dist/{rbac-roles-nm6924sy.js → rbac-roles-c9wqz83x.js} +4 -4
  72. package/dist/{render-v2-f026j6c4.js → render-v2-gryrh9h8.js} +4 -4
  73. package/dist/{setup-w606fr1q.js → setup-q9720bt6.js} +2 -2
  74. package/dist/{virtual-modules-8qheft18.js → virtual-modules-3htpgn8x.js} +1 -1
  75. package/dist/{work-objects-ezmggdne.js → work-objects-b76x7deb.js} +17 -17
  76. package/dist/{worker-1mbyv29k.js → worker-6vrxbv41.js} +26 -26
  77. package/openapi.json +1626 -629
  78. package/package.json +2 -1
  79. package/src/apps/catalog.generated.json +229 -0
  80. package/src/apps/page-validator.ts +71 -16
  81. package/src/apps/row-store.ts +66 -11
  82. package/src/apps/sync.ts +59 -17
  83. package/src/artifact-sdk/browser-sdk.ts +436 -2
  84. package/src/be/approval-sweeps.ts +109 -0
  85. package/src/be/boot-seeds.ts +40 -0
  86. package/src/be/chunking.ts +1 -1
  87. package/src/be/db/agents.ts +20 -0
  88. package/src/be/db/approvals.ts +78 -0
  89. package/src/be/db/tasks/read.ts +197 -39
  90. package/src/be/db/tasks/write.ts +41 -9
  91. package/src/be/db-client.ts +107 -48
  92. package/src/be/db.ts +219 -76
  93. package/src/be/events.ts +33 -1
  94. package/src/be/memory/graph-expansion.ts +10 -3
  95. package/src/be/memory/providers/sqlite-store.ts +55 -13
  96. package/src/be/memory/raters/retrieval.ts +5 -2
  97. package/src/be/memory/reranker.ts +26 -2
  98. package/src/be/memory/types.ts +2 -0
  99. package/src/be/migrations/166_approval_request_auto_cancellation.sql +9 -0
  100. package/src/be/migrations/171_memory_retrieval_relevance.sql +8 -0
  101. package/src/be/migrations/175_workflow_runs_list_index.sql +11 -0
  102. package/src/be/migrations/177_events_event_agent_created_index.sql +11 -0
  103. package/src/be/models-catalog.ts +2 -0
  104. package/src/be/modelsdev-cache.json +1857 -0
  105. package/src/be/modelsdev-cache.ts +14 -1
  106. package/src/be/pricing-normalize.ts +2 -0
  107. package/src/be/scripts/typecheck.ts +1 -0
  108. package/src/be/seed-pricing.ts +3 -1
  109. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  110. package/src/be/steering.ts +53 -3
  111. package/src/be/swarm-config-guard.ts +3 -0
  112. package/src/be/task-citations.ts +29 -0
  113. package/src/cli.tsx +14 -1
  114. package/src/commands/onboard/compose-generator.ts +8 -3
  115. package/src/commands/onboard/container-engine.ts +220 -0
  116. package/src/commands/onboard/manifest.ts +1 -0
  117. package/src/commands/onboard/steps/health-check.tsx +4 -1
  118. package/src/commands/onboard/steps/prereq-check.tsx +50 -54
  119. package/src/commands/onboard/steps/start.tsx +31 -16
  120. package/src/commands/onboard/types.ts +7 -2
  121. package/src/commands/onboard.tsx +39 -4
  122. package/src/commands/profile-sync.ts +79 -26
  123. package/src/commands/runner.ts +8 -1
  124. package/src/extensions/catalog.generated.json +17 -0
  125. package/src/extensions/contract-types.generated.ts +1 -1
  126. package/src/heartbeat/heartbeat.ts +263 -103
  127. package/src/hooks/hook.ts +21 -12
  128. package/src/http/agents.ts +35 -0
  129. package/src/http/approval-requests.ts +136 -64
  130. package/src/http/apps.ts +14 -3
  131. package/src/http/config.ts +15 -1
  132. package/src/http/events.ts +62 -2
  133. package/src/http/index.ts +6 -4
  134. package/src/http/memory.ts +21 -7
  135. package/src/http/schedules.ts +1 -1
  136. package/src/http/session-data.ts +9 -4
  137. package/src/http/sessions.ts +25 -18
  138. package/src/http/stats.ts +19 -7
  139. package/src/http/status.ts +11 -1
  140. package/src/http/tasks.ts +40 -8
  141. package/src/http/webhooks.ts +67 -46
  142. package/src/http/workflows.ts +15 -20
  143. package/src/linear/outbound.ts +37 -15
  144. package/src/otel-impl.ts +151 -1
  145. package/src/otel.ts +14 -3
  146. package/src/prompts/memories.ts +19 -2
  147. package/src/providers/claude-managed-models.ts +8 -0
  148. package/src/providers/modelsdev-reasoning.json +551 -0
  149. package/src/rbac/legacy-policy.ts +16 -0
  150. package/src/rbac/permissions.ts +4 -0
  151. package/src/scheduler/schedule-task.ts +3 -0
  152. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  153. package/src/scripts-runtime/swarm-sdk.ts +9 -0
  154. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  155. package/src/scripts-runtime/types/swarm-extension.d.ts +1 -0
  156. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  157. package/src/server.ts +12 -7
  158. package/src/slack/app.ts +35 -4
  159. package/src/slack/connection-state.ts +17 -0
  160. package/src/tasks/context-key.ts +1 -0
  161. package/src/tasks/worker-follow-up.ts +6 -2
  162. package/src/tests/agent-task-activity.test.ts +74 -0
  163. package/src/tests/agents-list-model-display.test.ts +6 -1
  164. package/src/tests/approval-auto-cancellation-migration.test.ts +104 -0
  165. package/src/tests/approval-requests.test.ts +395 -0
  166. package/src/tests/approval-sweeps-config.test.ts +47 -0
  167. package/src/tests/approval-sweeps.test.ts +508 -0
  168. package/src/tests/apps-kibo-blocks.test.ts +77 -0
  169. package/src/tests/apps-spike.test.ts +56 -1
  170. package/src/tests/apps-sync-engine.test.ts +20 -9
  171. package/src/tests/boot-seeds.test.ts +170 -0
  172. package/src/tests/cancel-approval-request-tool.test.ts +115 -0
  173. package/src/tests/claude-managed-adapter.test.ts +6 -0
  174. package/src/tests/config-key-filter.test.ts +80 -0
  175. package/src/tests/context-window.test.ts +1 -0
  176. package/src/tests/db-client-busy-retry.test.ts +96 -0
  177. package/src/tests/db-client.test.ts +60 -1
  178. package/src/tests/db-task-read-contract.test.ts +10 -0
  179. package/src/tests/db-task-write-contract.test.ts +9 -0
  180. package/src/tests/docs-index-footprint.test.js +3 -3
  181. package/src/tests/events-http.test.ts +103 -0
  182. package/src/tests/extensions-catalog.test.ts +22 -17
  183. package/src/tests/extensions-pre-task.test.ts +107 -0
  184. package/src/tests/fixtures/kibo-blocks-definition.json.txt +105 -0
  185. package/src/tests/follow-up-config-inheritance.test.ts +260 -0
  186. package/src/tests/heartbeat-orphan-resume.test.ts +84 -0
  187. package/src/tests/heartbeat-reboot-live.test.ts +95 -0
  188. package/src/tests/heartbeat-stall-cas.test.ts +80 -0
  189. package/src/tests/heartbeat.test.ts +99 -3
  190. package/src/tests/hook-mcp-config.test.ts +29 -0
  191. package/src/tests/http-api-integration.test.ts +16 -2
  192. package/src/tests/kv-storage.test.ts +62 -0
  193. package/src/tests/linear-outbound-sync.test.ts +120 -5
  194. package/src/tests/list-endpoint-slimming.test.ts +91 -0
  195. package/src/tests/memory-graph-organic-route.test.ts +155 -0
  196. package/src/tests/memory-http-recall-gating.test.ts +14 -4
  197. package/src/tests/memory-hybrid.test.ts +161 -1
  198. package/src/tests/memory-prompt-recall-crowdout.test.ts +297 -0
  199. package/src/tests/memory.test.ts +22 -0
  200. package/src/tests/onboard-container-engine.test.ts +240 -0
  201. package/src/tests/otel-event-loop-and-logs.test.ts +99 -0
  202. package/src/tests/pricing-routes.test.ts +14 -0
  203. package/src/tests/profile-sync-warning.test.ts +8 -4
  204. package/src/tests/promote-draft-task-route.test.ts +19 -0
  205. package/src/tests/provider-registration.test.ts +296 -42
  206. package/src/tests/rbac-engine.test.ts +30 -0
  207. package/src/tests/reasoning-effort.test.ts +8 -0
  208. package/src/tests/script-connections.test.ts +7 -0
  209. package/src/tests/sdk-allowlist.test.ts +39 -0
  210. package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
  211. package/src/tests/sessions-detail-batch.test.ts +250 -0
  212. package/src/tests/sessions.test.ts +118 -0
  213. package/src/tests/slack-config.test.ts +22 -0
  214. package/src/tests/sonnet-5-5-registry.test.ts +44 -0
  215. package/src/tests/status.test.ts +36 -1
  216. package/src/tests/swarm-diff.test.ts +316 -10
  217. package/src/tests/task-completion-idempotency.test.ts +2 -1
  218. package/src/tests/tasks-requested-by-filter.test.ts +3 -1
  219. package/src/tests/tool-annotations.test.ts +1 -1
  220. package/src/tests/ui-list-slim-projections.test.ts +158 -0
  221. package/src/tests/webhooks-task-blocked.test.ts +127 -0
  222. package/src/tests/workflow-approval-recovery.test.ts +203 -0
  223. package/src/tests/workflow-hitl-routing.test.ts +22 -1
  224. package/src/tests/workflow-hitl-slack-card.test.ts +52 -0
  225. package/src/tests/workflow-http-v2.test.ts +99 -8
  226. package/src/tests/workflow-retry-validation.test.ts +56 -0
  227. package/src/tests/workflow-tla-races.test.ts +389 -0
  228. package/src/tests/workflow-validation-port-routing.test.ts +113 -1
  229. package/src/tools/cancel-approval-request.ts +57 -0
  230. package/src/tools/memory-search.ts +1 -0
  231. package/src/tools/request-human-input.ts +4 -2
  232. package/src/tools/send-task.ts +4 -0
  233. package/src/tools/templates.ts +19 -0
  234. package/src/tools/tool-config.ts +2 -1
  235. package/src/tools/workflows/list-workflow-runs.ts +1 -0
  236. package/src/types.ts +38 -0
  237. package/src/utils/context-window.ts +1 -0
  238. package/src/workflows/approval-cancel.ts +152 -0
  239. package/src/workflows/approval-notifications.ts +84 -0
  240. package/src/workflows/checkpoint.ts +10 -5
  241. package/src/workflows/definition.ts +21 -0
  242. package/src/workflows/engine.ts +152 -21
  243. package/src/workflows/executors/human-in-the-loop.ts +1 -1
  244. package/src/workflows/recovery.ts +100 -45
  245. package/src/workflows/resume.ts +49 -18
  246. package/src/workflows/retry-poller.ts +71 -21
  247. package/templates/extensions/README.md +1 -0
  248. package/templates/extensions/github-sender-allowlist/README.md +18 -0
  249. package/templates/extensions/github-sender-allowlist/hooks.ts +55 -0
  250. package/templates/extensions/github-sender-allowlist/manifest.json +8 -0
  251. package/templates/skills/apps/content.md +20 -1
  252. package/templates/skills/code-quality/content.md +26 -1
  253. package/templates/skills/pages/content.md +16 -4
  254. package/templates/skills/pages/files/examples/multiplayer-board.html +3 -12
  255. package/dist/cli-2daksqps.js +0 -378
  256. package/dist/rbac-roles-3gxmnb20.js +0 -37
@@ -0,0 +1,78 @@
1
+ import { normalizeDate, normalizeDateRequired } from "../date-utils";
2
+ import { getDbClient } from "./runtime";
3
+
4
+ export interface ApprovalRequestListFilters {
5
+ status?: string;
6
+ workflowRunId?: string;
7
+ limit?: number;
8
+ }
9
+
10
+ /** WHERE + LIMIT shared by the full and slim approval list reads (default limit 100). */
11
+ export function approvalRequestListClause(filters?: ApprovalRequestListFilters): {
12
+ sql: string;
13
+ params: (string | number)[];
14
+ } {
15
+ const conditions: string[] = [];
16
+ const params: (string | number)[] = [];
17
+ if (filters?.status) {
18
+ conditions.push("status = ?");
19
+ params.push(filters.status);
20
+ }
21
+ if (filters?.workflowRunId) {
22
+ conditions.push("workflowRunId = ?");
23
+ params.push(filters.workflowRunId);
24
+ }
25
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
26
+ params.push(filters?.limit ?? 100);
27
+ return { sql: `${where} ORDER BY createdAt DESC LIMIT ?`, params };
28
+ }
29
+
30
+ /**
31
+ * Slim approval list row: what the approvals list page draws. The question
32
+ * bodies, answers, approvers and notification targets are dropped (read them
33
+ * via `getApprovalRequestById`); `questionCount` replaces `questions`.
34
+ */
35
+ export interface ApprovalRequestSummary {
36
+ id: string;
37
+ title: string;
38
+ questionCount: number;
39
+ workflowRunId: string | null;
40
+ workflowRunStepId: string | null;
41
+ sourceTaskId: string | null;
42
+ status: "pending" | "approved" | "rejected" | "timeout" | "cancelled";
43
+ resolvedBy: string | null;
44
+ resolvedAt: string | null;
45
+ timeoutSeconds: number | null;
46
+ expiresAt: string | null;
47
+ createdBy?: string;
48
+ createdAt: string;
49
+ updatedAt: string;
50
+ }
51
+
52
+ type ApprovalRequestSummaryRow = Omit<ApprovalRequestSummary, "questionCount" | "createdBy"> & {
53
+ questionCount: number | null;
54
+ created_by: string | null;
55
+ };
56
+
57
+ export async function listApprovalRequestSummaries(
58
+ filters?: ApprovalRequestListFilters,
59
+ ): Promise<ApprovalRequestSummary[]> {
60
+ const { sql, params } = approvalRequestListClause(filters);
61
+ // Project in SQL so the heavy JSON columns are never read or parsed.
62
+ const rows = await getDbClient().query<ApprovalRequestSummaryRow>(
63
+ `SELECT id, title, workflowRunId, workflowRunStepId, sourceTaskId, status, resolvedBy,
64
+ resolvedAt, timeoutSeconds, expiresAt, created_by, createdAt, updatedAt,
65
+ json_array_length(questions) AS questionCount
66
+ FROM approval_requests ${sql}`,
67
+ params,
68
+ );
69
+ return rows.map(({ created_by, questionCount, ...row }) => ({
70
+ ...row,
71
+ questionCount: questionCount ?? 0,
72
+ resolvedAt: normalizeDate(row.resolvedAt),
73
+ expiresAt: normalizeDate(row.expiresAt),
74
+ createdBy: created_by ?? undefined,
75
+ createdAt: normalizeDateRequired(row.createdAt),
76
+ updatedAt: normalizeDateRequired(row.updatedAt),
77
+ }));
78
+ }
@@ -5,6 +5,7 @@ import type {
5
5
  AgentTaskSource,
6
6
  AgentTaskStatus,
7
7
  AgentTaskSummary,
8
+ AgentTaskTimelineItem,
8
9
  FollowUpConfig,
9
10
  ProviderName,
10
11
  ReasoningEffort,
@@ -117,6 +118,97 @@ export type AgentTaskRow = {
117
118
  routingAffinity: string | null;
118
119
  };
119
120
 
121
+ /**
122
+ * `task` as the list projections read it: the full text, or the UTF-8 byte
123
+ * prefix `TASK_PREVIEW_SQL` selects.
124
+ */
125
+ type TaskPreviewSource = { task: string | Uint8Array | null };
126
+ type AgentTaskListRow = Omit<AgentTaskRow, "task"> & TaskPreviewSource;
127
+
128
+ /** Columns the slim list mapper reads; `task` may already be a SQL prefix. */
129
+ type AgentTaskSummaryRow = Pick<
130
+ AgentTaskListRow,
131
+ | "id"
132
+ | "key"
133
+ | "agentId"
134
+ | "creatorAgentId"
135
+ | "task"
136
+ | "title"
137
+ | "status"
138
+ | "source"
139
+ | "taskType"
140
+ | "tags"
141
+ | "priority"
142
+ | "dependsOn"
143
+ | "offeredTo"
144
+ | "acceptedAt"
145
+ | "parentTaskId"
146
+ | "scheduleId"
147
+ | "model"
148
+ | "modelTier"
149
+ | "effort"
150
+ | "provider"
151
+ | "requestedByUserId"
152
+ | "progress"
153
+ | "createdAt"
154
+ | "lastUpdatedAt"
155
+ | "finishedAt"
156
+ | "peakContextPercent"
157
+ | "totalCostUsd"
158
+ >;
159
+
160
+ type AgentTaskTimelineRow = Pick<
161
+ AgentTaskListRow,
162
+ | "id"
163
+ | "agentId"
164
+ | "parentTaskId"
165
+ | "task"
166
+ | "title"
167
+ | "status"
168
+ | "createdAt"
169
+ | "lastUpdatedAt"
170
+ | "finishedAt"
171
+ | "peakContextTokens"
172
+ | "totalCostUsd"
173
+ >;
174
+
175
+ /**
176
+ * SQL column lists for the list projections. `task` is cut to the UTF-8 bytes
177
+ * that can hold one char past the preview length (a UTF-16 unit is at most 4
178
+ * bytes), so the JS side never materializes the full prompt, output or
179
+ * provider blobs. The cut runs on the BLOB: SQLite's text `substr` stops at
180
+ * the first embedded NUL, which the full-text preview keeps. `taskPreview`
181
+ * decodes the bytes and truncates them exactly as it would the full text (the
182
+ * extra char keeps the "…" marker; a code point split at the byte cut lands
183
+ * past it).
184
+ */
185
+ const TASK_PREVIEW_BYTES = 4 * (TASK_PREVIEW_LENGTH + 1);
186
+ const TASK_PREVIEW_SQL = `substr(CAST(agent_tasks.task AS BLOB), 1, ${TASK_PREVIEW_BYTES}) AS task`;
187
+ // ignoreBOM keeps a leading U+FEFF, which the full-text preview keeps too.
188
+ const taskPreviewDecoder = new TextDecoder("utf-8", { ignoreBOM: true });
189
+
190
+ function taskPreview(task: TaskPreviewSource["task"]): string {
191
+ const text = task instanceof Uint8Array ? taskPreviewDecoder.decode(task) : task;
192
+ return dependencies.previewText(text, TASK_PREVIEW_LENGTH);
193
+ }
194
+
195
+ const LIST_COLUMNS = {
196
+ full: "agent_tasks.*",
197
+ slim: `agent_tasks.id, agent_tasks."key", agent_tasks.agentId, agent_tasks.creatorAgentId,
198
+ ${TASK_PREVIEW_SQL}, agent_tasks.title, agent_tasks.status, agent_tasks.source,
199
+ agent_tasks.taskType, agent_tasks.tags, agent_tasks.priority, agent_tasks.dependsOn,
200
+ agent_tasks.offeredTo, agent_tasks.acceptedAt, agent_tasks.parentTaskId,
201
+ agent_tasks.scheduleId, agent_tasks.model, agent_tasks.modelTier, agent_tasks.effort,
202
+ agent_tasks.provider, agent_tasks.requestedByUserId, agent_tasks.progress,
203
+ agent_tasks.createdAt, agent_tasks.lastUpdatedAt, agent_tasks.finishedAt,
204
+ agent_tasks.peakContextPercent`,
205
+ timeline: `agent_tasks.id, agent_tasks.agentId, agent_tasks.parentTaskId, ${TASK_PREVIEW_SQL},
206
+ agent_tasks.title, agent_tasks.status, agent_tasks.createdAt, agent_tasks.lastUpdatedAt,
207
+ agent_tasks.finishedAt, agent_tasks.peakContextTokens`,
208
+ } as const;
209
+
210
+ type TaskListFields = keyof typeof LIST_COLUMNS;
211
+
120
212
  export function rowToAgentTask(row: AgentTaskRow): AgentTask {
121
213
  let followUpConfig: FollowUpConfig | undefined;
122
214
  if (row.followUpConfig) {
@@ -251,36 +343,54 @@ export function rowToAgentTask(row: AgentTaskRow): AgentTask {
251
343
  * context-window fields). The preview is long enough for pool-triage; the full
252
344
  * brief is on `get-task-details` / `GET /api/tasks/{id}`.
253
345
  */
254
- export function rowToAgentTaskSummary(row: AgentTaskRow): AgentTaskSummary {
255
- const t = rowToAgentTask(row);
346
+ export function rowToAgentTaskSummary(row: AgentTaskSummaryRow): AgentTaskSummary {
347
+ return {
348
+ id: row.id,
349
+ key: row.key,
350
+ agentId: row.agentId,
351
+ creatorAgentId: row.creatorAgentId ?? undefined,
352
+ task: taskPreview(row.task),
353
+ title: row.title ?? undefined,
354
+ status: row.status,
355
+ source: row.source,
356
+ taskType: row.taskType ?? undefined,
357
+ tags: row.tags ? JSON.parse(row.tags) : [],
358
+ priority: row.priority ?? 50,
359
+ dependsOn: row.dependsOn ? JSON.parse(row.dependsOn) : [],
360
+ offeredTo: row.offeredTo ?? undefined,
361
+ acceptedAt: row.acceptedAt ?? undefined,
362
+ parentTaskId: row.parentTaskId ?? undefined,
363
+ scheduleId: row.scheduleId ?? undefined,
364
+ model: row.model ?? undefined,
365
+ modelTier: parseModelTier(row.modelTier) ?? undefined,
366
+ effort: ReasoningEffortSchema.safeParse(row.effort).success
367
+ ? (row.effort as ReasoningEffort)
368
+ : undefined,
369
+ provider: (row.provider as ProviderName | null) ?? undefined,
370
+ requestedByUserId: row.requestedByUserId ?? undefined,
371
+ progress: row.progress ?? undefined,
372
+ createdAt: row.createdAt,
373
+ lastUpdatedAt: row.lastUpdatedAt,
374
+ finishedAt: row.finishedAt ?? undefined,
375
+ peakContextPercent: row.peakContextPercent ?? undefined,
376
+ totalCostUsd: row.totalCostUsd ?? undefined,
377
+ };
378
+ }
379
+
380
+ /** Timeline list-row mapper: only what the dashboard timeline draws. */
381
+ function rowToAgentTaskTimelineItem(row: AgentTaskTimelineRow): AgentTaskTimelineItem {
256
382
  return {
257
- id: t.id,
258
- key: t.key,
259
- agentId: t.agentId,
260
- creatorAgentId: t.creatorAgentId,
261
- task: dependencies.previewText(t.task, TASK_PREVIEW_LENGTH),
262
- title: t.title,
263
- status: t.status,
264
- source: t.source,
265
- taskType: t.taskType,
266
- tags: t.tags,
267
- priority: t.priority,
268
- dependsOn: t.dependsOn,
269
- offeredTo: t.offeredTo,
270
- acceptedAt: t.acceptedAt,
271
- parentTaskId: t.parentTaskId,
272
- scheduleId: t.scheduleId,
273
- model: t.model,
274
- modelTier: t.modelTier,
275
- effort: t.effort,
276
- provider: t.provider,
277
- requestedByUserId: t.requestedByUserId,
278
- progress: t.progress,
279
- createdAt: t.createdAt,
280
- lastUpdatedAt: t.lastUpdatedAt,
281
- finishedAt: t.finishedAt,
282
- peakContextPercent: t.peakContextPercent,
283
- totalCostUsd: t.totalCostUsd,
383
+ id: row.id,
384
+ agentId: row.agentId,
385
+ parentTaskId: row.parentTaskId ?? undefined,
386
+ task: taskPreview(row.task),
387
+ title: row.title ?? undefined,
388
+ status: row.status,
389
+ createdAt: row.createdAt,
390
+ lastUpdatedAt: row.lastUpdatedAt,
391
+ finishedAt: row.finishedAt ?? undefined,
392
+ peakContextTokens: row.peakContextTokens ?? undefined,
393
+ totalCostUsd: row.totalCostUsd ?? undefined,
284
394
  };
285
395
  }
286
396
 
@@ -321,6 +431,33 @@ export async function hasNonTerminalResumeChild(parentId: string): Promise<boole
321
431
  return row !== undefined && row !== null;
322
432
  }
323
433
 
434
+ /**
435
+ * Superseded tasks that never got a resume child. Both supersede paths
436
+ * (heartbeat remediation and `POST /api/tasks/:id/supersede`) supersede
437
+ * first and create the resume in a later, separate write, so a process
438
+ * crash between the two leaves this state. Workflow steps are failed, not
439
+ * superseded, and are excluded. `finishedBefore` keeps the sweep from racing
440
+ * an in-flight supersede; `finishedAfter` bounds how far back it repairs.
441
+ */
442
+ export async function getSupersededTasksWithoutResume(
443
+ finishedBefore: string,
444
+ finishedAfter: string,
445
+ ): Promise<AgentTask[]> {
446
+ const rows = await getDbClient().query<AgentTaskRow>(
447
+ `SELECT * FROM agent_tasks t
448
+ WHERE t.status = 'superseded'
449
+ AND t.workflowRunStepId IS NULL
450
+ AND t.finishedAt < ?
451
+ AND t.finishedAt > ?
452
+ AND NOT EXISTS (
453
+ SELECT 1 FROM agent_tasks c WHERE c.parentTaskId = t.id AND c.taskType = 'resume'
454
+ )
455
+ ORDER BY t.finishedAt ASC`,
456
+ [finishedBefore, finishedAfter],
457
+ );
458
+ return rows.map(rowToAgentTask);
459
+ }
460
+
324
461
  /**
325
462
  * True when a non-terminal `reroute-decision` child exists for `parentId`.
326
463
  *
@@ -438,10 +575,14 @@ export function getAllTasks(
438
575
  filters: TaskFilters | undefined,
439
576
  opts: { slim: true },
440
577
  ): Promise<AgentTaskSummary[]>;
578
+ export function getAllTasks(
579
+ filters: TaskFilters | undefined,
580
+ opts: { fields: "timeline" },
581
+ ): Promise<AgentTaskTimelineItem[]>;
441
582
  export async function getAllTasks(
442
583
  filters?: TaskFilters,
443
- opts?: { slim?: boolean },
444
- ): Promise<AgentTask[] | AgentTaskSummary[]> {
584
+ opts?: { slim?: boolean; fields?: "timeline" },
585
+ ): Promise<AgentTask[] | AgentTaskSummary[] | AgentTaskTimelineItem[]> {
445
586
  const conditions: string[] = [];
446
587
  const params: (string | AgentTaskStatus)[] = [];
447
588
 
@@ -544,16 +685,31 @@ export async function getAllTasks(
544
685
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
545
686
  const limit = filters?.limit ?? 25;
546
687
  const offset = filters?.offset ?? 0;
547
- const orderBy =
688
+ const orderColumns =
548
689
  filters?.orderBy === "createdAt"
549
- ? "createdAt DESC, rowid DESC"
550
- : "lastUpdatedAt DESC, priority DESC";
551
- const query = `SELECT agent_tasks.*,
690
+ ? ["createdAt DESC", "rowid DESC"]
691
+ : ["lastUpdatedAt DESC", "priority DESC"];
692
+ const fields: TaskListFields =
693
+ opts?.fields === "timeline" ? "timeline" : opts?.slim ? "slim" : "full";
694
+ // Page the rowids first, then read the projected columns and the cost
695
+ // subquery for that page only, so no candidate row's prompt/output blobs
696
+ // are materialized unless the row is on the page. The page query keeps the
697
+ // exact ORDER BY: a `rowid` tiebreak there makes SQLite drop the
698
+ // lastUpdatedAt index for a full scan. The outer sort adds it only to fix
699
+ // the order of exact ties inside the page.
700
+ const query = `WITH page AS (
701
+ SELECT rowid AS rid FROM agent_tasks ${whereClause}
702
+ ORDER BY ${orderColumns.join(", ")} LIMIT ${limit} OFFSET ${offset}
703
+ )
704
+ SELECT ${LIST_COLUMNS[fields]},
552
705
  (SELECT SUM(totalCostUsd) FROM session_costs WHERE session_costs.taskId = agent_tasks.id) AS totalCostUsd
553
- FROM agent_tasks ${whereClause}
554
- ORDER BY ${orderBy} LIMIT ${limit} OFFSET ${offset}`;
706
+ FROM page JOIN agent_tasks ON agent_tasks.rowid = page.rid
707
+ ORDER BY ${[...new Set([...orderColumns, "rowid DESC"])].map((column) => `agent_tasks.${column}`).join(", ")}`;
555
708
 
556
- const rows = await getDbClient().query<AgentTaskRow>(query, params);
709
+ if (fields === "timeline") {
710
+ const rows = await getDbClient().query<AgentTaskTimelineRow>(query, params);
711
+ return rows.map(rowToAgentTaskTimelineItem);
712
+ }
557
713
 
558
714
  // Filter for ready tasks (dependencies met) if requested. Both the full and
559
715
  // the slim row shapes carry `id` + `dependsOn`, so the same predicate works.
@@ -569,12 +725,14 @@ export async function getAllTasks(
569
725
  return items.filter((_, i) => readyFlags[i]);
570
726
  };
571
727
 
572
- if (opts?.slim) {
728
+ if (fields === "slim") {
729
+ const rows = await getDbClient().query<AgentTaskSummaryRow>(query, params);
573
730
  let tasks = rows.map(rowToAgentTaskSummary);
574
731
  if (filters?.readyOnly) tasks = await filterReady(tasks);
575
732
  return tasks;
576
733
  }
577
734
 
735
+ const rows = await getDbClient().query<AgentTaskRow>(query, params);
578
736
  let tasks = rows.map(rowToAgentTask);
579
737
  if (filters?.readyOnly) tasks = await filterReady(tasks);
580
738
  return tasks;
@@ -436,7 +436,24 @@ export async function completeTask(
436
436
  return row ? rowToAgentTask(row) : null;
437
437
  }
438
438
 
439
- export async function failTask(id: string, reason: string): Promise<AgentTask | null> {
439
+ export async function failTask(
440
+ id: string,
441
+ reason: string,
442
+ opts: {
443
+ /**
444
+ * Compare-and-swap on the `lastUpdatedAt` the caller observed. The
445
+ * heartbeat passes it so a progress write that lands after its read
446
+ * cancels the remediation instead of being overwritten.
447
+ */
448
+ expectedLastUpdatedAt?: string;
449
+ /**
450
+ * `false` skips the dependent cascade. Only for a caller that settles the
451
+ * dependents itself — the reboot sweep re-points them to the retry child,
452
+ * then cascades whatever is left.
453
+ */
454
+ cascadeDependents?: boolean;
455
+ } = {},
456
+ ): Promise<AgentTask | null> {
440
457
  const oldTask = await getTaskById(id);
441
458
  if (!oldTask) return null;
442
459
 
@@ -453,8 +470,15 @@ export async function failTask(id: string, reason: string): Promise<AgentTask |
453
470
  // terminal transition can land during the await above).
454
471
  const row = await getDbClient().get<AgentTaskRow>(
455
472
  `UPDATE agent_tasks SET status = 'failed', failureReason = ?, finishedAt = ?, lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
456
- WHERE id = ? AND status NOT IN ('completed', 'failed', 'cancelled', 'superseded') RETURNING *`,
457
- [scrubbedReason, finishedAt, id],
473
+ WHERE id = ? AND status NOT IN ('completed', 'failed', 'cancelled', 'superseded')
474
+ AND (? IS NULL OR lastUpdatedAt = ?) RETURNING *`,
475
+ [
476
+ scrubbedReason,
477
+ finishedAt,
478
+ id,
479
+ opts.expectedLastUpdatedAt ?? null,
480
+ opts.expectedLastUpdatedAt ?? null,
481
+ ],
458
482
  );
459
483
  if (row && oldTask) {
460
484
  dependencies.emitTaskLifecycleTelemetryAfterCommit(
@@ -511,10 +535,12 @@ export async function failTask(id: string, reason: string): Promise<AgentTask |
511
535
 
512
536
  // Cascade-fail any non-terminal tasks that depend on this one.
513
537
  // The cascade is recursive (transitive closure) and cycle-safe.
514
- try {
515
- await dependencies.cascadeFailDependents(id, "failed");
516
- } catch (err) {
517
- console.error("[failTask] cascade-fail dependents error:", err);
538
+ if (opts.cascadeDependents !== false) {
539
+ try {
540
+ await dependencies.cascadeFailDependents(id, "failed");
541
+ } catch (err) {
542
+ console.error("[failTask] cascade-fail dependents error:", err);
543
+ }
518
544
  }
519
545
  }
520
546
  return row ? rowToAgentTask(row) : null;
@@ -653,7 +679,12 @@ export async function cancelTask(id: string, reason?: string): Promise<AgentTask
653
679
  */
654
680
  export async function supersedeTask(
655
681
  id: string,
656
- args: { reason: string; resumeTaskId: string | null },
682
+ args: {
683
+ reason: string;
684
+ resumeTaskId: string | null;
685
+ /** Compare-and-swap on the observed `lastUpdatedAt`; see `failTask`. */
686
+ expectedLastUpdatedAt?: string;
687
+ },
657
688
  ): Promise<AgentTask | null> {
658
689
  const oldTask = await getTaskById(id);
659
690
  if (!oldTask) return null;
@@ -670,8 +701,9 @@ export async function supersedeTask(
670
701
  finishedAt = ?,
671
702
  lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
672
703
  WHERE id = ? AND status NOT IN ('completed', 'failed', 'cancelled', 'superseded')
704
+ AND (? IS NULL OR lastUpdatedAt = ?)
673
705
  RETURNING *`,
674
- [finishedAt, id],
706
+ [finishedAt, id, args.expectedLastUpdatedAt ?? null, args.expectedLastUpdatedAt ?? null],
675
707
  );
676
708
 
677
709
  if (row && oldTask) {