@desplega.ai/agent-swarm 1.86.0 → 1.87.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/openapi.json +72 -1
- package/package.json +3 -1
- package/src/be/db-queries/tracker.ts +21 -0
- package/src/be/db.ts +235 -14
- package/src/be/migrations/079_task_followup_config.sql +1 -0
- package/src/be/modelsdev-cache.json +77663 -74073
- package/src/cli.tsx +26 -0
- package/src/commands/context-preamble.ts +272 -0
- package/src/commands/e2b.ts +728 -0
- package/src/commands/resume-session.ts +35 -78
- package/src/commands/runner.ts +125 -13
- package/src/e2b/dispatch.ts +429 -0
- package/src/e2b/env.ts +206 -0
- package/src/heartbeat/heartbeat.ts +145 -30
- package/src/heartbeat/templates.ts +11 -7
- package/src/http/session-data.ts +8 -1
- package/src/http/tasks.ts +152 -3
- package/src/jira/sync.ts +4 -4
- package/src/linear/sync.ts +6 -5
- package/src/providers/claude-adapter.ts +10 -76
- package/src/providers/claude-managed-adapter.ts +61 -75
- package/src/providers/codex-adapter.ts +15 -18
- package/src/providers/codex-oauth/auth-json.ts +18 -1
- package/src/providers/codex-oauth/flow.ts +24 -1
- package/src/providers/types.ts +6 -0
- package/src/tasks/worker-follow-up.ts +162 -2
- package/src/telemetry.ts +11 -1
- package/src/tests/claude-adapter.test.ts +5 -27
- package/src/tests/claude-managed-adapter.test.ts +38 -52
- package/src/tests/codex-adapter.test.ts +6 -31
- package/src/tests/codex-oauth.test.ts +149 -3
- package/src/tests/codex-pool.test.ts +14 -3
- package/src/tests/e2b-dispatch.test.ts +330 -0
- package/src/tests/heartbeat-supersede-resume.test.ts +285 -0
- package/src/tests/heartbeat.test.ts +26 -16
- package/src/tests/prompt-template-remaining.test.ts +4 -0
- package/src/tests/resume-session.test.ts +42 -50
- package/src/tests/structured-output.test.ts +69 -0
- package/src/tests/task-completion-idempotency.test.ts +185 -2
- package/src/tests/task-supersede-resume.test.ts +722 -0
- package/src/tests/telemetry-init.test.ts +69 -0
- package/src/tests/vcs-tracking.test.ts +39 -0
- package/src/tools/send-task.ts +12 -1
- package/src/tools/store-progress.ts +2 -2
- package/src/tools/templates.ts +14 -2
- package/src/types.ts +46 -1
- package/src/workflows/executors/agent-task.ts +3 -0
package/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Swarm API",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.87.0",
|
|
6
6
|
"description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
|
|
7
7
|
},
|
|
8
8
|
"servers": [
|
|
@@ -7139,6 +7139,16 @@
|
|
|
7139
7139
|
"required": true,
|
|
7140
7140
|
"name": "taskId",
|
|
7141
7141
|
"in": "path"
|
|
7142
|
+
},
|
|
7143
|
+
{
|
|
7144
|
+
"schema": {
|
|
7145
|
+
"type": "integer",
|
|
7146
|
+
"minimum": 1,
|
|
7147
|
+
"maximum": 1000
|
|
7148
|
+
},
|
|
7149
|
+
"required": false,
|
|
7150
|
+
"name": "limit",
|
|
7151
|
+
"in": "query"
|
|
7142
7152
|
}
|
|
7143
7153
|
],
|
|
7144
7154
|
"responses": {
|
|
@@ -10114,6 +10124,67 @@
|
|
|
10114
10124
|
}
|
|
10115
10125
|
}
|
|
10116
10126
|
},
|
|
10127
|
+
"/api/tasks/{id}/supersede": {
|
|
10128
|
+
"post": {
|
|
10129
|
+
"summary": "Supersede an in-progress task (terminate + spawn resume follow-up)",
|
|
10130
|
+
"description": "Marks the original task `superseded` (terminal) and creates a fresh `taskType=\"resume\"` follow-up so a worker can pick up the work in a new provider session. Workflow-step tasks (those with `workflowRunStepId`) are carved out: the original is marked `failed` with reason `superseded_workflow_task` and no follow-up is created — the workflow engine's retry/failure policy applies.",
|
|
10131
|
+
"tags": [
|
|
10132
|
+
"Tasks"
|
|
10133
|
+
],
|
|
10134
|
+
"security": [
|
|
10135
|
+
{
|
|
10136
|
+
"bearerAuth": []
|
|
10137
|
+
}
|
|
10138
|
+
],
|
|
10139
|
+
"parameters": [
|
|
10140
|
+
{
|
|
10141
|
+
"schema": {
|
|
10142
|
+
"type": "string"
|
|
10143
|
+
},
|
|
10144
|
+
"required": true,
|
|
10145
|
+
"name": "id",
|
|
10146
|
+
"in": "path"
|
|
10147
|
+
}
|
|
10148
|
+
],
|
|
10149
|
+
"requestBody": {
|
|
10150
|
+
"content": {
|
|
10151
|
+
"application/json": {
|
|
10152
|
+
"schema": {
|
|
10153
|
+
"type": "object",
|
|
10154
|
+
"properties": {
|
|
10155
|
+
"reason": {
|
|
10156
|
+
"type": "string",
|
|
10157
|
+
"enum": [
|
|
10158
|
+
"graceful_shutdown",
|
|
10159
|
+
"context_limits",
|
|
10160
|
+
"manual_supersede",
|
|
10161
|
+
"crash_recovery"
|
|
10162
|
+
]
|
|
10163
|
+
}
|
|
10164
|
+
},
|
|
10165
|
+
"required": [
|
|
10166
|
+
"reason"
|
|
10167
|
+
]
|
|
10168
|
+
}
|
|
10169
|
+
}
|
|
10170
|
+
}
|
|
10171
|
+
},
|
|
10172
|
+
"responses": {
|
|
10173
|
+
"200": {
|
|
10174
|
+
"description": "Task superseded (or workflow-failed)"
|
|
10175
|
+
},
|
|
10176
|
+
"400": {
|
|
10177
|
+
"description": "Task not in_progress"
|
|
10178
|
+
},
|
|
10179
|
+
"403": {
|
|
10180
|
+
"description": "Task belongs to another agent"
|
|
10181
|
+
},
|
|
10182
|
+
"404": {
|
|
10183
|
+
"description": "Task not found"
|
|
10184
|
+
}
|
|
10185
|
+
}
|
|
10186
|
+
}
|
|
10187
|
+
},
|
|
10117
10188
|
"/api/tasks/{id}/vcs": {
|
|
10118
10189
|
"patch": {
|
|
10119
10190
|
"summary": "Update VCS (PR/MR) info for a task",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desplega.ai/agent-swarm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.87.0",
|
|
4
4
|
"description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "desplega.sh <contact@desplega.sh>",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"tsc:check": "bun tsc --noEmit",
|
|
46
46
|
"check:db-boundary": "bash scripts/check-db-boundary.sh",
|
|
47
47
|
"check:api-key-boundary": "bash scripts/check-api-key-boundary.sh",
|
|
48
|
+
"prepare-release": "bun scripts/prepare-release.ts",
|
|
48
49
|
"sync-chart-version": "bun scripts/sync-chart-version.ts",
|
|
49
50
|
"check-chart-version": "bun scripts/sync-chart-version.ts --check-if-package-version-changed",
|
|
50
51
|
"cli": "bun src/cli.tsx",
|
|
@@ -130,6 +131,7 @@
|
|
|
130
131
|
"ai": "^6.0.116",
|
|
131
132
|
"cron-parser": "^5.4.0",
|
|
132
133
|
"date-fns": "^4.1.0",
|
|
134
|
+
"e2b": "2.26.0",
|
|
133
135
|
"hono": "^4.12.3",
|
|
134
136
|
"ink": "^6.5.1",
|
|
135
137
|
"oauth4webapi": "^3.8.5",
|
|
@@ -110,6 +110,27 @@ export function updateTrackerSyncSwarmId(id: string, swarmId: string): void {
|
|
|
110
110
|
getDb().query("UPDATE tracker_sync SET swarmId = ? WHERE id = ?").run(swarmId, id);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Repoint ALL `tracker_sync` rows currently keyed to `oldSwarmId` to
|
|
115
|
+
* `newSwarmId`. Returns the number of rows updated.
|
|
116
|
+
*
|
|
117
|
+
* Used when a task is superseded (PR #594): the supersede parent becomes
|
|
118
|
+
* terminal but the Linear/Jira issue is still active, and outbound
|
|
119
|
+
* completion posts + inbound webhooks lookup by swarmId. Without
|
|
120
|
+
* repointing, the resume child's completion never makes it back to the
|
|
121
|
+
* tracker and subsequent inbound events load the terminal parent and
|
|
122
|
+
* create duplicates.
|
|
123
|
+
*
|
|
124
|
+
* Safe to call when no rows match (no-op, returns 0). Repoints across
|
|
125
|
+
* all providers (Linear AND Jira) and all entity types in one call.
|
|
126
|
+
*/
|
|
127
|
+
export function repointTrackerSyncBySwarmId(oldSwarmId: string, newSwarmId: string): number {
|
|
128
|
+
const result = getDb()
|
|
129
|
+
.query("UPDATE tracker_sync SET swarmId = ? WHERE swarmId = ?")
|
|
130
|
+
.run(newSwarmId, oldSwarmId);
|
|
131
|
+
return Number(result.changes ?? 0);
|
|
132
|
+
}
|
|
133
|
+
|
|
113
134
|
export function createTrackerSync(data: {
|
|
114
135
|
provider: string;
|
|
115
136
|
entityType: "task";
|
package/src/be/db.ts
CHANGED
|
@@ -29,6 +29,7 @@ import type {
|
|
|
29
29
|
ContextSnapshotEventType,
|
|
30
30
|
ContextVersion,
|
|
31
31
|
CooldownConfig,
|
|
32
|
+
FollowUpConfig,
|
|
32
33
|
InboxItemState,
|
|
33
34
|
InboxItemStatus,
|
|
34
35
|
InboxItemType,
|
|
@@ -87,6 +88,7 @@ import type {
|
|
|
87
88
|
WorkflowSummary,
|
|
88
89
|
WorkflowVersion,
|
|
89
90
|
} from "../types";
|
|
91
|
+
import { FollowUpConfigSchema, isTerminalTaskStatus } from "../types";
|
|
90
92
|
import { deriveProviderFromKeyType } from "../utils/credentials";
|
|
91
93
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
92
94
|
import { decryptSecret, encryptSecret, getEncryptionKey, resolveEncryptionKey } from "./crypto";
|
|
@@ -993,6 +995,7 @@ type AgentTaskRow = {
|
|
|
993
995
|
workflowRunId: string | null;
|
|
994
996
|
workflowRunStepId: string | null;
|
|
995
997
|
outputSchema: string | null;
|
|
998
|
+
followUpConfig: string | null;
|
|
996
999
|
contextKey: string | null;
|
|
997
1000
|
createdAt: string;
|
|
998
1001
|
lastUpdatedAt: string;
|
|
@@ -1016,6 +1019,27 @@ type AgentTaskRow = {
|
|
|
1016
1019
|
};
|
|
1017
1020
|
|
|
1018
1021
|
function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
1022
|
+
let followUpConfig: FollowUpConfig | undefined;
|
|
1023
|
+
if (row.followUpConfig) {
|
|
1024
|
+
try {
|
|
1025
|
+
const parsed = FollowUpConfigSchema.safeParse(JSON.parse(row.followUpConfig));
|
|
1026
|
+
if (parsed.success) {
|
|
1027
|
+
followUpConfig = parsed.data;
|
|
1028
|
+
} else {
|
|
1029
|
+
console.warn(
|
|
1030
|
+
`[db] Ignoring invalid agent_tasks.followUpConfig for task ${row.id}:`,
|
|
1031
|
+
parsed.error.message,
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
console.warn(
|
|
1036
|
+
`[db] Ignoring malformed agent_tasks.followUpConfig for task ${row.id}:`,
|
|
1037
|
+
error instanceof Error ? error.message : String(error),
|
|
1038
|
+
);
|
|
1039
|
+
followUpConfig = undefined;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1019
1043
|
return {
|
|
1020
1044
|
id: row.id,
|
|
1021
1045
|
agentId: row.agentId,
|
|
@@ -1057,6 +1081,7 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
|
1057
1081
|
workflowRunId: row.workflowRunId ?? undefined,
|
|
1058
1082
|
workflowRunStepId: row.workflowRunStepId ?? undefined,
|
|
1059
1083
|
outputSchema: row.outputSchema ? JSON.parse(row.outputSchema) : undefined,
|
|
1084
|
+
followUpConfig,
|
|
1060
1085
|
contextKey: row.contextKey ?? undefined,
|
|
1061
1086
|
compactionCount: row.compactionCount ?? undefined,
|
|
1062
1087
|
peakContextPercent: row.peakContextPercent ?? undefined,
|
|
@@ -1173,7 +1198,7 @@ export const taskQueries = {
|
|
|
1173
1198
|
setProgress: () =>
|
|
1174
1199
|
getDb().prepare<AgentTaskRow, [string, string]>(
|
|
1175
1200
|
`UPDATE agent_tasks SET progress = ?,
|
|
1176
|
-
status = CASE WHEN status IN ('completed', 'failed', 'cancelled') THEN status ELSE 'in_progress' END,
|
|
1201
|
+
status = CASE WHEN status IN ('completed', 'failed', 'cancelled', 'superseded') THEN status ELSE 'in_progress' END,
|
|
1177
1202
|
lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
1178
1203
|
WHERE id = ? RETURNING *`,
|
|
1179
1204
|
),
|
|
@@ -1244,14 +1269,14 @@ export function startTask(taskId: string): AgentTask | null {
|
|
|
1244
1269
|
if (!oldTask) return null;
|
|
1245
1270
|
|
|
1246
1271
|
// Guard: never revive tasks that are already in a terminal state
|
|
1247
|
-
if (
|
|
1272
|
+
if (isTerminalTaskStatus(oldTask.status)) {
|
|
1248
1273
|
return null;
|
|
1249
1274
|
}
|
|
1250
1275
|
|
|
1251
1276
|
const row = getDb()
|
|
1252
1277
|
.prepare<AgentTaskRow, [string]>(
|
|
1253
1278
|
`UPDATE agent_tasks SET status = 'in_progress', lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
1254
|
-
WHERE id = ? AND status NOT IN ('completed', 'failed', 'cancelled') RETURNING *`,
|
|
1279
|
+
WHERE id = ? AND status NOT IN ('completed', 'failed', 'cancelled', 'superseded') RETURNING *`,
|
|
1255
1280
|
)
|
|
1256
1281
|
.get(taskId);
|
|
1257
1282
|
if (row && oldTask) {
|
|
@@ -1291,6 +1316,31 @@ export function getChildTasks(parentTaskId: string): AgentTask[] {
|
|
|
1291
1316
|
.map(rowToAgentTask);
|
|
1292
1317
|
}
|
|
1293
1318
|
|
|
1319
|
+
/**
|
|
1320
|
+
* Returns true if `parentId` has at least one non-terminal child task with
|
|
1321
|
+
* `taskType = 'resume'`. Used by the heartbeat sweep as an idempotency guard:
|
|
1322
|
+
* if a prior sweep tick already created a resume follow-up for this parent,
|
|
1323
|
+
* don't create a duplicate.
|
|
1324
|
+
*
|
|
1325
|
+
* **Filters by taskType = 'resume'** specifically. A parent task can also
|
|
1326
|
+
* have ordinary non-terminal delegation children (`send-task` auto-defaults
|
|
1327
|
+
* `parentTaskId` to the caller's current task — see src/tools/send-task.ts).
|
|
1328
|
+
* Treating those as "already resumed" would incorrectly skip the resume
|
|
1329
|
+
* path for a crashed worker that had delegated subtasks (PR #594 review).
|
|
1330
|
+
*/
|
|
1331
|
+
export function hasNonTerminalResumeChild(parentId: string): boolean {
|
|
1332
|
+
const row = getDb()
|
|
1333
|
+
.prepare(
|
|
1334
|
+
`SELECT 1 FROM agent_tasks
|
|
1335
|
+
WHERE parentTaskId = ?
|
|
1336
|
+
AND taskType = 'resume'
|
|
1337
|
+
AND status NOT IN ('completed', 'failed', 'cancelled', 'superseded')
|
|
1338
|
+
LIMIT 1`,
|
|
1339
|
+
)
|
|
1340
|
+
.get(parentId);
|
|
1341
|
+
return row !== undefined && row !== null;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1294
1344
|
export function updateTaskClaudeSessionId(
|
|
1295
1345
|
taskId: string,
|
|
1296
1346
|
claudeSessionId: string,
|
|
@@ -1370,14 +1420,18 @@ export function getTasksByStatus(status: AgentTaskStatus): AgentTask[] {
|
|
|
1370
1420
|
|
|
1371
1421
|
/**
|
|
1372
1422
|
* Find a task by VCS repo and issue/PR/MR number.
|
|
1373
|
-
* Returns the most recent non-
|
|
1423
|
+
* Returns the most recent non-terminal task for this VCS entity.
|
|
1424
|
+
*
|
|
1425
|
+
* Terminal exclusion MUST stay in lock-step with `TERMINAL_TASK_STATUSES`
|
|
1426
|
+
* in `src/types.ts`. SQL strings can't import a TS const — if you add a
|
|
1427
|
+
* new terminal status, grep for `NOT IN ('completed'` across this file.
|
|
1374
1428
|
*/
|
|
1375
1429
|
export function findTaskByVcs(vcsRepo: string, vcsNumber: number): AgentTask | null {
|
|
1376
1430
|
const row = getDb()
|
|
1377
1431
|
.prepare<AgentTaskRow, [string, number]>(
|
|
1378
1432
|
`SELECT * FROM agent_tasks
|
|
1379
1433
|
WHERE vcsRepo = ? AND vcsNumber = ?
|
|
1380
|
-
AND status NOT IN ('completed', 'failed')
|
|
1434
|
+
AND status NOT IN ('completed', 'failed', 'cancelled', 'superseded')
|
|
1381
1435
|
ORDER BY createdAt DESC
|
|
1382
1436
|
LIMIT 1`,
|
|
1383
1437
|
)
|
|
@@ -1926,7 +1980,7 @@ export function completeTask(id: string, output?: string): AgentTask | null {
|
|
|
1926
1980
|
// Idempotency guard: don't re-complete a task already in a terminal state.
|
|
1927
1981
|
// Mirrors cancelTask. Prevents duplicate task.completed events, duplicate
|
|
1928
1982
|
// log entries, and duplicate follow-up tasks when multiple sessions race.
|
|
1929
|
-
if (
|
|
1983
|
+
if (isTerminalTaskStatus(oldTask.status)) {
|
|
1930
1984
|
return null;
|
|
1931
1985
|
}
|
|
1932
1986
|
|
|
@@ -1971,7 +2025,7 @@ export function failTask(id: string, reason: string): AgentTask | null {
|
|
|
1971
2025
|
// Idempotency guard: don't re-fail a task already in a terminal state.
|
|
1972
2026
|
// Mirrors cancelTask / completeTask. Prevents duplicate task.failed events
|
|
1973
2027
|
// and duplicate follow-up tasks when multiple sessions race.
|
|
1974
|
-
if (
|
|
2028
|
+
if (isTerminalTaskStatus(oldTask.status)) {
|
|
1975
2029
|
return null;
|
|
1976
2030
|
}
|
|
1977
2031
|
|
|
@@ -2008,8 +2062,7 @@ export function cancelTask(id: string, reason?: string): AgentTask | null {
|
|
|
2008
2062
|
if (!oldTask) return null;
|
|
2009
2063
|
|
|
2010
2064
|
// Only cancel tasks that are not already in a terminal state
|
|
2011
|
-
|
|
2012
|
-
if (terminalStatuses.includes(oldTask.status)) {
|
|
2065
|
+
if (isTerminalTaskStatus(oldTask.status)) {
|
|
2013
2066
|
return null;
|
|
2014
2067
|
}
|
|
2015
2068
|
|
|
@@ -2043,6 +2096,69 @@ export function cancelTask(id: string, reason?: string): AgentTask | null {
|
|
|
2043
2096
|
return row ? rowToAgentTask(row) : null;
|
|
2044
2097
|
}
|
|
2045
2098
|
|
|
2099
|
+
/**
|
|
2100
|
+
* Supersede a task: mark it as `superseded` (terminal) so a fresh "resume"
|
|
2101
|
+
* follow-up task can pick up where it left off. Used by the graceful-shutdown
|
|
2102
|
+
* path and the `POST /api/tasks/:id/supersede` route. Returns null if the task
|
|
2103
|
+
* is already terminal (mirrors `completeTask` / `cancelTask` idempotency).
|
|
2104
|
+
*
|
|
2105
|
+
* Writes a `task_superseded` agent_log with `{ reason, resumeTaskId }` payload
|
|
2106
|
+
* and emits a `task.superseded` workflow event. The caller is responsible for
|
|
2107
|
+
* creating the resume follow-up (via `createResumeFollowUp`) and passing the
|
|
2108
|
+
* resulting id as `resumeTaskId`.
|
|
2109
|
+
*/
|
|
2110
|
+
export function supersedeTask(
|
|
2111
|
+
id: string,
|
|
2112
|
+
args: { reason: string; resumeTaskId: string | null },
|
|
2113
|
+
): AgentTask | null {
|
|
2114
|
+
const oldTask = getTaskById(id);
|
|
2115
|
+
if (!oldTask) return null;
|
|
2116
|
+
|
|
2117
|
+
// Idempotency guard: don't re-supersede a task already in a terminal state.
|
|
2118
|
+
if (isTerminalTaskStatus(oldTask.status)) {
|
|
2119
|
+
return null;
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
const finishedAt = new Date().toISOString();
|
|
2123
|
+
const row = getDb()
|
|
2124
|
+
.prepare<AgentTaskRow, [string, string]>(
|
|
2125
|
+
`UPDATE agent_tasks
|
|
2126
|
+
SET status = 'superseded',
|
|
2127
|
+
finishedAt = ?,
|
|
2128
|
+
lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
2129
|
+
WHERE id = ? AND status NOT IN ('completed', 'failed', 'cancelled', 'superseded')
|
|
2130
|
+
RETURNING *`,
|
|
2131
|
+
)
|
|
2132
|
+
.get(finishedAt, id);
|
|
2133
|
+
|
|
2134
|
+
if (row && oldTask) {
|
|
2135
|
+
try {
|
|
2136
|
+
createLogEntry({
|
|
2137
|
+
eventType: "task_superseded",
|
|
2138
|
+
taskId: id,
|
|
2139
|
+
agentId: row.agentId ?? undefined,
|
|
2140
|
+
oldValue: oldTask.status,
|
|
2141
|
+
newValue: "superseded",
|
|
2142
|
+
metadata: { reason: args.reason, resumeTaskId: args.resumeTaskId },
|
|
2143
|
+
});
|
|
2144
|
+
} catch {}
|
|
2145
|
+
try {
|
|
2146
|
+
import("../workflows/event-bus").then(({ workflowEventBus }) => {
|
|
2147
|
+
workflowEventBus.emit("task.superseded", {
|
|
2148
|
+
taskId: id,
|
|
2149
|
+
reason: args.reason,
|
|
2150
|
+
resumeTaskId: args.resumeTaskId,
|
|
2151
|
+
agentId: row.agentId,
|
|
2152
|
+
workflowRunId: row.workflowRunId,
|
|
2153
|
+
workflowRunStepId: row.workflowRunStepId,
|
|
2154
|
+
});
|
|
2155
|
+
});
|
|
2156
|
+
} catch {}
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
return row ? rowToAgentTask(row) : null;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2046
2162
|
/**
|
|
2047
2163
|
* Pause a task that is currently in progress.
|
|
2048
2164
|
* Used during graceful shutdown to allow tasks to resume after container restart.
|
|
@@ -2560,6 +2676,7 @@ export interface CreateTaskOptions {
|
|
|
2560
2676
|
* a schema'd task should be defensive about JSON parsing.
|
|
2561
2677
|
*/
|
|
2562
2678
|
outputSchema?: Record<string, unknown>;
|
|
2679
|
+
followUpConfig?: FollowUpConfig;
|
|
2563
2680
|
requestedByUserId?: string;
|
|
2564
2681
|
contextKey?: string;
|
|
2565
2682
|
}
|
|
@@ -2578,8 +2695,9 @@ export function findRecentSimilarTasks(opts: {
|
|
|
2578
2695
|
const conditions: string[] = ["createdAt > ?"];
|
|
2579
2696
|
const params: (string | number)[] = [since];
|
|
2580
2697
|
|
|
2581
|
-
// Exclude
|
|
2582
|
-
|
|
2698
|
+
// Exclude all terminal statuses — only active or recently created.
|
|
2699
|
+
// Keep in lock-step with `TERMINAL_TASK_STATUSES` in src/types.ts.
|
|
2700
|
+
conditions.push("status NOT IN ('completed', 'failed', 'cancelled', 'superseded')");
|
|
2583
2701
|
|
|
2584
2702
|
if (opts.creatorAgentId) {
|
|
2585
2703
|
conditions.push("creatorAgentId = ?");
|
|
@@ -2614,6 +2732,16 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
2614
2732
|
if (options?.parentTaskId) {
|
|
2615
2733
|
const parent = getTaskById(options.parentTaskId);
|
|
2616
2734
|
if (parent) {
|
|
2735
|
+
// Identity & routing — anything that says "what work is this, who asked
|
|
2736
|
+
// for it, where does it run" carries forward to every child (follow-ups,
|
|
2737
|
+
// reboot retries, resume tasks). Explicit options always win.
|
|
2738
|
+
//
|
|
2739
|
+
// When adding a new identity-shaped column to `agent_tasks`, ADD IT HERE
|
|
2740
|
+
// unless you have a specific reason a child should NOT inherit it. This
|
|
2741
|
+
// is the single source of truth — `createResumeFollowUp` and the other
|
|
2742
|
+
// follow-up creators rely on this block instead of re-listing fields.
|
|
2743
|
+
|
|
2744
|
+
// Slack context
|
|
2617
2745
|
if (parent.slackChannelId && !options.slackChannelId) {
|
|
2618
2746
|
options.slackChannelId = parent.slackChannelId;
|
|
2619
2747
|
}
|
|
@@ -2623,18 +2751,98 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
2623
2751
|
if (parent.slackUserId && !options.slackUserId) {
|
|
2624
2752
|
options.slackUserId = parent.slackUserId;
|
|
2625
2753
|
}
|
|
2754
|
+
|
|
2755
|
+
// AgentMail context
|
|
2626
2756
|
if (parent.agentmailInboxId && !options.agentmailInboxId) {
|
|
2627
2757
|
options.agentmailInboxId = parent.agentmailInboxId;
|
|
2628
2758
|
}
|
|
2759
|
+
if (parent.agentmailMessageId && !options.agentmailMessageId) {
|
|
2760
|
+
options.agentmailMessageId = parent.agentmailMessageId;
|
|
2761
|
+
}
|
|
2629
2762
|
if (parent.agentmailThreadId && !options.agentmailThreadId) {
|
|
2630
2763
|
options.agentmailThreadId = parent.agentmailThreadId;
|
|
2631
2764
|
}
|
|
2765
|
+
|
|
2766
|
+
// Mention context (Slack @-mentions)
|
|
2767
|
+
if (parent.mentionMessageId && !options.mentionMessageId) {
|
|
2768
|
+
options.mentionMessageId = parent.mentionMessageId;
|
|
2769
|
+
}
|
|
2770
|
+
if (parent.mentionChannelId && !options.mentionChannelId) {
|
|
2771
|
+
options.mentionChannelId = parent.mentionChannelId;
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
// VCS identity (GitHub / GitLab issue / PR / MR + webhook routing)
|
|
2775
|
+
// Webhook handlers locate active work via `findTaskByVcs(repo, number)`,
|
|
2776
|
+
// so a resume / follow-up child MUST carry the full VCS identity or
|
|
2777
|
+
// subsequent review/update events get dropped.
|
|
2778
|
+
if (parent.vcsProvider && !options.vcsProvider) {
|
|
2779
|
+
options.vcsProvider = parent.vcsProvider;
|
|
2780
|
+
}
|
|
2781
|
+
if (parent.vcsRepo && !options.vcsRepo) {
|
|
2782
|
+
options.vcsRepo = parent.vcsRepo;
|
|
2783
|
+
}
|
|
2784
|
+
if (parent.vcsNumber != null && options.vcsNumber == null) {
|
|
2785
|
+
options.vcsNumber = parent.vcsNumber;
|
|
2786
|
+
}
|
|
2787
|
+
if (parent.vcsEventType && !options.vcsEventType) {
|
|
2788
|
+
options.vcsEventType = parent.vcsEventType;
|
|
2789
|
+
}
|
|
2790
|
+
if (parent.vcsCommentId != null && options.vcsCommentId == null) {
|
|
2791
|
+
options.vcsCommentId = parent.vcsCommentId;
|
|
2792
|
+
}
|
|
2793
|
+
if (parent.vcsAuthor && !options.vcsAuthor) {
|
|
2794
|
+
options.vcsAuthor = parent.vcsAuthor;
|
|
2795
|
+
}
|
|
2796
|
+
if (parent.vcsUrl && !options.vcsUrl) {
|
|
2797
|
+
options.vcsUrl = parent.vcsUrl;
|
|
2798
|
+
}
|
|
2799
|
+
if (parent.vcsInstallationId != null && options.vcsInstallationId == null) {
|
|
2800
|
+
options.vcsInstallationId = parent.vcsInstallationId;
|
|
2801
|
+
}
|
|
2802
|
+
if (parent.vcsNodeId && !options.vcsNodeId) {
|
|
2803
|
+
options.vcsNodeId = parent.vcsNodeId;
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
// Execution context (per-task overrides)
|
|
2807
|
+
//
|
|
2808
|
+
// `model` is DELIBERATELY NOT inherited. A parent task's `model` is a
|
|
2809
|
+
// concrete, provider-specific resolved string (e.g. `claude-opus-4-8`,
|
|
2810
|
+
// `openrouter/moonshotai/kimi-k2.6`). Derived tasks (resume follow-ups,
|
|
2811
|
+
// completion/review follow-ups, re-dispatches) routinely land on a
|
|
2812
|
+
// DIFFERENT agent — and therefore a different harness/provider — than the
|
|
2813
|
+
// parent. Carrying the parent's concrete model across that boundary makes
|
|
2814
|
+
// the child die at session-init with a model-incompatibility error before
|
|
2815
|
+
// any worker code runs (e.g. a `claude-opus-4-8` resume claimed by a Codex
|
|
2816
|
+
// worker → `400 model is not supported when using Codex`, or a
|
|
2817
|
+
// `kimi-k2.6` review follow-up routed to a Claude-harness Lead → session
|
|
2818
|
+
// exit 1). Per Taras's directive (2026-05-29): derived tasks must never
|
|
2819
|
+
// set the model — it resolves from the ASSIGNEE agent's own provider /
|
|
2820
|
+
// `MODEL_OVERRIDE` config at session-init (see
|
|
2821
|
+
// `src/commands/runner.ts` — `opts.model || configModel`). A null `model`
|
|
2822
|
+
// here is the correct, intended state. Do NOT re-add inheritance here; if
|
|
2823
|
+
// a same-provider child genuinely needs a specific model, the creator must
|
|
2824
|
+
// pass it explicitly.
|
|
2825
|
+
if (parent.dir && !options.dir) {
|
|
2826
|
+
options.dir = parent.dir;
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
// Contract (schema validation) — `store-progress` validates completion
|
|
2830
|
+
// output against `outputSchema`, runner injects structured-output
|
|
2831
|
+
// instructions only when it's present.
|
|
2832
|
+
if (parent.outputSchema && !options.outputSchema) {
|
|
2833
|
+
options.outputSchema = parent.outputSchema;
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
// Attribution
|
|
2632
2837
|
if (parent.requestedByUserId && !options.requestedByUserId) {
|
|
2633
2838
|
options.requestedByUserId = parent.requestedByUserId;
|
|
2634
2839
|
}
|
|
2635
2840
|
if (parent.contextKey && !options.contextKey) {
|
|
2636
2841
|
options.contextKey = parent.contextKey;
|
|
2637
2842
|
}
|
|
2843
|
+
if (parent.followUpConfig && !options.followUpConfig) {
|
|
2844
|
+
options.followUpConfig = parent.followUpConfig;
|
|
2845
|
+
}
|
|
2638
2846
|
}
|
|
2639
2847
|
}
|
|
2640
2848
|
|
|
@@ -2660,8 +2868,8 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
2660
2868
|
vcsInstallationId, vcsNodeId,
|
|
2661
2869
|
agentmailInboxId, agentmailMessageId, agentmailThreadId,
|
|
2662
2870
|
mentionMessageId, mentionChannelId, dir, parentTaskId, model, scheduleId,
|
|
2663
|
-
workflowRunId, workflowRunStepId, outputSchema, requestedByUserId, contextKey, swarmVersion, createdAt, lastUpdatedAt
|
|
2664
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
2871
|
+
workflowRunId, workflowRunStepId, outputSchema, followUpConfig, requestedByUserId, contextKey, swarmVersion, createdAt, lastUpdatedAt
|
|
2872
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
2665
2873
|
)
|
|
2666
2874
|
.get(
|
|
2667
2875
|
id,
|
|
@@ -2700,6 +2908,7 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
2700
2908
|
options?.workflowRunId ?? null,
|
|
2701
2909
|
options?.workflowRunStepId ?? null,
|
|
2702
2910
|
options?.outputSchema ? JSON.stringify(options.outputSchema) : null,
|
|
2911
|
+
options?.followUpConfig ? JSON.stringify(options.followUpConfig) : null,
|
|
2703
2912
|
options?.requestedByUserId ?? null,
|
|
2704
2913
|
options?.contextKey ?? null,
|
|
2705
2914
|
pkg.version,
|
|
@@ -4017,6 +4226,15 @@ export const sessionLogQueries = {
|
|
|
4017
4226
|
"SELECT * FROM session_logs WHERE taskId = ? ORDER BY iteration ASC, lineNumber ASC",
|
|
4018
4227
|
),
|
|
4019
4228
|
|
|
4229
|
+
getRecentByTaskId: () =>
|
|
4230
|
+
getDb().prepare<SessionLogRow, [string, number]>(
|
|
4231
|
+
`SELECT * FROM (
|
|
4232
|
+
SELECT * FROM session_logs WHERE taskId = ?
|
|
4233
|
+
ORDER BY iteration DESC, lineNumber DESC
|
|
4234
|
+
LIMIT ?
|
|
4235
|
+
) ORDER BY iteration ASC, lineNumber ASC`,
|
|
4236
|
+
),
|
|
4237
|
+
|
|
4020
4238
|
getBySessionId: () =>
|
|
4021
4239
|
getDb().prepare<SessionLogRow, [string, number]>(
|
|
4022
4240
|
"SELECT * FROM session_logs WHERE sessionId = ? AND iteration = ? ORDER BY lineNumber ASC",
|
|
@@ -4052,7 +4270,10 @@ export function createSessionLogs(logs: {
|
|
|
4052
4270
|
})();
|
|
4053
4271
|
}
|
|
4054
4272
|
|
|
4055
|
-
export function getSessionLogsByTaskId(taskId: string): SessionLog[] {
|
|
4273
|
+
export function getSessionLogsByTaskId(taskId: string, limit?: number): SessionLog[] {
|
|
4274
|
+
if (typeof limit === "number" && limit > 0) {
|
|
4275
|
+
return sessionLogQueries.getRecentByTaskId().all(taskId, limit).map(rowToSessionLog);
|
|
4276
|
+
}
|
|
4056
4277
|
return sessionLogQueries.getByTaskId().all(taskId).map(rowToSessionLog);
|
|
4057
4278
|
}
|
|
4058
4279
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE agent_tasks ADD COLUMN followUpConfig TEXT;
|