@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.
- package/README.md +2 -1
- package/dist/{acp-adapter-580t41c0.js → acp-adapter-9n319wqc.js} +61 -6
- package/dist/{actions-m662tjng.js → actions-2vxqvpr9.js} +9 -8
- package/dist/{app-zb3rndm8.js → app-w066xfy0.js} +6 -5
- package/dist/assistant-kt31dj2h.js +236 -0
- package/dist/{boot-reembed-072x00vn.js → boot-reembed-az5rassp.js} +7 -6
- package/dist/{boot-reembed-65zz2smt.js → boot-reembed-j3mm3rfz.js} +8 -7
- package/dist/{boot-scrub-logs-yz0ams04.js → boot-scrub-logs-b6h54817.js} +4 -4
- package/dist/{claude-adapter-paj270y2.js → claude-adapter-w1z04ann.js} +8 -6
- package/dist/{claude-managed-setup-w619jncn.js → claude-managed-setup-f5pmsyrp.js} +24 -3
- package/dist/{claude-sdk-session-swt80j9z.js → claude-sdk-session-f0cc1kec.js} +8 -6
- package/dist/{cli-dt553ktb.js → cli-0b6y7kxm.js} +1 -1
- package/dist/{cli-bxdhw615.js → cli-12fz972k.js} +4 -4
- package/dist/{cli-s2f20r5w.js → cli-1th1728g.js} +2 -2
- package/dist/{cli-rs6ec3b9.js → cli-3tgwnf43.js} +3 -3
- package/dist/{cli-gxnzc6wh.js → cli-4nr988h6.js} +1 -1
- package/dist/{cli-axs8e6d4.js → cli-53s590z8.js} +1 -1
- package/dist/{cli-ygbjp3z8.js → cli-54wwve05.js} +5 -4
- package/dist/{cli-v0asr5yv.js → cli-719k9j2c.js} +2 -2
- package/dist/{cli-anktweqr.js → cli-89xfd7f9.js} +21 -47
- package/dist/{cli-q34a0e10.js → cli-8v0g1yc8.js} +1 -1
- package/dist/cli-9d8ntetj.js +42 -0
- package/dist/{cli-z9fprcrf.js → cli-atcve9y8.js} +881 -738
- package/dist/{cli-jmb7g47h.js → cli-b5bq9crk.js} +14 -31
- package/dist/cli-d8brzxjb.js +190 -0
- package/dist/{cli-gzky0gkr.js → cli-de14znh8.js} +2 -2
- package/dist/{cli-xytwff7f.js → cli-dttph1bw.js} +2 -2
- package/dist/cli-ep19k3mb.js +19 -0
- package/dist/{cli-m1xa8be7.js → cli-f146mn5s.js} +7 -50
- package/dist/{cli-1fj0chdk.js → cli-f7dsy61y.js} +5 -3
- package/dist/{cli-f567d5c1.js → cli-ftht3zzj.js} +2782 -2589
- package/dist/{cli-a0g1gbt5.js → cli-hbv0kq6w.js} +2 -2
- package/dist/{cli-fhsz3twr.js → cli-jsp14b6c.js} +171 -1
- package/dist/{cli-7fnkj1hj.js → cli-kpv03zhj.js} +21 -17
- package/dist/{cli-m4j6trn4.js → cli-m8yrsg97.js} +2 -2
- package/dist/cli-m9sxbkhm.js +779 -0
- package/dist/cli-mmemxxdc.js +823 -0
- package/dist/{cli-tqtvj5pt.js → cli-n8508nre.js} +1 -1
- package/dist/{cli-z96t0thj.js → cli-p1d5b073.js} +37 -2
- package/dist/{cli-gh8y8f36.js → cli-rw51bq3j.js} +13 -10
- package/dist/{cli-59ej1c07.js → cli-smby5m17.js} +7 -6
- package/dist/{cli-65vqzz3v.js → cli-tzvk9haz.js} +94 -372
- package/dist/{cli-dc9k2ba9.js → cli-w9kb3bk1.js} +1 -1
- package/dist/{cli-kpev657q.js → cli-y6b6fb76.js} +25 -3
- package/dist/{cli-123q2get.js → cli-yz3djrm0.js} +9 -9
- package/dist/{cli-wvp6bvy4.js → cli-zhjmqxzh.js} +1 -1
- package/dist/cli.js +13 -13
- package/dist/{codex-adapter-k98vaw2r.js → codex-adapter-zw4dwsx7.js} +2 -2
- package/dist/{codex-hook-9pf19cks.js → codex-hook-bw8p5nh4.js} +2 -2
- package/dist/{codex-session-runner-tngtphx7.js → codex-session-runner-k981s65v.js} +2 -2
- package/dist/{commands-y8nbr3fv.js → commands-sykrxx7e.js} +4 -4
- package/dist/{db-30p4h2t1.js → db-sfbsc8wt.js} +6 -4
- package/dist/handlers-0aspcejm.js +603 -0
- package/dist/{hook-dcw4jfbj.js → hook-4nvqj2sb.js} +7 -5
- package/dist/{hook-21jexvfx.js → hook-hxccs7m5.js} +24 -49
- package/dist/{http-r4kq5g8d.js → http-y06z517d.js} +230 -94
- package/dist/{index-q3qja6ta.js → index-hssww7kj.js} +15 -14
- package/dist/{index-gay66b9w.js → index-k8znfx0z.js} +13 -12
- package/dist/{index-53zyht4z.js → index-tce1rss4.js} +371 -110
- package/dist/{index-nqvf26za.js → index-x5j894zf.js} +14 -13
- package/dist/{keepalive-xs8fa55y.js → keepalive-w733ax66.js} +6 -6
- package/dist/{lead-d9m2t4s0.js → lead-75bjqa9d.js} +29 -27
- package/dist/{maintenance-3zh82g6z.js → maintenance-w4f1zjck.js} +8 -7
- package/dist/{oauth-refresh-sweep-03snsye5.js → oauth-refresh-sweep-wdetky26.js} +5 -5
- package/dist/{onboard-0cg1925x.js → onboard-bb5net8s.js} +2 -2
- package/dist/{otel-impl-0ab95z27.js → otel-impl-0w7c14ap.js} +1 -1
- package/dist/{pi-mono-adapter-dtkz4e7j.js → pi-mono-adapter-2has4gnp.js} +12 -2
- package/dist/{pricing-refresh-e5jhzz4j.js → pricing-refresh-9nzsrgcq.js} +6 -6
- package/dist/{rbac-roles-p1mf40z3.js → rbac-roles-r3hqjp09.js} +5 -5
- package/dist/{rbac-roles-2w3cmrkc.js → rbac-roles-z1jrffbt.js} +4 -4
- package/dist/{registry-9kdck5ye.js → registry-v5wytg0s.js} +1 -2
- package/dist/{render-v2-xhawry3k.js → render-v2-vbr89fnb.js} +6 -5
- package/dist/{seed-pricing-t223sw91.js → seed-pricing-g62hy1pk.js} +7 -5
- package/dist/{setup-zcs0rce7.js → setup-gnfqnptp.js} +2 -2
- package/dist/{worker-qnp2ka62.js → worker-kznxp4qm.js} +29 -27
- package/openapi.json +263 -12
- package/package.json +1 -1
- package/plugin/commands/start-worker.md +4 -9
- package/plugin/pi-skills/start-worker/SKILL.md +4 -9
- package/src/be/db/agents.ts +556 -0
- package/src/be/db/context-versions.ts +117 -0
- package/src/be/db/runtime.ts +406 -0
- package/src/be/db/tasks/read.ts +1167 -0
- package/src/be/db/tasks/write.ts +953 -0
- package/src/be/db.ts +584 -3429
- package/src/be/migrations/147_scheduled_tasks_parent_task.sql +7 -0
- package/src/be/migrations/148_pages_svg.sql +123 -0
- package/src/be/migrations/149_session_tokens.sql +24 -0
- package/src/be/scripts/typecheck.ts +1 -0
- package/src/be/seed-pricing.ts +13 -8
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
- package/src/be/seed-scripts/catalog/star-history-refresh.ts +101 -0
- package/src/be/seed-scripts/catalog/star-history-renderer.ts +119 -0
- package/src/be/seed-scripts/index.ts +17 -0
- package/src/be/seed-skills/index.ts +14 -0
- package/src/be/swarm-config-guard.ts +5 -2
- package/src/be/task-attachment-store.ts +70 -0
- package/src/be/users.ts +77 -0
- package/src/commands/attachments-section.ts +37 -0
- package/src/commands/claude-managed-setup.ts +45 -3
- package/src/commands/claude-md-session.ts +408 -0
- package/src/commands/context-preamble.ts +15 -3
- package/src/commands/profile-sync.ts +205 -5
- package/src/commands/provider-credentials.ts +60 -27
- package/src/commands/runner.ts +63 -58
- package/src/commands/templates.ts +25 -2
- package/src/hooks/hook.ts +26 -67
- package/src/http/agents.ts +40 -1
- package/src/http/auth.ts +13 -1
- package/src/http/fs.ts +21 -51
- package/src/http/index.ts +2 -0
- package/src/http/mcp.ts +32 -0
- package/src/http/pages-public.ts +20 -4
- package/src/http/pages.ts +11 -3
- package/src/http/poll.ts +4 -0
- package/src/http/repos.ts +3 -3
- package/src/http/sessions.ts +82 -0
- package/src/http/tasks.ts +10 -2
- package/src/http/utils.ts +79 -1
- package/src/prompts/session-templates.ts +12 -8
- package/src/providers/acp-adapter.ts +62 -8
- package/src/providers/pi-mono-adapter.ts +23 -6
- package/src/providers/pricing-sources.md +11 -0
- package/src/scheduler/scheduler.test.ts +49 -2
- package/src/scheduler/scheduler.ts +4 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +8 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +8 -0
- package/src/server.ts +2 -0
- package/src/slack/assistant.ts +87 -46
- package/src/slack/handlers.test.ts +1 -3
- package/src/slack/handlers.ts +102 -87
- package/src/slack/inbound-files.ts +466 -0
- package/src/slack/templates.ts +14 -0
- package/src/tasks/task-terminal-effects.ts +158 -0
- package/src/tests/acp-adapter.test.ts +369 -117
- package/src/tests/budget-claim-gate.test.ts +1 -0
- package/src/tests/build-pi-skills.test.ts +5 -7
- package/src/tests/claude-managed-setup.test.ts +52 -4
- package/src/tests/claude-md-session.test.ts +603 -0
- package/src/tests/codex-skill-resolver.test.ts +19 -0
- package/src/tests/context-versioning.test.ts +107 -0
- package/src/tests/cors-allowlist.test.ts +309 -0
- package/src/tests/credential-check.test.ts +157 -14
- package/src/tests/db-agents.test.ts +91 -0
- package/src/tests/db-context-versions.test.ts +37 -0
- package/src/tests/db-extraction-contract.test.ts +45 -0
- package/src/tests/db-runtime.test.ts +70 -0
- package/src/tests/db-task-read-contract.test.ts +624 -0
- package/src/tests/db-task-write-contract.test.ts +653 -0
- package/src/tests/db-tasks-read.test.ts +183 -0
- package/src/tests/db-tasks-write.test.ts +230 -0
- package/src/tests/defer-task.test.ts +389 -0
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/e2e-visuals-stage.test.ts +151 -0
- package/src/tests/file-lock.test.ts +144 -0
- package/src/tests/fixtures/file-lock-holder.ts +13 -0
- package/src/tests/fixtures/hold-file-lock.ts +25 -0
- package/src/tests/heartbeat-expiry.test.ts +188 -0
- package/src/tests/http-api-integration.test.ts +78 -1
- package/src/tests/pages-public-html.test.ts +43 -0
- package/src/tests/pages-svg-migration.test.ts +42 -0
- package/src/tests/preload.ts +4 -1
- package/src/tests/profile-sync-warning.test.ts +16 -0
- package/src/tests/profile-sync.test.ts +126 -0
- package/src/tests/prompt-template-session.test.ts +20 -4
- package/src/tests/provider-registration.test.ts +93 -0
- package/src/tests/rbac-e2e-helpers.test.ts +53 -0
- package/src/tests/rbac-e2e-helpers.ts +19 -2
- package/src/tests/reload-config.test.ts +26 -0
- package/src/tests/routing-affinity-inventory.test.ts +1 -1
- package/src/tests/runner-attachments-section.test.ts +22 -2
- package/src/tests/runner-context-preamble.test.ts +30 -0
- package/src/tests/runner-deployment-resume.test.ts +107 -0
- package/src/tests/runner-repo-autostash.test.ts +12 -0
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/seed-scripts.test.ts +2 -2
- package/src/tests/session-tokens.test.ts +279 -0
- package/src/tests/slack-file-tools-attachments.test.ts +408 -0
- package/src/tests/slack-inbound-files.test.ts +847 -0
- package/src/tests/star-history-refresh.test.ts +102 -0
- package/src/tests/swarm-config-reserved-keys.test.ts +21 -0
- package/src/tests/swarm-repos.test.ts +20 -0
- package/src/tests/tool-annotations.test.ts +4 -3
- package/src/tests/ui-logs-parser.test.ts +176 -0
- package/src/tools/create-page.ts +4 -2
- package/src/tools/defer-task.ts +239 -0
- package/src/tools/repos/update-repo.ts +2 -2
- package/src/tools/schedules/create-schedule.ts +1 -0
- package/src/tools/schedules/list-schedules.ts +1 -0
- package/src/tools/schedules/patch-schedule.ts +1 -0
- package/src/tools/schedules/update-schedule.ts +1 -0
- package/src/tools/slack-download-file.ts +60 -29
- package/src/tools/slack-read.ts +73 -32
- package/src/tools/store-progress.ts +16 -146
- package/src/tools/tool-config.ts +1 -0
- package/src/tools/utils.ts +21 -0
- package/src/types.ts +37 -4
- package/src/utils/acp-session-token.ts +56 -0
- package/src/utils/file-lock.ts +111 -0
- package/src/utils/heartbeat-expiry.ts +78 -0
- package/src/utils/request-auth-context.ts +2 -0
- package/src/utils/task-attachment-links.ts +22 -0
- package/templates/skills/code-quality/content.md +8 -5
- package/templates/skills/download-task-attachment/content.md +2 -1
- package/{plugin/pi-skills → templates/skills}/review-offered-task/SKILL.md +5 -5
- package/templates/skills/review-offered-task/config.json +14 -0
- package/{plugin/commands/review-offered-task.md → templates/skills/review-offered-task/content.md} +3 -8
- package/templates/skills/scheduling/content.md +4 -0
- package/templates/skills/slack-interaction/content.md +2 -2
- package/templates/skills/work-on-task/SKILL.md +16 -0
- package/templates/skills/work-on-task/config.json +14 -0
- package/templates/skills/work-on-task/content.md +11 -0
- package/dist/assistant-0qt42nzg.js +0 -212
- package/dist/cli-fs1kkqgr.js +0 -317
- package/dist/cli-jnf5zghe.js +0 -269
- package/dist/cli-n1q8q14d.js +0 -177
- package/dist/handlers-yfp1qn81.js +0 -592
- package/plugin/commands/work-on-task.md +0 -39
- package/plugin/pi-skills/work-on-task/SKILL.md +0 -39
|
@@ -0,0 +1,1167 @@
|
|
|
1
|
+
import { normalizeAssetKey } from "../../../assets/key";
|
|
2
|
+
import type {
|
|
3
|
+
Agent,
|
|
4
|
+
AgentTask,
|
|
5
|
+
AgentTaskSource,
|
|
6
|
+
AgentTaskStatus,
|
|
7
|
+
AgentTaskSummary,
|
|
8
|
+
FollowUpConfig,
|
|
9
|
+
ProviderName,
|
|
10
|
+
ReasoningEffort,
|
|
11
|
+
RoutingAffinity,
|
|
12
|
+
} from "../../../types";
|
|
13
|
+
import {
|
|
14
|
+
FollowUpConfigSchema,
|
|
15
|
+
parseModelTier,
|
|
16
|
+
ReasoningEffortSchema,
|
|
17
|
+
RoutingAffinitySchema,
|
|
18
|
+
} from "../../../types";
|
|
19
|
+
import { parseProviderMeta } from "../../../utils/provider-metadata";
|
|
20
|
+
import { getAgentById } from "../agents";
|
|
21
|
+
import { getDbClient } from "../runtime";
|
|
22
|
+
|
|
23
|
+
type TaskReadDependencies = {
|
|
24
|
+
checkDependencies: (taskId: string) => Promise<{ ready: boolean; blockedBy: string[] }>;
|
|
25
|
+
assetKeyPrefixPattern: (input: string) => string;
|
|
26
|
+
previewText: (text: string | null | undefined, maxChars: number) => string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let dependencies: TaskReadDependencies;
|
|
30
|
+
|
|
31
|
+
// The facade registers deferred calls to helpers it still owns.
|
|
32
|
+
export function configureTaskReadDependencies(value: TaskReadDependencies): void {
|
|
33
|
+
dependencies = value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Preview length for a task's `task` text (pool-triage needs to read it). */
|
|
37
|
+
const TASK_PREVIEW_LENGTH = 300;
|
|
38
|
+
|
|
39
|
+
export type AgentTaskRow = {
|
|
40
|
+
id: string;
|
|
41
|
+
key: string;
|
|
42
|
+
agentId: string | null;
|
|
43
|
+
creatorAgentId: string | null;
|
|
44
|
+
task: string;
|
|
45
|
+
title: string | null;
|
|
46
|
+
status: AgentTaskStatus;
|
|
47
|
+
source: AgentTaskSource;
|
|
48
|
+
taskType: string | null;
|
|
49
|
+
tags: string | null;
|
|
50
|
+
priority: number;
|
|
51
|
+
dependsOn: string | null;
|
|
52
|
+
offeredTo: string | null;
|
|
53
|
+
offeredAt: string | null;
|
|
54
|
+
acceptedAt: string | null;
|
|
55
|
+
rejectionReason: string | null;
|
|
56
|
+
slackChannelId: string | null;
|
|
57
|
+
slackThreadTs: string | null;
|
|
58
|
+
slackTriggerMessageTs: string | null;
|
|
59
|
+
slackUserId: string | null;
|
|
60
|
+
slackReplySent: number;
|
|
61
|
+
slackProgressMessageTs: string | null;
|
|
62
|
+
slackTreeRootMessageTs: string | null;
|
|
63
|
+
vcsProvider: string | null;
|
|
64
|
+
vcsRepo: string | null;
|
|
65
|
+
vcsEventType: string | null;
|
|
66
|
+
vcsNumber: number | null;
|
|
67
|
+
vcsCommentId: number | null;
|
|
68
|
+
vcsAuthor: string | null;
|
|
69
|
+
vcsUrl: string | null;
|
|
70
|
+
vcsInstallationId: number | null;
|
|
71
|
+
vcsNodeId: string | null;
|
|
72
|
+
agentmailInboxId: string | null;
|
|
73
|
+
agentmailMessageId: string | null;
|
|
74
|
+
agentmailThreadId: string | null;
|
|
75
|
+
mentionMessageId: string | null;
|
|
76
|
+
mentionChannelId: string | null;
|
|
77
|
+
dir: string | null;
|
|
78
|
+
parentTaskId: string | null;
|
|
79
|
+
claudeSessionId: string | null;
|
|
80
|
+
model: string | null;
|
|
81
|
+
modelTier: string | null;
|
|
82
|
+
effort: string | null;
|
|
83
|
+
scheduleId: string | null;
|
|
84
|
+
workflowRunId: string | null;
|
|
85
|
+
workflowRunStepId: string | null;
|
|
86
|
+
outputSchema: string | null;
|
|
87
|
+
followUpConfig: string | null;
|
|
88
|
+
contextKey: string | null;
|
|
89
|
+
createdAt: string;
|
|
90
|
+
lastUpdatedAt: string;
|
|
91
|
+
finishedAt: string | null;
|
|
92
|
+
notifiedAt: string | null;
|
|
93
|
+
failureReason: string | null;
|
|
94
|
+
output: string | null;
|
|
95
|
+
progress: string | null;
|
|
96
|
+
compactionCount: number | null;
|
|
97
|
+
peakContextPercent: number | null;
|
|
98
|
+
peakContextTokens: number | null;
|
|
99
|
+
contextWindowSize: number | null;
|
|
100
|
+
was_paused: number;
|
|
101
|
+
credentialKeySuffix: string | null;
|
|
102
|
+
credentialKeyType: string | null;
|
|
103
|
+
requestedByUserId: string | null;
|
|
104
|
+
requestedByUserIdInherited: number;
|
|
105
|
+
swarmVersion: string | null;
|
|
106
|
+
provider: string | null;
|
|
107
|
+
providerMeta: string | null;
|
|
108
|
+
harnessVariant: string | null;
|
|
109
|
+
harnessVariantMeta: string | null;
|
|
110
|
+
totalCostUsd?: number | null;
|
|
111
|
+
routingAffinity: string | null;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
115
|
+
let followUpConfig: FollowUpConfig | undefined;
|
|
116
|
+
if (row.followUpConfig) {
|
|
117
|
+
try {
|
|
118
|
+
const parsed = FollowUpConfigSchema.safeParse(JSON.parse(row.followUpConfig));
|
|
119
|
+
if (parsed.success) {
|
|
120
|
+
followUpConfig = parsed.data;
|
|
121
|
+
} else {
|
|
122
|
+
console.warn(
|
|
123
|
+
`[db] Ignoring invalid agent_tasks.followUpConfig for task ${row.id}:`,
|
|
124
|
+
parsed.error.message,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.warn(
|
|
129
|
+
`[db] Ignoring malformed agent_tasks.followUpConfig for task ${row.id}:`,
|
|
130
|
+
error instanceof Error ? error.message : String(error),
|
|
131
|
+
);
|
|
132
|
+
followUpConfig = undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let routingAffinity: RoutingAffinity | undefined;
|
|
137
|
+
let routingAffinityInvalid = false;
|
|
138
|
+
if (row.routingAffinity) {
|
|
139
|
+
try {
|
|
140
|
+
const parsed = RoutingAffinitySchema.safeParse(JSON.parse(row.routingAffinity));
|
|
141
|
+
if (parsed.success) {
|
|
142
|
+
routingAffinity = parsed.data;
|
|
143
|
+
} else {
|
|
144
|
+
routingAffinityInvalid = true;
|
|
145
|
+
console.warn(
|
|
146
|
+
`[db] Quarantining invalid agent_tasks.routingAffinity for task ${row.id}:`,
|
|
147
|
+
parsed.error.message,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
routingAffinityInvalid = true;
|
|
152
|
+
console.warn(
|
|
153
|
+
`[db] Quarantining malformed agent_tasks.routingAffinity for task ${row.id}:`,
|
|
154
|
+
error instanceof Error ? error.message : String(error),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
id: row.id,
|
|
161
|
+
key: row.key,
|
|
162
|
+
agentId: row.agentId,
|
|
163
|
+
creatorAgentId: row.creatorAgentId ?? undefined,
|
|
164
|
+
task: row.task,
|
|
165
|
+
title: row.title ?? undefined,
|
|
166
|
+
status: row.status,
|
|
167
|
+
source: row.source,
|
|
168
|
+
taskType: row.taskType ?? undefined,
|
|
169
|
+
tags: row.tags ? JSON.parse(row.tags) : [],
|
|
170
|
+
priority: row.priority ?? 50,
|
|
171
|
+
dependsOn: row.dependsOn ? JSON.parse(row.dependsOn) : [],
|
|
172
|
+
offeredTo: row.offeredTo ?? undefined,
|
|
173
|
+
offeredAt: row.offeredAt ?? undefined,
|
|
174
|
+
acceptedAt: row.acceptedAt ?? undefined,
|
|
175
|
+
rejectionReason: row.rejectionReason ?? undefined,
|
|
176
|
+
slackChannelId: row.slackChannelId ?? undefined,
|
|
177
|
+
slackThreadTs: row.slackThreadTs ?? undefined,
|
|
178
|
+
slackTriggerMessageTs: row.slackTriggerMessageTs ?? undefined,
|
|
179
|
+
slackUserId: row.slackUserId ?? undefined,
|
|
180
|
+
slackReplySent: !!row.slackReplySent,
|
|
181
|
+
slackProgressMessageTs: row.slackProgressMessageTs ?? undefined,
|
|
182
|
+
slackTreeRootMessageTs: row.slackTreeRootMessageTs ?? undefined,
|
|
183
|
+
vcsProvider: (row.vcsProvider as "github" | "gitlab" | null) ?? undefined,
|
|
184
|
+
vcsRepo: row.vcsRepo ?? undefined,
|
|
185
|
+
vcsEventType: row.vcsEventType ?? undefined,
|
|
186
|
+
vcsNumber: row.vcsNumber ?? undefined,
|
|
187
|
+
vcsCommentId: row.vcsCommentId ?? undefined,
|
|
188
|
+
vcsAuthor: row.vcsAuthor ?? undefined,
|
|
189
|
+
vcsUrl: row.vcsUrl ?? undefined,
|
|
190
|
+
vcsInstallationId: row.vcsInstallationId ?? undefined,
|
|
191
|
+
vcsNodeId: row.vcsNodeId ?? undefined,
|
|
192
|
+
agentmailInboxId: row.agentmailInboxId ?? undefined,
|
|
193
|
+
agentmailMessageId: row.agentmailMessageId ?? undefined,
|
|
194
|
+
agentmailThreadId: row.agentmailThreadId ?? undefined,
|
|
195
|
+
mentionMessageId: row.mentionMessageId ?? undefined,
|
|
196
|
+
mentionChannelId: row.mentionChannelId ?? undefined,
|
|
197
|
+
dir: row.dir ?? undefined,
|
|
198
|
+
parentTaskId: row.parentTaskId ?? undefined,
|
|
199
|
+
claudeSessionId: row.claudeSessionId ?? undefined,
|
|
200
|
+
model: row.model ?? undefined,
|
|
201
|
+
modelTier: parseModelTier(row.modelTier) ?? undefined,
|
|
202
|
+
effort: ReasoningEffortSchema.safeParse(row.effort).success
|
|
203
|
+
? (row.effort as ReasoningEffort)
|
|
204
|
+
: undefined,
|
|
205
|
+
scheduleId: row.scheduleId ?? undefined,
|
|
206
|
+
workflowRunId: row.workflowRunId ?? undefined,
|
|
207
|
+
workflowRunStepId: row.workflowRunStepId ?? undefined,
|
|
208
|
+
outputSchema: row.outputSchema ? JSON.parse(row.outputSchema) : undefined,
|
|
209
|
+
followUpConfig,
|
|
210
|
+
contextKey: row.contextKey ?? undefined,
|
|
211
|
+
compactionCount: row.compactionCount ?? undefined,
|
|
212
|
+
peakContextPercent: row.peakContextPercent ?? undefined,
|
|
213
|
+
peakContextTokens: row.peakContextTokens ?? undefined,
|
|
214
|
+
contextWindowSize: row.contextWindowSize ?? undefined,
|
|
215
|
+
createdAt: row.createdAt,
|
|
216
|
+
lastUpdatedAt: row.lastUpdatedAt,
|
|
217
|
+
finishedAt: row.finishedAt ?? undefined,
|
|
218
|
+
notifiedAt: row.notifiedAt ?? undefined,
|
|
219
|
+
failureReason: row.failureReason ?? undefined,
|
|
220
|
+
output: row.output ?? undefined,
|
|
221
|
+
progress: row.progress ?? undefined,
|
|
222
|
+
wasPaused: !!row.was_paused,
|
|
223
|
+
credentialKeySuffix: row.credentialKeySuffix ?? undefined,
|
|
224
|
+
credentialKeyType: row.credentialKeyType ?? undefined,
|
|
225
|
+
requestedByUserId: row.requestedByUserId ?? undefined,
|
|
226
|
+
swarmVersion: row.swarmVersion ?? undefined,
|
|
227
|
+
provider: (row.provider as ProviderName | null) ?? undefined,
|
|
228
|
+
providerMeta: parseProviderMeta(row.provider as ProviderName | null, row.providerMeta),
|
|
229
|
+
harnessVariant: row.harnessVariant ?? undefined,
|
|
230
|
+
harnessVariantMeta: row.harnessVariantMeta ? JSON.parse(row.harnessVariantMeta) : undefined,
|
|
231
|
+
totalCostUsd: row.totalCostUsd ?? undefined,
|
|
232
|
+
routingAffinity,
|
|
233
|
+
routingAffinityInvalid: routingAffinityInvalid || undefined,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Slim list-row mapper — truncates the `task` text to a bounded preview and
|
|
239
|
+
* drops completion/integration/context blobs (`output`, `failureReason`,
|
|
240
|
+
* `providerMeta`, all `vcs*`/`slack*`/`agentmail*`/`credential*`/`mention*` and
|
|
241
|
+
* context-window fields). The preview is long enough for pool-triage; the full
|
|
242
|
+
* brief is on `get-task-details` / `GET /api/tasks/{id}`.
|
|
243
|
+
*/
|
|
244
|
+
export function rowToAgentTaskSummary(row: AgentTaskRow): AgentTaskSummary {
|
|
245
|
+
const t = rowToAgentTask(row);
|
|
246
|
+
return {
|
|
247
|
+
id: t.id,
|
|
248
|
+
key: t.key,
|
|
249
|
+
agentId: t.agentId,
|
|
250
|
+
creatorAgentId: t.creatorAgentId,
|
|
251
|
+
task: dependencies.previewText(t.task, TASK_PREVIEW_LENGTH),
|
|
252
|
+
title: t.title,
|
|
253
|
+
status: t.status,
|
|
254
|
+
source: t.source,
|
|
255
|
+
taskType: t.taskType,
|
|
256
|
+
tags: t.tags,
|
|
257
|
+
priority: t.priority,
|
|
258
|
+
dependsOn: t.dependsOn,
|
|
259
|
+
offeredTo: t.offeredTo,
|
|
260
|
+
acceptedAt: t.acceptedAt,
|
|
261
|
+
parentTaskId: t.parentTaskId,
|
|
262
|
+
scheduleId: t.scheduleId,
|
|
263
|
+
model: t.model,
|
|
264
|
+
modelTier: t.modelTier,
|
|
265
|
+
effort: t.effort,
|
|
266
|
+
provider: t.provider,
|
|
267
|
+
requestedByUserId: t.requestedByUserId,
|
|
268
|
+
progress: t.progress,
|
|
269
|
+
createdAt: t.createdAt,
|
|
270
|
+
lastUpdatedAt: t.lastUpdatedAt,
|
|
271
|
+
finishedAt: t.finishedAt,
|
|
272
|
+
peakContextPercent: t.peakContextPercent,
|
|
273
|
+
totalCostUsd: t.totalCostUsd,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function getTaskById(id: string): Promise<AgentTask | null> {
|
|
278
|
+
const row = await getDbClient().get<AgentTaskRow>("SELECT * FROM agent_tasks WHERE id = ?", [id]);
|
|
279
|
+
return row ? rowToAgentTask(row) : null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function getChildTasks(parentTaskId: string): Promise<AgentTask[]> {
|
|
283
|
+
const rows = await getDbClient().query<AgentTaskRow>(
|
|
284
|
+
`SELECT * FROM agent_tasks WHERE parentTaskId = ? ORDER BY createdAt ASC, rowid ASC`,
|
|
285
|
+
[parentTaskId],
|
|
286
|
+
);
|
|
287
|
+
return rows.map(rowToAgentTask);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Returns true if `parentId` has at least one non-terminal child task with
|
|
292
|
+
* `taskType = 'resume'`. Used by the heartbeat sweep as an idempotency guard:
|
|
293
|
+
* if a prior sweep tick already created a resume follow-up for this parent,
|
|
294
|
+
* don't create a duplicate.
|
|
295
|
+
*
|
|
296
|
+
* **Filters by taskType = 'resume'** specifically. A parent task can also
|
|
297
|
+
* have ordinary non-terminal delegation children (`send-task` auto-defaults
|
|
298
|
+
* `parentTaskId` to the caller's current task — see src/tools/send-task.ts).
|
|
299
|
+
* Treating those as "already resumed" would incorrectly skip the resume
|
|
300
|
+
* path for a crashed worker that had delegated subtasks (PR #594 review).
|
|
301
|
+
*/
|
|
302
|
+
export async function hasNonTerminalResumeChild(parentId: string): Promise<boolean> {
|
|
303
|
+
const row = await getDbClient().get(
|
|
304
|
+
`SELECT 1 FROM agent_tasks
|
|
305
|
+
WHERE parentTaskId = ?
|
|
306
|
+
AND taskType = 'resume'
|
|
307
|
+
AND status NOT IN ('completed', 'failed', 'cancelled', 'superseded')
|
|
308
|
+
LIMIT 1`,
|
|
309
|
+
[parentId],
|
|
310
|
+
);
|
|
311
|
+
return row !== undefined && row !== null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* True when a non-terminal `reroute-decision` child exists for `parentId`.
|
|
316
|
+
*
|
|
317
|
+
* Mirrors {@link hasNonTerminalResumeChild} but filters on
|
|
318
|
+
* `taskType = 'reroute-decision'` — the Lead-owned re-delegation decision
|
|
319
|
+
* created when a pinned crash-recovery resume is never reclaimed (DES-523).
|
|
320
|
+
* Makes escalation idempotent: a later heartbeat sweep must not create a second
|
|
321
|
+
* decision for the same original task. We filter on the taskType marker
|
|
322
|
+
* specifically (not any child) so ordinary delegation / completion follow-up
|
|
323
|
+
* children of the original cannot suppress a needed decision, and nothing else
|
|
324
|
+
* is mistaken for one.
|
|
325
|
+
*/
|
|
326
|
+
export async function hasNonTerminalRerouteDecisionChild(parentId: string): Promise<boolean> {
|
|
327
|
+
const row = await getDbClient().get<Record<string, number>>(
|
|
328
|
+
`SELECT 1 FROM agent_tasks
|
|
329
|
+
WHERE parentTaskId = ?
|
|
330
|
+
AND taskType = 'reroute-decision'
|
|
331
|
+
AND status NOT IN ('completed', 'failed', 'cancelled', 'superseded')
|
|
332
|
+
LIMIT 1`,
|
|
333
|
+
[parentId],
|
|
334
|
+
);
|
|
335
|
+
return row !== undefined && row !== null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export async function getTasksByAgentId(agentId: string): Promise<AgentTask[]> {
|
|
339
|
+
const rows = await getDbClient().query<AgentTaskRow>(
|
|
340
|
+
"SELECT * FROM agent_tasks WHERE agentId = ? ORDER BY createdAt DESC",
|
|
341
|
+
[agentId],
|
|
342
|
+
);
|
|
343
|
+
return rows.map(rowToAgentTask);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Get the most recently updated in-progress task for an agent.
|
|
348
|
+
* Used as a fallback when X-Source-Task-Id header is missing (e.g. lead agent HITL requests).
|
|
349
|
+
*
|
|
350
|
+
* Note: if agent has multiple in-progress tasks, returns the most recently
|
|
351
|
+
* updated one. This is a best-effort fallback — the X-Source-Task-Id header
|
|
352
|
+
* is the authoritative source when available.
|
|
353
|
+
*/
|
|
354
|
+
export async function getAgentCurrentTask(agentId: string): Promise<AgentTask | null> {
|
|
355
|
+
const row = await getDbClient().get<AgentTaskRow>(
|
|
356
|
+
"SELECT * FROM agent_tasks WHERE agentId = ? AND status = 'in_progress' ORDER BY lastUpdatedAt DESC LIMIT 1",
|
|
357
|
+
[agentId],
|
|
358
|
+
);
|
|
359
|
+
return row ? rowToAgentTask(row) : null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function getTasksByStatus(status: AgentTaskStatus): Promise<AgentTask[]> {
|
|
363
|
+
const rows = await getDbClient().query<AgentTaskRow>(
|
|
364
|
+
"SELECT * FROM agent_tasks WHERE status = ? ORDER BY createdAt DESC",
|
|
365
|
+
[status],
|
|
366
|
+
);
|
|
367
|
+
return rows.map(rowToAgentTask);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Find a task by VCS repo and issue/PR/MR number.
|
|
372
|
+
* Returns the most recent non-terminal task for this VCS entity.
|
|
373
|
+
*
|
|
374
|
+
* Terminal exclusion MUST stay in lock-step with `TERMINAL_TASK_STATUSES`
|
|
375
|
+
* in `src/types.ts`. SQL strings can't import a TS const — if you add a
|
|
376
|
+
* new terminal status, grep for `NOT IN ('completed'` across this file.
|
|
377
|
+
*/
|
|
378
|
+
export async function findTaskByVcs(vcsRepo: string, vcsNumber: number): Promise<AgentTask | null> {
|
|
379
|
+
const row = await getDbClient().get<AgentTaskRow>(
|
|
380
|
+
`SELECT * FROM agent_tasks
|
|
381
|
+
WHERE vcsRepo = ? AND vcsNumber = ?
|
|
382
|
+
AND status NOT IN ('completed', 'failed', 'cancelled', 'superseded')
|
|
383
|
+
ORDER BY createdAt DESC
|
|
384
|
+
LIMIT 1`,
|
|
385
|
+
[vcsRepo, vcsNumber],
|
|
386
|
+
);
|
|
387
|
+
return row ? rowToAgentTask(row) : null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** @deprecated Use findTaskByVcs instead */
|
|
391
|
+
export const findTaskByGitHub = findTaskByVcs;
|
|
392
|
+
|
|
393
|
+
export interface TaskFilters {
|
|
394
|
+
/** Single status (back-compat) OR array of statuses (multi-status filter). */
|
|
395
|
+
status?: AgentTaskStatus | AgentTaskStatus[];
|
|
396
|
+
agentId?: string;
|
|
397
|
+
search?: string;
|
|
398
|
+
// New filters
|
|
399
|
+
unassigned?: boolean;
|
|
400
|
+
offeredTo?: string;
|
|
401
|
+
readyOnly?: boolean;
|
|
402
|
+
taskType?: string;
|
|
403
|
+
tags?: string[];
|
|
404
|
+
scheduleId?: string;
|
|
405
|
+
/** Exact canonical asset namespace. */
|
|
406
|
+
key?: string;
|
|
407
|
+
/** Canonical namespace subtree prefix. */
|
|
408
|
+
keyPrefix?: string;
|
|
409
|
+
/** Filter to tasks whose `source` is in this list. Empty/undefined → no filter. */
|
|
410
|
+
source?: AgentTaskSource[];
|
|
411
|
+
/** ISO 8601 timestamp; only return tasks where createdAt >= this. */
|
|
412
|
+
createdAfter?: string;
|
|
413
|
+
/** ISO 8601 timestamp; only return tasks where createdAt < this. */
|
|
414
|
+
createdBefore?: string;
|
|
415
|
+
/** Only return tasks requested by this canonical user. NULL rows are excluded. */
|
|
416
|
+
requestedByUserId?: string;
|
|
417
|
+
/** When set, restrict to rows where `requestedByUserId` IS NULL. Takes priority over `requestedByUserId`. */
|
|
418
|
+
requestedByUserIdIsNull?: boolean;
|
|
419
|
+
/** Sort list rows for either table freshness or timeline paging. */
|
|
420
|
+
orderBy?: "lastUpdatedAt" | "createdAt";
|
|
421
|
+
limit?: number;
|
|
422
|
+
offset?: number;
|
|
423
|
+
includeHeartbeat?: boolean;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function getAllTasks(filters?: TaskFilters): Promise<AgentTask[]>;
|
|
427
|
+
export function getAllTasks(
|
|
428
|
+
filters: TaskFilters | undefined,
|
|
429
|
+
opts: { slim: true },
|
|
430
|
+
): Promise<AgentTaskSummary[]>;
|
|
431
|
+
export async function getAllTasks(
|
|
432
|
+
filters?: TaskFilters,
|
|
433
|
+
opts?: { slim?: boolean },
|
|
434
|
+
): Promise<AgentTask[] | AgentTaskSummary[]> {
|
|
435
|
+
const conditions: string[] = [];
|
|
436
|
+
const params: (string | AgentTaskStatus)[] = [];
|
|
437
|
+
|
|
438
|
+
if (filters?.status) {
|
|
439
|
+
if (Array.isArray(filters.status)) {
|
|
440
|
+
if (filters.status.length === 1) {
|
|
441
|
+
conditions.push("status = ?");
|
|
442
|
+
params.push(filters.status[0]!);
|
|
443
|
+
} else if (filters.status.length > 1) {
|
|
444
|
+
const placeholders = filters.status.map(() => "?").join(", ");
|
|
445
|
+
conditions.push(`status IN (${placeholders})`);
|
|
446
|
+
for (const s of filters.status) params.push(s);
|
|
447
|
+
}
|
|
448
|
+
} else {
|
|
449
|
+
conditions.push("status = ?");
|
|
450
|
+
params.push(filters.status);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (filters?.agentId) {
|
|
455
|
+
conditions.push("agentId = ?");
|
|
456
|
+
params.push(filters.agentId);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (filters?.search) {
|
|
460
|
+
conditions.push("(task LIKE ? OR id LIKE ?)");
|
|
461
|
+
params.push(`%${filters.search}%`, `%${filters.search}%`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// New filters
|
|
465
|
+
if (filters?.unassigned) {
|
|
466
|
+
conditions.push("(agentId IS NULL OR status = 'unassigned')");
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (filters?.offeredTo) {
|
|
470
|
+
conditions.push("offeredTo = ?");
|
|
471
|
+
params.push(filters.offeredTo);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (filters?.taskType) {
|
|
475
|
+
conditions.push("taskType = ?");
|
|
476
|
+
params.push(filters.taskType);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (filters?.tags && filters.tags.length > 0) {
|
|
480
|
+
// Match any of the tags
|
|
481
|
+
const tagConditions = filters.tags.map(() => "tags LIKE ?");
|
|
482
|
+
conditions.push(`(${tagConditions.join(" OR ")})`);
|
|
483
|
+
for (const tag of filters.tags) {
|
|
484
|
+
params.push(`%"${tag}"%`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (filters?.scheduleId) {
|
|
489
|
+
conditions.push("scheduleId = ?");
|
|
490
|
+
params.push(filters.scheduleId);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (filters?.key) {
|
|
494
|
+
conditions.push('"key" = ?');
|
|
495
|
+
params.push(normalizeAssetKey(filters.key));
|
|
496
|
+
} else if (filters?.keyPrefix) {
|
|
497
|
+
conditions.push(`"key" LIKE ? ESCAPE '\\'`);
|
|
498
|
+
params.push(dependencies.assetKeyPrefixPattern(filters.keyPrefix));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (filters?.source && filters.source.length > 0) {
|
|
502
|
+
const placeholders = filters.source.map(() => "?").join(", ");
|
|
503
|
+
conditions.push(`source IN (${placeholders})`);
|
|
504
|
+
for (const s of filters.source) params.push(s);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (filters?.createdAfter) {
|
|
508
|
+
conditions.push("createdAt >= ?");
|
|
509
|
+
params.push(filters.createdAfter);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (filters?.createdBefore) {
|
|
513
|
+
conditions.push("createdAt < ?");
|
|
514
|
+
params.push(filters.createdBefore);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (filters?.requestedByUserIdIsNull) {
|
|
518
|
+
conditions.push("requestedByUserId IS NULL");
|
|
519
|
+
} else if (filters?.requestedByUserId) {
|
|
520
|
+
conditions.push("requestedByUserId = ?");
|
|
521
|
+
params.push(filters.requestedByUserId);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Exclude system/heartbeat tasks by default. The flag is still called
|
|
525
|
+
// `includeHeartbeat` for backward compat with existing API callers, but we
|
|
526
|
+
// also gate boot-triage + heartbeat-checklist behind it since those are
|
|
527
|
+
// equally noisy in the dashboard task list.
|
|
528
|
+
if (!filters?.includeHeartbeat) {
|
|
529
|
+
conditions.push(
|
|
530
|
+
"(IFNULL(taskType, '') NOT IN ('heartbeat', 'heartbeat-checklist', 'boot-triage') AND tags NOT LIKE '%\"heartbeat\"%')",
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
535
|
+
const limit = filters?.limit ?? 25;
|
|
536
|
+
const offset = filters?.offset ?? 0;
|
|
537
|
+
const orderBy =
|
|
538
|
+
filters?.orderBy === "createdAt"
|
|
539
|
+
? "createdAt DESC, rowid DESC"
|
|
540
|
+
: "lastUpdatedAt DESC, priority DESC";
|
|
541
|
+
const query = `SELECT agent_tasks.*,
|
|
542
|
+
(SELECT SUM(totalCostUsd) FROM session_costs WHERE session_costs.taskId = agent_tasks.id) AS totalCostUsd
|
|
543
|
+
FROM agent_tasks ${whereClause}
|
|
544
|
+
ORDER BY ${orderBy} LIMIT ${limit} OFFSET ${offset}`;
|
|
545
|
+
|
|
546
|
+
const rows = await getDbClient().query<AgentTaskRow>(query, params);
|
|
547
|
+
|
|
548
|
+
// Filter for ready tasks (dependencies met) if requested. Both the full and
|
|
549
|
+
// the slim row shapes carry `id` + `dependsOn`, so the same predicate works.
|
|
550
|
+
const filterReady = async <T extends { id: string; dependsOn: string[] }>(
|
|
551
|
+
items: T[],
|
|
552
|
+
): Promise<T[]> => {
|
|
553
|
+
const readyFlags = await Promise.all(
|
|
554
|
+
items.map(async (task) => {
|
|
555
|
+
if (!task.dependsOn || task.dependsOn.length === 0) return true;
|
|
556
|
+
return (await dependencies.checkDependencies(task.id)).ready;
|
|
557
|
+
}),
|
|
558
|
+
);
|
|
559
|
+
return items.filter((_, i) => readyFlags[i]);
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
if (opts?.slim) {
|
|
563
|
+
let tasks = rows.map(rowToAgentTaskSummary);
|
|
564
|
+
if (filters?.readyOnly) tasks = await filterReady(tasks);
|
|
565
|
+
return tasks;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
let tasks = rows.map(rowToAgentTask);
|
|
569
|
+
if (filters?.readyOnly) tasks = await filterReady(tasks);
|
|
570
|
+
return tasks;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Get total count of tasks matching the given filters (ignoring limit).
|
|
575
|
+
* Used alongside getAllTasks to display accurate total counts in UI.
|
|
576
|
+
*/
|
|
577
|
+
export async function getTasksCount(
|
|
578
|
+
filters?: Omit<TaskFilters, "limit" | "readyOnly">,
|
|
579
|
+
): Promise<number> {
|
|
580
|
+
const conditions: string[] = [];
|
|
581
|
+
const params: (string | AgentTaskStatus)[] = [];
|
|
582
|
+
|
|
583
|
+
if (filters?.status) {
|
|
584
|
+
if (Array.isArray(filters.status)) {
|
|
585
|
+
if (filters.status.length === 1) {
|
|
586
|
+
conditions.push("status = ?");
|
|
587
|
+
params.push(filters.status[0]!);
|
|
588
|
+
} else if (filters.status.length > 1) {
|
|
589
|
+
const placeholders = filters.status.map(() => "?").join(", ");
|
|
590
|
+
conditions.push(`status IN (${placeholders})`);
|
|
591
|
+
for (const s of filters.status) params.push(s);
|
|
592
|
+
}
|
|
593
|
+
} else {
|
|
594
|
+
conditions.push("status = ?");
|
|
595
|
+
params.push(filters.status);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (filters?.agentId) {
|
|
600
|
+
conditions.push("agentId = ?");
|
|
601
|
+
params.push(filters.agentId);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (filters?.search) {
|
|
605
|
+
conditions.push("(task LIKE ? OR id LIKE ?)");
|
|
606
|
+
params.push(`%${filters.search}%`, `%${filters.search}%`);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (filters?.unassigned) {
|
|
610
|
+
conditions.push("(agentId IS NULL OR status = 'unassigned')");
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (filters?.offeredTo) {
|
|
614
|
+
conditions.push("offeredTo = ?");
|
|
615
|
+
params.push(filters.offeredTo);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (filters?.taskType) {
|
|
619
|
+
conditions.push("taskType = ?");
|
|
620
|
+
params.push(filters.taskType);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (filters?.tags && filters.tags.length > 0) {
|
|
624
|
+
const tagConditions = filters.tags.map(() => "tags LIKE ?");
|
|
625
|
+
conditions.push(`(${tagConditions.join(" OR ")})`);
|
|
626
|
+
for (const tag of filters.tags) {
|
|
627
|
+
params.push(`%"${tag}"%`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (filters?.scheduleId) {
|
|
632
|
+
conditions.push("scheduleId = ?");
|
|
633
|
+
params.push(filters.scheduleId);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (filters?.key) {
|
|
637
|
+
conditions.push('"key" = ?');
|
|
638
|
+
params.push(normalizeAssetKey(filters.key));
|
|
639
|
+
} else if (filters?.keyPrefix) {
|
|
640
|
+
conditions.push(`"key" LIKE ? ESCAPE '\\'`);
|
|
641
|
+
params.push(dependencies.assetKeyPrefixPattern(filters.keyPrefix));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (filters?.source && filters.source.length > 0) {
|
|
645
|
+
const placeholders = filters.source.map(() => "?").join(", ");
|
|
646
|
+
conditions.push(`source IN (${placeholders})`);
|
|
647
|
+
for (const s of filters.source) params.push(s);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (filters?.createdAfter) {
|
|
651
|
+
conditions.push("createdAt >= ?");
|
|
652
|
+
params.push(filters.createdAfter);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (filters?.createdBefore) {
|
|
656
|
+
conditions.push("createdAt < ?");
|
|
657
|
+
params.push(filters.createdBefore);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (filters?.requestedByUserIdIsNull) {
|
|
661
|
+
conditions.push("requestedByUserId IS NULL");
|
|
662
|
+
} else if (filters?.requestedByUserId) {
|
|
663
|
+
conditions.push("requestedByUserId = ?");
|
|
664
|
+
params.push(filters.requestedByUserId);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Exclude system/heartbeat tasks by default. The flag is still called
|
|
668
|
+
// `includeHeartbeat` for backward compat with existing API callers, but we
|
|
669
|
+
// also gate boot-triage + heartbeat-checklist behind it since those are
|
|
670
|
+
// equally noisy in the dashboard task list.
|
|
671
|
+
if (!filters?.includeHeartbeat) {
|
|
672
|
+
conditions.push(
|
|
673
|
+
"(IFNULL(taskType, '') NOT IN ('heartbeat', 'heartbeat-checklist', 'boot-triage') AND tags NOT LIKE '%\"heartbeat\"%')",
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
678
|
+
const query = `SELECT COUNT(*) as count FROM agent_tasks ${whereClause}`;
|
|
679
|
+
|
|
680
|
+
const result = await getDbClient().get<{ count: number }>(query, params);
|
|
681
|
+
|
|
682
|
+
return result?.count ?? 0;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Get task statistics (counts by status) without any limit.
|
|
687
|
+
* This is more efficient than fetching all tasks for stats purposes.
|
|
688
|
+
*/
|
|
689
|
+
export async function getTaskStats(): Promise<{
|
|
690
|
+
total: number;
|
|
691
|
+
unassigned: number;
|
|
692
|
+
offered: number;
|
|
693
|
+
reviewing: number;
|
|
694
|
+
pending: number;
|
|
695
|
+
in_progress: number;
|
|
696
|
+
paused: number;
|
|
697
|
+
completed: number;
|
|
698
|
+
failed: number;
|
|
699
|
+
}> {
|
|
700
|
+
const row = await getDbClient().get<{
|
|
701
|
+
total: number;
|
|
702
|
+
unassigned: number;
|
|
703
|
+
offered: number;
|
|
704
|
+
reviewing: number;
|
|
705
|
+
pending: number;
|
|
706
|
+
in_progress: number;
|
|
707
|
+
paused: number;
|
|
708
|
+
completed: number;
|
|
709
|
+
failed: number;
|
|
710
|
+
}>(
|
|
711
|
+
`SELECT
|
|
712
|
+
COUNT(*) as total,
|
|
713
|
+
SUM(CASE WHEN status = 'unassigned' THEN 1 ELSE 0 END) as unassigned,
|
|
714
|
+
SUM(CASE WHEN status = 'offered' THEN 1 ELSE 0 END) as offered,
|
|
715
|
+
SUM(CASE WHEN status = 'reviewing' THEN 1 ELSE 0 END) as reviewing,
|
|
716
|
+
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
|
|
717
|
+
SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) as in_progress,
|
|
718
|
+
SUM(CASE WHEN status = 'paused' THEN 1 ELSE 0 END) as paused,
|
|
719
|
+
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
|
|
720
|
+
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed
|
|
721
|
+
FROM agent_tasks`,
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
return (
|
|
725
|
+
row ?? {
|
|
726
|
+
total: 0,
|
|
727
|
+
unassigned: 0,
|
|
728
|
+
offered: 0,
|
|
729
|
+
reviewing: 0,
|
|
730
|
+
pending: 0,
|
|
731
|
+
in_progress: 0,
|
|
732
|
+
paused: 0,
|
|
733
|
+
completed: 0,
|
|
734
|
+
failed: 0,
|
|
735
|
+
}
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
export async function getCompletedSlackTasks(): Promise<AgentTask[]> {
|
|
740
|
+
const rows = await getDbClient().query<AgentTaskRow>(
|
|
741
|
+
`SELECT * FROM agent_tasks
|
|
742
|
+
WHERE slackChannelId IS NOT NULL
|
|
743
|
+
AND status IN ('completed', 'failed')
|
|
744
|
+
ORDER BY lastUpdatedAt DESC
|
|
745
|
+
LIMIT 200`,
|
|
746
|
+
);
|
|
747
|
+
return rows.map(rowToAgentTask);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Return terminal Slack-rooted tasks whose durable relay obligation is pending.
|
|
752
|
+
* The obligation is inserted by a DB trigger in the same transaction as the
|
|
753
|
+
* terminal status transition, so a process restart cannot lose the send.
|
|
754
|
+
*/
|
|
755
|
+
export async function getPendingSlackRelayTasks(): Promise<AgentTask[]> {
|
|
756
|
+
const rows = await getDbClient().query<AgentTaskRow>(
|
|
757
|
+
`SELECT task.* FROM slack_relay_obligations obligation
|
|
758
|
+
JOIN agent_tasks task ON task.id = obligation.task_id
|
|
759
|
+
WHERE obligation.delivered_at IS NULL
|
|
760
|
+
AND task.status IN ('completed', 'failed', 'cancelled')
|
|
761
|
+
ORDER BY obligation.last_attempt_at IS NOT NULL,
|
|
762
|
+
obligation.last_attempt_at ASC,
|
|
763
|
+
obligation.created_at ASC
|
|
764
|
+
LIMIT 200`,
|
|
765
|
+
);
|
|
766
|
+
return rows.map(rowToAgentTask);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** Rotate attempted rows behind fresh obligations so poison rows cannot starve the queue. */
|
|
770
|
+
export async function markSlackRelayAttempted(taskId: string): Promise<boolean> {
|
|
771
|
+
const now = new Date().toISOString();
|
|
772
|
+
const result = await getDbClient().run(
|
|
773
|
+
`UPDATE slack_relay_obligations
|
|
774
|
+
SET attempt_count = attempt_count + 1,
|
|
775
|
+
last_attempt_at = ?,
|
|
776
|
+
updated_at = ?
|
|
777
|
+
WHERE task_id = ? AND delivered_at IS NULL`,
|
|
778
|
+
[now, now, taskId],
|
|
779
|
+
);
|
|
780
|
+
return result.changes > 0;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** Mark a relay obligation delivered only after Slack accepted the final result. */
|
|
784
|
+
export async function markSlackRelayDelivered(taskId: string): Promise<boolean> {
|
|
785
|
+
const now = new Date().toISOString();
|
|
786
|
+
const result = await getDbClient().run(
|
|
787
|
+
`UPDATE slack_relay_obligations
|
|
788
|
+
SET delivered_at = ?, updated_at = ?
|
|
789
|
+
WHERE task_id = ? AND delivered_at IS NULL`,
|
|
790
|
+
[now, now, taskId],
|
|
791
|
+
);
|
|
792
|
+
return result.changes > 0;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/** Discharge obligations already fulfilled by renderer v2's durable outcome record. */
|
|
796
|
+
export async function markFinalizedSlackRelaysDelivered(): Promise<number> {
|
|
797
|
+
const now = new Date().toISOString();
|
|
798
|
+
const result = await getDbClient().run(
|
|
799
|
+
`UPDATE slack_relay_obligations AS obligation
|
|
800
|
+
SET delivered_at = ?, updated_at = ?
|
|
801
|
+
WHERE obligation.delivered_at IS NULL
|
|
802
|
+
AND EXISTS (
|
|
803
|
+
SELECT 1 FROM slack_messages message
|
|
804
|
+
WHERE message.kind = 'outcome'
|
|
805
|
+
AND message.task_id = obligation.task_id
|
|
806
|
+
AND message.finalized_at IS NOT NULL
|
|
807
|
+
)`,
|
|
808
|
+
[now, now],
|
|
809
|
+
);
|
|
810
|
+
return result.changes;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Get tasks that were recently finished (completed/failed) by workers (non-lead agents).
|
|
815
|
+
* Used by leads to know when workers complete tasks.
|
|
816
|
+
*/
|
|
817
|
+
export async function getRecentlyFinishedWorkerTasks(): Promise<AgentTask[]> {
|
|
818
|
+
// Query for finished tasks that haven't been notified yet
|
|
819
|
+
const rows = await getDbClient().query<AgentTaskRow>(
|
|
820
|
+
`SELECT t.* FROM agent_tasks t
|
|
821
|
+
LEFT JOIN agents a ON t.agentId = a.id
|
|
822
|
+
WHERE t.status IN ('completed', 'failed')
|
|
823
|
+
AND t.finishedAt IS NOT NULL
|
|
824
|
+
AND t.notifiedAt IS NULL
|
|
825
|
+
AND (a.isLead = 0 OR a.isLead IS NULL)
|
|
826
|
+
ORDER BY t.finishedAt DESC LIMIT 50`,
|
|
827
|
+
);
|
|
828
|
+
return rows.map(rowToAgentTask);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Atomically mark finished tasks as notified.
|
|
833
|
+
* Sets notifiedAt timestamp to prevent returning them in future polls.
|
|
834
|
+
*/
|
|
835
|
+
export async function markTasksNotified(taskIds: string[]): Promise<number> {
|
|
836
|
+
if (taskIds.length === 0) return 0;
|
|
837
|
+
|
|
838
|
+
const now = new Date().toISOString();
|
|
839
|
+
const placeholders = taskIds.map(() => "?").join(",");
|
|
840
|
+
|
|
841
|
+
const result = await getDbClient().run(
|
|
842
|
+
`UPDATE agent_tasks SET notifiedAt = ?
|
|
843
|
+
WHERE id IN (${placeholders}) AND notifiedAt IS NULL`,
|
|
844
|
+
[now, ...taskIds],
|
|
845
|
+
);
|
|
846
|
+
|
|
847
|
+
return result.changes;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Reset notifiedAt for tasks, allowing them to be re-delivered on next poll.
|
|
852
|
+
* Used when a trigger was consumed but the session that should process it failed.
|
|
853
|
+
* This prevents permanent notification loss from the mark-before-process race.
|
|
854
|
+
*/
|
|
855
|
+
export async function resetTasksNotified(taskIds: string[]): Promise<number> {
|
|
856
|
+
if (taskIds.length === 0) return 0;
|
|
857
|
+
|
|
858
|
+
const placeholders = taskIds.map(() => "?").join(",");
|
|
859
|
+
|
|
860
|
+
const result = await getDbClient().run(
|
|
861
|
+
`UPDATE agent_tasks SET notifiedAt = NULL
|
|
862
|
+
WHERE id IN (${placeholders}) AND notifiedAt IS NOT NULL`,
|
|
863
|
+
taskIds,
|
|
864
|
+
);
|
|
865
|
+
|
|
866
|
+
return result.changes;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
export async function getInProgressSlackTasks(): Promise<AgentTask[]> {
|
|
870
|
+
const rows = await getDbClient().query<AgentTaskRow>(
|
|
871
|
+
`SELECT * FROM agent_tasks
|
|
872
|
+
WHERE slackChannelId IS NOT NULL
|
|
873
|
+
AND status = 'in_progress'
|
|
874
|
+
ORDER BY lastUpdatedAt DESC
|
|
875
|
+
LIMIT 200`,
|
|
876
|
+
);
|
|
877
|
+
return rows.map(rowToAgentTask);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
export async function getSlackTasksMissingTree(): Promise<AgentTask[]> {
|
|
881
|
+
const rows = await getDbClient().query<AgentTaskRow>(
|
|
882
|
+
`SELECT task.* FROM agent_tasks task
|
|
883
|
+
JOIN slack_render_v2_state state ON state.id = 1
|
|
884
|
+
WHERE task.source = 'slack'
|
|
885
|
+
AND task.slackChannelId IS NOT NULL
|
|
886
|
+
AND task.slackThreadTs IS NOT NULL
|
|
887
|
+
AND task.createdAt >= state.activated_at
|
|
888
|
+
AND task.status NOT IN ('backlog', 'unassigned', 'superseded')
|
|
889
|
+
AND NOT EXISTS (
|
|
890
|
+
SELECT 1 FROM agent_tasks earlier
|
|
891
|
+
WHERE earlier.source = 'slack'
|
|
892
|
+
AND earlier.slackChannelId = task.slackChannelId
|
|
893
|
+
AND earlier.slackThreadTs = task.slackThreadTs
|
|
894
|
+
AND earlier.createdAt < state.activated_at
|
|
895
|
+
)
|
|
896
|
+
AND NOT EXISTS (
|
|
897
|
+
SELECT 1 FROM slack_messages tree
|
|
898
|
+
WHERE tree.kind = 'tree'
|
|
899
|
+
AND tree.channel_id = task.slackChannelId
|
|
900
|
+
AND tree.thread_ts = task.slackThreadTs
|
|
901
|
+
)
|
|
902
|
+
ORDER BY task.lastUpdatedAt DESC
|
|
903
|
+
LIMIT 200`,
|
|
904
|
+
);
|
|
905
|
+
return rows.map(rowToAgentTask);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Return sibling tasks for a given cross-ingress context key, optionally
|
|
910
|
+
* filtered by status. The returned shape mirrors getInProgressSlackTasks for
|
|
911
|
+
* consistency; callers can narrow further in TypeScript.
|
|
912
|
+
*
|
|
913
|
+
* See src/tasks/context-key.ts for the key schema.
|
|
914
|
+
*/
|
|
915
|
+
export async function getInProgressTasksByContextKey(
|
|
916
|
+
contextKey: string,
|
|
917
|
+
statuses: AgentTaskStatus[] = ["pending", "in_progress", "offered", "paused"],
|
|
918
|
+
): Promise<AgentTask[]> {
|
|
919
|
+
if (!contextKey || statuses.length === 0) return [];
|
|
920
|
+
const placeholders = statuses.map(() => "?").join(",");
|
|
921
|
+
const rows = await getDbClient().query<AgentTaskRow>(
|
|
922
|
+
`SELECT * FROM agent_tasks
|
|
923
|
+
WHERE contextKey = ?
|
|
924
|
+
AND status IN (${placeholders})
|
|
925
|
+
ORDER BY lastUpdatedAt DESC
|
|
926
|
+
LIMIT 200`,
|
|
927
|
+
[contextKey, ...statuses],
|
|
928
|
+
);
|
|
929
|
+
return rows.map(rowToAgentTask);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
export type ExistingTrackerContextWorkReason = "active_task" | "linked_open_pr";
|
|
933
|
+
|
|
934
|
+
export type ExistingTrackerContextWork = {
|
|
935
|
+
task: AgentTask;
|
|
936
|
+
reason: ExistingTrackerContextWorkReason;
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
const LINEAR_TRACKER_CONTEXT_KEY_PREFIX = "task:trackers:linear:";
|
|
940
|
+
|
|
941
|
+
function isLinearTrackerContextKey(contextKey: string | null | undefined): contextKey is string {
|
|
942
|
+
return !!contextKey && contextKey.startsWith(LINEAR_TRACKER_CONTEXT_KEY_PREFIX);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Return existing work for a Linear tracker key before creating another task.
|
|
947
|
+
*
|
|
948
|
+
* Active means any non-terminal task. A completed task with persisted VCS PR/MR
|
|
949
|
+
* metadata is also treated as existing work because the task can be complete
|
|
950
|
+
* while the PR is still awaiting review/merge.
|
|
951
|
+
*/
|
|
952
|
+
export async function findExistingLinearTrackerContextWork(
|
|
953
|
+
contextKey: string | null | undefined,
|
|
954
|
+
): Promise<ExistingTrackerContextWork | null> {
|
|
955
|
+
if (!isLinearTrackerContextKey(contextKey)) return null;
|
|
956
|
+
|
|
957
|
+
const activeRow = await getDbClient().get<AgentTaskRow>(
|
|
958
|
+
`SELECT * FROM agent_tasks
|
|
959
|
+
WHERE contextKey = ?
|
|
960
|
+
AND status NOT IN ('completed', 'failed', 'cancelled', 'superseded')
|
|
961
|
+
ORDER BY lastUpdatedAt DESC
|
|
962
|
+
LIMIT 1`,
|
|
963
|
+
[contextKey],
|
|
964
|
+
);
|
|
965
|
+
if (activeRow) {
|
|
966
|
+
return { task: rowToAgentTask(activeRow), reason: "active_task" };
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const linkedPrRow = await getDbClient().get<AgentTaskRow>(
|
|
970
|
+
`SELECT * FROM agent_tasks
|
|
971
|
+
WHERE contextKey = ?
|
|
972
|
+
AND status = 'completed'
|
|
973
|
+
AND vcsProvider IS NOT NULL
|
|
974
|
+
AND vcsRepo IS NOT NULL
|
|
975
|
+
AND vcsNumber IS NOT NULL
|
|
976
|
+
AND vcsUrl IS NOT NULL
|
|
977
|
+
ORDER BY lastUpdatedAt DESC
|
|
978
|
+
LIMIT 1`,
|
|
979
|
+
[contextKey],
|
|
980
|
+
);
|
|
981
|
+
if (linkedPrRow) {
|
|
982
|
+
return { task: rowToAgentTask(linkedPrRow), reason: "linked_open_pr" };
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
export async function getLatestTaskByContextKey(contextKey: string): Promise<AgentTask | null> {
|
|
989
|
+
if (!contextKey) return null;
|
|
990
|
+
const row = await getDbClient().get<AgentTaskRow>(
|
|
991
|
+
`SELECT * FROM agent_tasks
|
|
992
|
+
WHERE contextKey = ?
|
|
993
|
+
ORDER BY createdAt DESC
|
|
994
|
+
LIMIT 1`,
|
|
995
|
+
[contextKey],
|
|
996
|
+
);
|
|
997
|
+
return row ? rowToAgentTask(row) : null;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
export async function getLatestScriptRunStepTaskByContextKey(
|
|
1001
|
+
contextKey: string,
|
|
1002
|
+
): Promise<AgentTask | null> {
|
|
1003
|
+
if (!contextKey) return null;
|
|
1004
|
+
const row = await getDbClient().get<AgentTaskRow>(
|
|
1005
|
+
`SELECT * FROM agent_tasks
|
|
1006
|
+
WHERE contextKey = ?
|
|
1007
|
+
AND taskType = 'script-run-step'
|
|
1008
|
+
ORDER BY createdAt DESC, rowid DESC
|
|
1009
|
+
LIMIT 1`,
|
|
1010
|
+
[contextKey],
|
|
1011
|
+
);
|
|
1012
|
+
return row ? rowToAgentTask(row) : null;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Find the most recent agent associated with a specific Slack thread.
|
|
1017
|
+
* No status filter — returns the last agent that touched this thread regardless of task state.
|
|
1018
|
+
* This is intentional: follow-up messages should route to the same agent even after task completion.
|
|
1019
|
+
* Callers (e.g. assistant.ts) apply their own status checks (e.g. agent.status !== "offline").
|
|
1020
|
+
*/
|
|
1021
|
+
export async function getAgentWorkingOnThread(
|
|
1022
|
+
channelId: string,
|
|
1023
|
+
threadTs: string,
|
|
1024
|
+
): Promise<Agent | null> {
|
|
1025
|
+
const taskRow = await getDbClient().get<AgentTaskRow>(
|
|
1026
|
+
`SELECT * FROM agent_tasks
|
|
1027
|
+
WHERE source = 'slack'
|
|
1028
|
+
AND slackChannelId = ?
|
|
1029
|
+
AND slackThreadTs = ?
|
|
1030
|
+
ORDER BY createdAt DESC
|
|
1031
|
+
LIMIT 1`,
|
|
1032
|
+
[channelId, threadTs],
|
|
1033
|
+
);
|
|
1034
|
+
|
|
1035
|
+
if (taskRow?.agentId) return getAgentById(taskRow.agentId);
|
|
1036
|
+
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Find the latest active (in_progress or pending) task in a specific Slack thread.
|
|
1042
|
+
* Used for dependency chaining in additive Slack buffer.
|
|
1043
|
+
*/
|
|
1044
|
+
export async function getLatestActiveTaskInThread(
|
|
1045
|
+
channelId: string,
|
|
1046
|
+
threadTs: string,
|
|
1047
|
+
): Promise<AgentTask | null> {
|
|
1048
|
+
const row = await getDbClient().get<AgentTaskRow>(
|
|
1049
|
+
`SELECT * FROM agent_tasks
|
|
1050
|
+
WHERE source = 'slack'
|
|
1051
|
+
AND slackChannelId = ?
|
|
1052
|
+
AND slackThreadTs = ?
|
|
1053
|
+
AND status IN ('in_progress', 'pending')
|
|
1054
|
+
ORDER BY createdAt DESC, rowid DESC
|
|
1055
|
+
LIMIT 1`,
|
|
1056
|
+
[channelId, threadTs],
|
|
1057
|
+
);
|
|
1058
|
+
|
|
1059
|
+
return row ? rowToAgentTask(row) : null;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Find the latest task assigned to a lead agent in a specific Slack thread.
|
|
1064
|
+
* Source is deliberately restricted to Slack ingress so inherited worker-task
|
|
1065
|
+
* metadata cannot become the thread's steering target.
|
|
1066
|
+
*/
|
|
1067
|
+
export async function getLatestLeadTaskInThread(
|
|
1068
|
+
channelId: string,
|
|
1069
|
+
threadTs: string,
|
|
1070
|
+
): Promise<AgentTask | null> {
|
|
1071
|
+
const row = await getDbClient().get<AgentTaskRow>(
|
|
1072
|
+
`SELECT t.*
|
|
1073
|
+
FROM agent_tasks t
|
|
1074
|
+
JOIN agents a ON a.id = t.agentId
|
|
1075
|
+
WHERE t.source = 'slack'
|
|
1076
|
+
AND t.slackChannelId = ?
|
|
1077
|
+
AND t.slackThreadTs = ?
|
|
1078
|
+
AND a.isLead = 1
|
|
1079
|
+
ORDER BY t.createdAt DESC, t.rowid DESC
|
|
1080
|
+
LIMIT 1`,
|
|
1081
|
+
[channelId, threadTs],
|
|
1082
|
+
);
|
|
1083
|
+
|
|
1084
|
+
return row ? rowToAgentTask(row) : null;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Find the most recent task in a Slack thread, regardless of source or status.
|
|
1089
|
+
* Unlike getAgentWorkingOnThread (which filters source='slack'), this finds ALL tasks
|
|
1090
|
+
* including worker tasks that inherited Slack metadata via parentTaskId.
|
|
1091
|
+
*/
|
|
1092
|
+
export async function getMostRecentTaskInThread(
|
|
1093
|
+
channelId: string,
|
|
1094
|
+
threadTs: string,
|
|
1095
|
+
): Promise<AgentTask | null> {
|
|
1096
|
+
const row = await getDbClient().get<AgentTaskRow>(
|
|
1097
|
+
`SELECT * FROM agent_tasks
|
|
1098
|
+
WHERE slackChannelId = ?
|
|
1099
|
+
AND slackThreadTs = ?
|
|
1100
|
+
ORDER BY createdAt DESC
|
|
1101
|
+
LIMIT 1`,
|
|
1102
|
+
[channelId, threadTs],
|
|
1103
|
+
);
|
|
1104
|
+
return row ? rowToAgentTask(row) : null;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
export async function findCompletedTaskInThread(
|
|
1108
|
+
channelId: string,
|
|
1109
|
+
threadTs: string,
|
|
1110
|
+
windowMinutes: number,
|
|
1111
|
+
): Promise<AgentTask | null> {
|
|
1112
|
+
const since = new Date(Date.now() - windowMinutes * 60 * 1000).toISOString();
|
|
1113
|
+
const row = await getDbClient().get<AgentTaskRow>(
|
|
1114
|
+
`SELECT * FROM agent_tasks
|
|
1115
|
+
WHERE slackChannelId = ?
|
|
1116
|
+
AND slackThreadTs = ?
|
|
1117
|
+
AND status = 'completed'
|
|
1118
|
+
AND lastUpdatedAt > ?
|
|
1119
|
+
ORDER BY lastUpdatedAt DESC
|
|
1120
|
+
LIMIT 1`,
|
|
1121
|
+
[channelId, threadTs, since],
|
|
1122
|
+
);
|
|
1123
|
+
return row ? rowToAgentTask(row) : null;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Find the most recent CANCELLED task in a Slack thread. Used by the
|
|
1128
|
+
* follow-up re-delegation guard so a cancellation (worker SIGTERM,
|
|
1129
|
+
* runner-side abort, swarm-events tool-loop abort) doesn't permanently
|
|
1130
|
+
* jam re-dispatch when an earlier sibling task in the same thread also
|
|
1131
|
+
* completed.
|
|
1132
|
+
*
|
|
1133
|
+
* Matches both:
|
|
1134
|
+
* - `status = 'cancelled'` (the canonical terminal state from cancelTask)
|
|
1135
|
+
* - `status = 'failed'` with a failureReason that starts with "cancelled"
|
|
1136
|
+
* or "exit 130" or contains "cancelled" (the codex-adapter abort path
|
|
1137
|
+
* emits `failureReason: "cancelled"` and exits 130).
|
|
1138
|
+
*/
|
|
1139
|
+
export async function findRecentCancelledTaskInThread(
|
|
1140
|
+
channelId: string,
|
|
1141
|
+
threadTs: string,
|
|
1142
|
+
windowMinutes: number,
|
|
1143
|
+
): Promise<AgentTask | null> {
|
|
1144
|
+
const since = new Date(Date.now() - windowMinutes * 60 * 1000).toISOString();
|
|
1145
|
+
const row = await getDbClient().get<AgentTaskRow>(
|
|
1146
|
+
`SELECT * FROM agent_tasks
|
|
1147
|
+
WHERE slackChannelId = ?
|
|
1148
|
+
AND slackThreadTs = ?
|
|
1149
|
+
AND lastUpdatedAt > ?
|
|
1150
|
+
AND (
|
|
1151
|
+
status = 'cancelled'
|
|
1152
|
+
OR (
|
|
1153
|
+
status = 'failed'
|
|
1154
|
+
AND failureReason IS NOT NULL
|
|
1155
|
+
AND (
|
|
1156
|
+
failureReason LIKE 'cancelled%'
|
|
1157
|
+
OR failureReason LIKE 'exit 130%'
|
|
1158
|
+
OR failureReason LIKE '%cancelled%'
|
|
1159
|
+
)
|
|
1160
|
+
)
|
|
1161
|
+
)
|
|
1162
|
+
ORDER BY lastUpdatedAt DESC
|
|
1163
|
+
LIMIT 1`,
|
|
1164
|
+
[channelId, threadTs, since],
|
|
1165
|
+
);
|
|
1166
|
+
return row ? rowToAgentTask(row) : null;
|
|
1167
|
+
}
|