@desplega.ai/agent-swarm 1.86.0 → 1.88.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/openapi.json +84 -1
- package/package.json +7 -5
- package/src/be/db-queries/tracker.ts +21 -0
- package/src/be/db.ts +284 -21
- package/src/be/migrations/079_task_followup_config.sql +1 -0
- package/src/be/migrations/080_skill_system_defaults.sql +8 -0
- package/src/be/modelsdev-cache.json +77652 -73973
- package/src/be/seed/registry.ts +3 -2
- package/src/be/seed-skills/index.ts +172 -0
- package/src/cli.tsx +55 -0
- package/src/commands/context-preamble.ts +272 -0
- package/src/commands/e2b-stack-wizard.tsx +394 -0
- package/src/commands/e2b.ts +2027 -0
- package/src/commands/onboard/dashboard-url.ts +29 -0
- package/src/commands/onboard/steps/post-dashboard.tsx +3 -1
- package/src/commands/onboard.tsx +3 -1
- package/src/commands/resume-session.ts +35 -78
- package/src/commands/runner.ts +126 -13
- package/src/e2b/dispatch.ts +645 -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/memory.ts +13 -1
- package/src/http/session-data.ts +8 -1
- package/src/http/skills.ts +53 -0
- package/src/http/tasks.ts +152 -3
- package/src/http/webhooks.ts +75 -0
- package/src/integrations/kapso/client.ts +82 -0
- package/src/jira/sync.ts +4 -4
- package/src/linear/sync.ts +6 -5
- package/src/memory/automatic-task-gate.ts +47 -0
- package/src/prompts/base-prompt.ts +16 -1
- package/src/prompts/session-templates.ts +51 -0
- package/src/providers/claude-adapter.ts +29 -76
- package/src/providers/claude-managed-adapter.ts +61 -75
- package/src/providers/codex-adapter.ts +37 -18
- package/src/providers/codex-oauth/auth-json.ts +18 -1
- package/src/providers/codex-oauth/flow.ts +24 -1
- package/src/providers/ctx-mode-env.ts +10 -0
- package/src/providers/opencode-adapter.ts +50 -1
- package/src/providers/types.ts +6 -0
- package/src/slack/blocks.ts +12 -4
- package/src/slack/watcher.ts +3 -3
- package/src/tasks/worker-follow-up.ts +162 -2
- package/src/telemetry.ts +25 -2
- package/src/templates.d.ts +4 -0
- package/src/tests/base-prompt.test.ts +41 -0
- package/src/tests/claude-adapter.test.ts +87 -24
- package/src/tests/claude-managed-adapter.test.ts +38 -52
- package/src/tests/codex-adapter.test.ts +95 -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 +922 -0
- package/src/tests/heartbeat-supersede-resume.test.ts +285 -0
- package/src/tests/heartbeat.test.ts +26 -16
- package/src/tests/http-api-integration.test.ts +113 -0
- package/src/tests/kapso-client.test.ts +74 -1
- package/src/tests/kapso-inbound.test.ts +60 -2
- package/src/tests/opencode-adapter.test.ts +95 -0
- package/src/tests/prompt-template-remaining.test.ts +4 -0
- package/src/tests/prompt-template-session.test.ts +4 -2
- package/src/tests/resume-session.test.ts +42 -50
- package/src/tests/self-improvement.test.ts +89 -0
- package/src/tests/skill-update-scope.test.ts +88 -1
- package/src/tests/slack-blocks.test.ts +15 -0
- package/src/tests/structured-output.test.ts +69 -0
- package/src/tests/system-default-skills.test.ts +119 -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 +155 -0
- package/src/tests/vcs-tracking.test.ts +39 -0
- package/src/tools/send-task.ts +12 -1
- package/src/tools/skills/skill-delete.ts +14 -0
- package/src/tools/skills/skill-update.ts +14 -0
- package/src/tools/store-progress.ts +21 -7
- package/src/tools/templates.ts +14 -2
- package/src/types.ts +47 -1
- package/src/workflows/executors/agent-task.ts +3 -0
- package/templates/skills/artifacts/config.json +1 -0
- package/templates/skills/kv-storage/config.json +1 -0
- package/templates/skills/pages/config.json +1 -0
- package/templates/skills/scheduled-task-resilience/config.json +1 -0
- package/templates/skills/swarm-scripts/SKILL.md +91 -0
- package/templates/skills/swarm-scripts/config.json +14 -0
- package/templates/skills/swarm-scripts/content.md +86 -0
- package/templates/skills/workflow-iterate/config.json +1 -0
- package/templates/skills/workflow-structured-output/config.json +1 -0
- package/tsconfig.json +2 -1
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";
|
|
@@ -689,6 +691,14 @@ export function createAgent(
|
|
|
689
691
|
agent.harnessProvider ?? null,
|
|
690
692
|
);
|
|
691
693
|
if (!row) throw new Error("Failed to create agent");
|
|
694
|
+
try {
|
|
695
|
+
installSystemDefaultSkillsForAgent(id);
|
|
696
|
+
} catch (err) {
|
|
697
|
+
console.warn(
|
|
698
|
+
"[db] Failed to install system-default skills for new agent:",
|
|
699
|
+
(err as Error).message,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
692
702
|
try {
|
|
693
703
|
createLogEntry({ eventType: "agent_joined", agentId: id, newValue: agent.status });
|
|
694
704
|
} catch {}
|
|
@@ -993,6 +1003,7 @@ type AgentTaskRow = {
|
|
|
993
1003
|
workflowRunId: string | null;
|
|
994
1004
|
workflowRunStepId: string | null;
|
|
995
1005
|
outputSchema: string | null;
|
|
1006
|
+
followUpConfig: string | null;
|
|
996
1007
|
contextKey: string | null;
|
|
997
1008
|
createdAt: string;
|
|
998
1009
|
lastUpdatedAt: string;
|
|
@@ -1016,6 +1027,27 @@ type AgentTaskRow = {
|
|
|
1016
1027
|
};
|
|
1017
1028
|
|
|
1018
1029
|
function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
1030
|
+
let followUpConfig: FollowUpConfig | undefined;
|
|
1031
|
+
if (row.followUpConfig) {
|
|
1032
|
+
try {
|
|
1033
|
+
const parsed = FollowUpConfigSchema.safeParse(JSON.parse(row.followUpConfig));
|
|
1034
|
+
if (parsed.success) {
|
|
1035
|
+
followUpConfig = parsed.data;
|
|
1036
|
+
} else {
|
|
1037
|
+
console.warn(
|
|
1038
|
+
`[db] Ignoring invalid agent_tasks.followUpConfig for task ${row.id}:`,
|
|
1039
|
+
parsed.error.message,
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
console.warn(
|
|
1044
|
+
`[db] Ignoring malformed agent_tasks.followUpConfig for task ${row.id}:`,
|
|
1045
|
+
error instanceof Error ? error.message : String(error),
|
|
1046
|
+
);
|
|
1047
|
+
followUpConfig = undefined;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1019
1051
|
return {
|
|
1020
1052
|
id: row.id,
|
|
1021
1053
|
agentId: row.agentId,
|
|
@@ -1057,6 +1089,7 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
|
1057
1089
|
workflowRunId: row.workflowRunId ?? undefined,
|
|
1058
1090
|
workflowRunStepId: row.workflowRunStepId ?? undefined,
|
|
1059
1091
|
outputSchema: row.outputSchema ? JSON.parse(row.outputSchema) : undefined,
|
|
1092
|
+
followUpConfig,
|
|
1060
1093
|
contextKey: row.contextKey ?? undefined,
|
|
1061
1094
|
compactionCount: row.compactionCount ?? undefined,
|
|
1062
1095
|
peakContextPercent: row.peakContextPercent ?? undefined,
|
|
@@ -1173,7 +1206,7 @@ export const taskQueries = {
|
|
|
1173
1206
|
setProgress: () =>
|
|
1174
1207
|
getDb().prepare<AgentTaskRow, [string, string]>(
|
|
1175
1208
|
`UPDATE agent_tasks SET progress = ?,
|
|
1176
|
-
status = CASE WHEN status IN ('completed', 'failed', 'cancelled') THEN status ELSE 'in_progress' END,
|
|
1209
|
+
status = CASE WHEN status IN ('completed', 'failed', 'cancelled', 'superseded') THEN status ELSE 'in_progress' END,
|
|
1177
1210
|
lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
1178
1211
|
WHERE id = ? RETURNING *`,
|
|
1179
1212
|
),
|
|
@@ -1244,14 +1277,14 @@ export function startTask(taskId: string): AgentTask | null {
|
|
|
1244
1277
|
if (!oldTask) return null;
|
|
1245
1278
|
|
|
1246
1279
|
// Guard: never revive tasks that are already in a terminal state
|
|
1247
|
-
if (
|
|
1280
|
+
if (isTerminalTaskStatus(oldTask.status)) {
|
|
1248
1281
|
return null;
|
|
1249
1282
|
}
|
|
1250
1283
|
|
|
1251
1284
|
const row = getDb()
|
|
1252
1285
|
.prepare<AgentTaskRow, [string]>(
|
|
1253
1286
|
`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 *`,
|
|
1287
|
+
WHERE id = ? AND status NOT IN ('completed', 'failed', 'cancelled', 'superseded') RETURNING *`,
|
|
1255
1288
|
)
|
|
1256
1289
|
.get(taskId);
|
|
1257
1290
|
if (row && oldTask) {
|
|
@@ -1291,6 +1324,31 @@ export function getChildTasks(parentTaskId: string): AgentTask[] {
|
|
|
1291
1324
|
.map(rowToAgentTask);
|
|
1292
1325
|
}
|
|
1293
1326
|
|
|
1327
|
+
/**
|
|
1328
|
+
* Returns true if `parentId` has at least one non-terminal child task with
|
|
1329
|
+
* `taskType = 'resume'`. Used by the heartbeat sweep as an idempotency guard:
|
|
1330
|
+
* if a prior sweep tick already created a resume follow-up for this parent,
|
|
1331
|
+
* don't create a duplicate.
|
|
1332
|
+
*
|
|
1333
|
+
* **Filters by taskType = 'resume'** specifically. A parent task can also
|
|
1334
|
+
* have ordinary non-terminal delegation children (`send-task` auto-defaults
|
|
1335
|
+
* `parentTaskId` to the caller's current task — see src/tools/send-task.ts).
|
|
1336
|
+
* Treating those as "already resumed" would incorrectly skip the resume
|
|
1337
|
+
* path for a crashed worker that had delegated subtasks (PR #594 review).
|
|
1338
|
+
*/
|
|
1339
|
+
export function hasNonTerminalResumeChild(parentId: string): boolean {
|
|
1340
|
+
const row = getDb()
|
|
1341
|
+
.prepare(
|
|
1342
|
+
`SELECT 1 FROM agent_tasks
|
|
1343
|
+
WHERE parentTaskId = ?
|
|
1344
|
+
AND taskType = 'resume'
|
|
1345
|
+
AND status NOT IN ('completed', 'failed', 'cancelled', 'superseded')
|
|
1346
|
+
LIMIT 1`,
|
|
1347
|
+
)
|
|
1348
|
+
.get(parentId);
|
|
1349
|
+
return row !== undefined && row !== null;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1294
1352
|
export function updateTaskClaudeSessionId(
|
|
1295
1353
|
taskId: string,
|
|
1296
1354
|
claudeSessionId: string,
|
|
@@ -1370,14 +1428,18 @@ export function getTasksByStatus(status: AgentTaskStatus): AgentTask[] {
|
|
|
1370
1428
|
|
|
1371
1429
|
/**
|
|
1372
1430
|
* Find a task by VCS repo and issue/PR/MR number.
|
|
1373
|
-
* Returns the most recent non-
|
|
1431
|
+
* Returns the most recent non-terminal task for this VCS entity.
|
|
1432
|
+
*
|
|
1433
|
+
* Terminal exclusion MUST stay in lock-step with `TERMINAL_TASK_STATUSES`
|
|
1434
|
+
* in `src/types.ts`. SQL strings can't import a TS const — if you add a
|
|
1435
|
+
* new terminal status, grep for `NOT IN ('completed'` across this file.
|
|
1374
1436
|
*/
|
|
1375
1437
|
export function findTaskByVcs(vcsRepo: string, vcsNumber: number): AgentTask | null {
|
|
1376
1438
|
const row = getDb()
|
|
1377
1439
|
.prepare<AgentTaskRow, [string, number]>(
|
|
1378
1440
|
`SELECT * FROM agent_tasks
|
|
1379
1441
|
WHERE vcsRepo = ? AND vcsNumber = ?
|
|
1380
|
-
AND status NOT IN ('completed', 'failed')
|
|
1442
|
+
AND status NOT IN ('completed', 'failed', 'cancelled', 'superseded')
|
|
1381
1443
|
ORDER BY createdAt DESC
|
|
1382
1444
|
LIMIT 1`,
|
|
1383
1445
|
)
|
|
@@ -1926,7 +1988,7 @@ export function completeTask(id: string, output?: string): AgentTask | null {
|
|
|
1926
1988
|
// Idempotency guard: don't re-complete a task already in a terminal state.
|
|
1927
1989
|
// Mirrors cancelTask. Prevents duplicate task.completed events, duplicate
|
|
1928
1990
|
// log entries, and duplicate follow-up tasks when multiple sessions race.
|
|
1929
|
-
if (
|
|
1991
|
+
if (isTerminalTaskStatus(oldTask.status)) {
|
|
1930
1992
|
return null;
|
|
1931
1993
|
}
|
|
1932
1994
|
|
|
@@ -1971,7 +2033,7 @@ export function failTask(id: string, reason: string): AgentTask | null {
|
|
|
1971
2033
|
// Idempotency guard: don't re-fail a task already in a terminal state.
|
|
1972
2034
|
// Mirrors cancelTask / completeTask. Prevents duplicate task.failed events
|
|
1973
2035
|
// and duplicate follow-up tasks when multiple sessions race.
|
|
1974
|
-
if (
|
|
2036
|
+
if (isTerminalTaskStatus(oldTask.status)) {
|
|
1975
2037
|
return null;
|
|
1976
2038
|
}
|
|
1977
2039
|
|
|
@@ -2008,8 +2070,7 @@ export function cancelTask(id: string, reason?: string): AgentTask | null {
|
|
|
2008
2070
|
if (!oldTask) return null;
|
|
2009
2071
|
|
|
2010
2072
|
// Only cancel tasks that are not already in a terminal state
|
|
2011
|
-
|
|
2012
|
-
if (terminalStatuses.includes(oldTask.status)) {
|
|
2073
|
+
if (isTerminalTaskStatus(oldTask.status)) {
|
|
2013
2074
|
return null;
|
|
2014
2075
|
}
|
|
2015
2076
|
|
|
@@ -2043,6 +2104,69 @@ export function cancelTask(id: string, reason?: string): AgentTask | null {
|
|
|
2043
2104
|
return row ? rowToAgentTask(row) : null;
|
|
2044
2105
|
}
|
|
2045
2106
|
|
|
2107
|
+
/**
|
|
2108
|
+
* Supersede a task: mark it as `superseded` (terminal) so a fresh "resume"
|
|
2109
|
+
* follow-up task can pick up where it left off. Used by the graceful-shutdown
|
|
2110
|
+
* path and the `POST /api/tasks/:id/supersede` route. Returns null if the task
|
|
2111
|
+
* is already terminal (mirrors `completeTask` / `cancelTask` idempotency).
|
|
2112
|
+
*
|
|
2113
|
+
* Writes a `task_superseded` agent_log with `{ reason, resumeTaskId }` payload
|
|
2114
|
+
* and emits a `task.superseded` workflow event. The caller is responsible for
|
|
2115
|
+
* creating the resume follow-up (via `createResumeFollowUp`) and passing the
|
|
2116
|
+
* resulting id as `resumeTaskId`.
|
|
2117
|
+
*/
|
|
2118
|
+
export function supersedeTask(
|
|
2119
|
+
id: string,
|
|
2120
|
+
args: { reason: string; resumeTaskId: string | null },
|
|
2121
|
+
): AgentTask | null {
|
|
2122
|
+
const oldTask = getTaskById(id);
|
|
2123
|
+
if (!oldTask) return null;
|
|
2124
|
+
|
|
2125
|
+
// Idempotency guard: don't re-supersede a task already in a terminal state.
|
|
2126
|
+
if (isTerminalTaskStatus(oldTask.status)) {
|
|
2127
|
+
return null;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
const finishedAt = new Date().toISOString();
|
|
2131
|
+
const row = getDb()
|
|
2132
|
+
.prepare<AgentTaskRow, [string, string]>(
|
|
2133
|
+
`UPDATE agent_tasks
|
|
2134
|
+
SET status = 'superseded',
|
|
2135
|
+
finishedAt = ?,
|
|
2136
|
+
lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
2137
|
+
WHERE id = ? AND status NOT IN ('completed', 'failed', 'cancelled', 'superseded')
|
|
2138
|
+
RETURNING *`,
|
|
2139
|
+
)
|
|
2140
|
+
.get(finishedAt, id);
|
|
2141
|
+
|
|
2142
|
+
if (row && oldTask) {
|
|
2143
|
+
try {
|
|
2144
|
+
createLogEntry({
|
|
2145
|
+
eventType: "task_superseded",
|
|
2146
|
+
taskId: id,
|
|
2147
|
+
agentId: row.agentId ?? undefined,
|
|
2148
|
+
oldValue: oldTask.status,
|
|
2149
|
+
newValue: "superseded",
|
|
2150
|
+
metadata: { reason: args.reason, resumeTaskId: args.resumeTaskId },
|
|
2151
|
+
});
|
|
2152
|
+
} catch {}
|
|
2153
|
+
try {
|
|
2154
|
+
import("../workflows/event-bus").then(({ workflowEventBus }) => {
|
|
2155
|
+
workflowEventBus.emit("task.superseded", {
|
|
2156
|
+
taskId: id,
|
|
2157
|
+
reason: args.reason,
|
|
2158
|
+
resumeTaskId: args.resumeTaskId,
|
|
2159
|
+
agentId: row.agentId,
|
|
2160
|
+
workflowRunId: row.workflowRunId,
|
|
2161
|
+
workflowRunStepId: row.workflowRunStepId,
|
|
2162
|
+
});
|
|
2163
|
+
});
|
|
2164
|
+
} catch {}
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
return row ? rowToAgentTask(row) : null;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2046
2170
|
/**
|
|
2047
2171
|
* Pause a task that is currently in progress.
|
|
2048
2172
|
* Used during graceful shutdown to allow tasks to resume after container restart.
|
|
@@ -2560,6 +2684,7 @@ export interface CreateTaskOptions {
|
|
|
2560
2684
|
* a schema'd task should be defensive about JSON parsing.
|
|
2561
2685
|
*/
|
|
2562
2686
|
outputSchema?: Record<string, unknown>;
|
|
2687
|
+
followUpConfig?: FollowUpConfig;
|
|
2563
2688
|
requestedByUserId?: string;
|
|
2564
2689
|
contextKey?: string;
|
|
2565
2690
|
}
|
|
@@ -2578,8 +2703,9 @@ export function findRecentSimilarTasks(opts: {
|
|
|
2578
2703
|
const conditions: string[] = ["createdAt > ?"];
|
|
2579
2704
|
const params: (string | number)[] = [since];
|
|
2580
2705
|
|
|
2581
|
-
// Exclude
|
|
2582
|
-
|
|
2706
|
+
// Exclude all terminal statuses — only active or recently created.
|
|
2707
|
+
// Keep in lock-step with `TERMINAL_TASK_STATUSES` in src/types.ts.
|
|
2708
|
+
conditions.push("status NOT IN ('completed', 'failed', 'cancelled', 'superseded')");
|
|
2583
2709
|
|
|
2584
2710
|
if (opts.creatorAgentId) {
|
|
2585
2711
|
conditions.push("creatorAgentId = ?");
|
|
@@ -2614,6 +2740,16 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
2614
2740
|
if (options?.parentTaskId) {
|
|
2615
2741
|
const parent = getTaskById(options.parentTaskId);
|
|
2616
2742
|
if (parent) {
|
|
2743
|
+
// Identity & routing — anything that says "what work is this, who asked
|
|
2744
|
+
// for it, where does it run" carries forward to every child (follow-ups,
|
|
2745
|
+
// reboot retries, resume tasks). Explicit options always win.
|
|
2746
|
+
//
|
|
2747
|
+
// When adding a new identity-shaped column to `agent_tasks`, ADD IT HERE
|
|
2748
|
+
// unless you have a specific reason a child should NOT inherit it. This
|
|
2749
|
+
// is the single source of truth — `createResumeFollowUp` and the other
|
|
2750
|
+
// follow-up creators rely on this block instead of re-listing fields.
|
|
2751
|
+
|
|
2752
|
+
// Slack context
|
|
2617
2753
|
if (parent.slackChannelId && !options.slackChannelId) {
|
|
2618
2754
|
options.slackChannelId = parent.slackChannelId;
|
|
2619
2755
|
}
|
|
@@ -2623,18 +2759,98 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
2623
2759
|
if (parent.slackUserId && !options.slackUserId) {
|
|
2624
2760
|
options.slackUserId = parent.slackUserId;
|
|
2625
2761
|
}
|
|
2762
|
+
|
|
2763
|
+
// AgentMail context
|
|
2626
2764
|
if (parent.agentmailInboxId && !options.agentmailInboxId) {
|
|
2627
2765
|
options.agentmailInboxId = parent.agentmailInboxId;
|
|
2628
2766
|
}
|
|
2767
|
+
if (parent.agentmailMessageId && !options.agentmailMessageId) {
|
|
2768
|
+
options.agentmailMessageId = parent.agentmailMessageId;
|
|
2769
|
+
}
|
|
2629
2770
|
if (parent.agentmailThreadId && !options.agentmailThreadId) {
|
|
2630
2771
|
options.agentmailThreadId = parent.agentmailThreadId;
|
|
2631
2772
|
}
|
|
2773
|
+
|
|
2774
|
+
// Mention context (Slack @-mentions)
|
|
2775
|
+
if (parent.mentionMessageId && !options.mentionMessageId) {
|
|
2776
|
+
options.mentionMessageId = parent.mentionMessageId;
|
|
2777
|
+
}
|
|
2778
|
+
if (parent.mentionChannelId && !options.mentionChannelId) {
|
|
2779
|
+
options.mentionChannelId = parent.mentionChannelId;
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
// VCS identity (GitHub / GitLab issue / PR / MR + webhook routing)
|
|
2783
|
+
// Webhook handlers locate active work via `findTaskByVcs(repo, number)`,
|
|
2784
|
+
// so a resume / follow-up child MUST carry the full VCS identity or
|
|
2785
|
+
// subsequent review/update events get dropped.
|
|
2786
|
+
if (parent.vcsProvider && !options.vcsProvider) {
|
|
2787
|
+
options.vcsProvider = parent.vcsProvider;
|
|
2788
|
+
}
|
|
2789
|
+
if (parent.vcsRepo && !options.vcsRepo) {
|
|
2790
|
+
options.vcsRepo = parent.vcsRepo;
|
|
2791
|
+
}
|
|
2792
|
+
if (parent.vcsNumber != null && options.vcsNumber == null) {
|
|
2793
|
+
options.vcsNumber = parent.vcsNumber;
|
|
2794
|
+
}
|
|
2795
|
+
if (parent.vcsEventType && !options.vcsEventType) {
|
|
2796
|
+
options.vcsEventType = parent.vcsEventType;
|
|
2797
|
+
}
|
|
2798
|
+
if (parent.vcsCommentId != null && options.vcsCommentId == null) {
|
|
2799
|
+
options.vcsCommentId = parent.vcsCommentId;
|
|
2800
|
+
}
|
|
2801
|
+
if (parent.vcsAuthor && !options.vcsAuthor) {
|
|
2802
|
+
options.vcsAuthor = parent.vcsAuthor;
|
|
2803
|
+
}
|
|
2804
|
+
if (parent.vcsUrl && !options.vcsUrl) {
|
|
2805
|
+
options.vcsUrl = parent.vcsUrl;
|
|
2806
|
+
}
|
|
2807
|
+
if (parent.vcsInstallationId != null && options.vcsInstallationId == null) {
|
|
2808
|
+
options.vcsInstallationId = parent.vcsInstallationId;
|
|
2809
|
+
}
|
|
2810
|
+
if (parent.vcsNodeId && !options.vcsNodeId) {
|
|
2811
|
+
options.vcsNodeId = parent.vcsNodeId;
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
// Execution context (per-task overrides)
|
|
2815
|
+
//
|
|
2816
|
+
// `model` is DELIBERATELY NOT inherited. A parent task's `model` is a
|
|
2817
|
+
// concrete, provider-specific resolved string (e.g. `claude-opus-4-8`,
|
|
2818
|
+
// `openrouter/moonshotai/kimi-k2.6`). Derived tasks (resume follow-ups,
|
|
2819
|
+
// completion/review follow-ups, re-dispatches) routinely land on a
|
|
2820
|
+
// DIFFERENT agent — and therefore a different harness/provider — than the
|
|
2821
|
+
// parent. Carrying the parent's concrete model across that boundary makes
|
|
2822
|
+
// the child die at session-init with a model-incompatibility error before
|
|
2823
|
+
// any worker code runs (e.g. a `claude-opus-4-8` resume claimed by a Codex
|
|
2824
|
+
// worker → `400 model is not supported when using Codex`, or a
|
|
2825
|
+
// `kimi-k2.6` review follow-up routed to a Claude-harness Lead → session
|
|
2826
|
+
// exit 1). Per Taras's directive (2026-05-29): derived tasks must never
|
|
2827
|
+
// set the model — it resolves from the ASSIGNEE agent's own provider /
|
|
2828
|
+
// `MODEL_OVERRIDE` config at session-init (see
|
|
2829
|
+
// `src/commands/runner.ts` — `opts.model || configModel`). A null `model`
|
|
2830
|
+
// here is the correct, intended state. Do NOT re-add inheritance here; if
|
|
2831
|
+
// a same-provider child genuinely needs a specific model, the creator must
|
|
2832
|
+
// pass it explicitly.
|
|
2833
|
+
if (parent.dir && !options.dir) {
|
|
2834
|
+
options.dir = parent.dir;
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
// Contract (schema validation) — `store-progress` validates completion
|
|
2838
|
+
// output against `outputSchema`, runner injects structured-output
|
|
2839
|
+
// instructions only when it's present.
|
|
2840
|
+
if (parent.outputSchema && !options.outputSchema) {
|
|
2841
|
+
options.outputSchema = parent.outputSchema;
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
// Attribution
|
|
2632
2845
|
if (parent.requestedByUserId && !options.requestedByUserId) {
|
|
2633
2846
|
options.requestedByUserId = parent.requestedByUserId;
|
|
2634
2847
|
}
|
|
2635
2848
|
if (parent.contextKey && !options.contextKey) {
|
|
2636
2849
|
options.contextKey = parent.contextKey;
|
|
2637
2850
|
}
|
|
2851
|
+
if (parent.followUpConfig && !options.followUpConfig) {
|
|
2852
|
+
options.followUpConfig = parent.followUpConfig;
|
|
2853
|
+
}
|
|
2638
2854
|
}
|
|
2639
2855
|
}
|
|
2640
2856
|
|
|
@@ -2660,8 +2876,8 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
2660
2876
|
vcsInstallationId, vcsNodeId,
|
|
2661
2877
|
agentmailInboxId, agentmailMessageId, agentmailThreadId,
|
|
2662
2878
|
mentionMessageId, mentionChannelId, dir, parentTaskId, model, scheduleId,
|
|
2663
|
-
workflowRunId, workflowRunStepId, outputSchema, requestedByUserId, contextKey, swarmVersion, createdAt, lastUpdatedAt
|
|
2664
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
2879
|
+
workflowRunId, workflowRunStepId, outputSchema, followUpConfig, requestedByUserId, contextKey, swarmVersion, createdAt, lastUpdatedAt
|
|
2880
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
2665
2881
|
)
|
|
2666
2882
|
.get(
|
|
2667
2883
|
id,
|
|
@@ -2700,6 +2916,7 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
2700
2916
|
options?.workflowRunId ?? null,
|
|
2701
2917
|
options?.workflowRunStepId ?? null,
|
|
2702
2918
|
options?.outputSchema ? JSON.stringify(options.outputSchema) : null,
|
|
2919
|
+
options?.followUpConfig ? JSON.stringify(options.followUpConfig) : null,
|
|
2703
2920
|
options?.requestedByUserId ?? null,
|
|
2704
2921
|
options?.contextKey ?? null,
|
|
2705
2922
|
pkg.version,
|
|
@@ -4017,6 +4234,15 @@ export const sessionLogQueries = {
|
|
|
4017
4234
|
"SELECT * FROM session_logs WHERE taskId = ? ORDER BY iteration ASC, lineNumber ASC",
|
|
4018
4235
|
),
|
|
4019
4236
|
|
|
4237
|
+
getRecentByTaskId: () =>
|
|
4238
|
+
getDb().prepare<SessionLogRow, [string, number]>(
|
|
4239
|
+
`SELECT * FROM (
|
|
4240
|
+
SELECT * FROM session_logs WHERE taskId = ?
|
|
4241
|
+
ORDER BY iteration DESC, lineNumber DESC
|
|
4242
|
+
LIMIT ?
|
|
4243
|
+
) ORDER BY iteration ASC, lineNumber ASC`,
|
|
4244
|
+
),
|
|
4245
|
+
|
|
4020
4246
|
getBySessionId: () =>
|
|
4021
4247
|
getDb().prepare<SessionLogRow, [string, number]>(
|
|
4022
4248
|
"SELECT * FROM session_logs WHERE sessionId = ? AND iteration = ? ORDER BY lineNumber ASC",
|
|
@@ -4052,7 +4278,10 @@ export function createSessionLogs(logs: {
|
|
|
4052
4278
|
})();
|
|
4053
4279
|
}
|
|
4054
4280
|
|
|
4055
|
-
export function getSessionLogsByTaskId(taskId: string): SessionLog[] {
|
|
4281
|
+
export function getSessionLogsByTaskId(taskId: string, limit?: number): SessionLog[] {
|
|
4282
|
+
if (typeof limit === "number" && limit > 0) {
|
|
4283
|
+
return sessionLogQueries.getRecentByTaskId().all(taskId, limit).map(rowToSessionLog);
|
|
4284
|
+
}
|
|
4056
4285
|
return sessionLogQueries.getByTaskId().all(taskId).map(rowToSessionLog);
|
|
4057
4286
|
}
|
|
4058
4287
|
|
|
@@ -8026,6 +8255,7 @@ type SkillRow = {
|
|
|
8026
8255
|
userInvocable: number;
|
|
8027
8256
|
version: number;
|
|
8028
8257
|
isEnabled: number;
|
|
8258
|
+
systemDefault: number;
|
|
8029
8259
|
createdAt: string;
|
|
8030
8260
|
lastUpdatedAt: string;
|
|
8031
8261
|
lastFetchedAt: string | null;
|
|
@@ -8055,6 +8285,7 @@ function rowToSkill(row: SkillRow): Skill {
|
|
|
8055
8285
|
userInvocable: row.userInvocable === 1,
|
|
8056
8286
|
version: row.version,
|
|
8057
8287
|
isEnabled: row.isEnabled === 1,
|
|
8288
|
+
systemDefault: row.systemDefault === 1,
|
|
8058
8289
|
createdAt: row.createdAt,
|
|
8059
8290
|
lastUpdatedAt: row.lastUpdatedAt,
|
|
8060
8291
|
lastFetchedAt: row.lastFetchedAt,
|
|
@@ -8079,7 +8310,12 @@ function rowToAgentSkill(row: AgentSkillRow): AgentSkill {
|
|
|
8079
8310
|
};
|
|
8080
8311
|
}
|
|
8081
8312
|
|
|
8082
|
-
type SkillWithInstallRow = SkillRow & {
|
|
8313
|
+
type SkillWithInstallRow = SkillRow & {
|
|
8314
|
+
isActive: number;
|
|
8315
|
+
installedAt: string;
|
|
8316
|
+
sourceRank?: number;
|
|
8317
|
+
typeRank?: number;
|
|
8318
|
+
};
|
|
8083
8319
|
|
|
8084
8320
|
function rowToSkillWithInstall(row: SkillWithInstallRow): SkillWithInstallInfo {
|
|
8085
8321
|
return {
|
|
@@ -8109,6 +8345,7 @@ export interface SkillInsert {
|
|
|
8109
8345
|
agent?: string;
|
|
8110
8346
|
disableModelInvocation?: boolean;
|
|
8111
8347
|
userInvocable?: boolean;
|
|
8348
|
+
systemDefault?: boolean;
|
|
8112
8349
|
}
|
|
8113
8350
|
|
|
8114
8351
|
export function createSkill(data: SkillInsert): Skill {
|
|
@@ -8121,8 +8358,8 @@ export function createSkill(data: SkillInsert): Skill {
|
|
|
8121
8358
|
id, name, description, content, type, scope, ownerAgentId,
|
|
8122
8359
|
sourceUrl, sourceRepo, sourcePath, sourceBranch, sourceHash, isComplex,
|
|
8123
8360
|
allowedTools, model, effort, context, agent, disableModelInvocation, userInvocable,
|
|
8124
|
-
version, isEnabled, createdAt, lastUpdatedAt
|
|
8125
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 1, ?, ?) RETURNING *`,
|
|
8361
|
+
version, isEnabled, systemDefault, createdAt, lastUpdatedAt
|
|
8362
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 1, ?, ?, ?) RETURNING *`,
|
|
8126
8363
|
)
|
|
8127
8364
|
.get(
|
|
8128
8365
|
id,
|
|
@@ -8145,6 +8382,7 @@ export function createSkill(data: SkillInsert): Skill {
|
|
|
8145
8382
|
data.agent ?? null,
|
|
8146
8383
|
data.disableModelInvocation ? 1 : 0,
|
|
8147
8384
|
data.userInvocable === false ? 0 : 1,
|
|
8385
|
+
data.systemDefault ? 1 : 0,
|
|
8148
8386
|
now,
|
|
8149
8387
|
now,
|
|
8150
8388
|
);
|
|
@@ -8184,6 +8422,10 @@ export function updateSkill(
|
|
|
8184
8422
|
sets.push("isEnabled = ?");
|
|
8185
8423
|
params.push(updates.isEnabled ? 1 : 0);
|
|
8186
8424
|
}
|
|
8425
|
+
if (updates.systemDefault !== undefined) {
|
|
8426
|
+
sets.push("systemDefault = ?");
|
|
8427
|
+
params.push(updates.systemDefault ? 1 : 0);
|
|
8428
|
+
}
|
|
8187
8429
|
if (updates.allowedTools !== undefined) {
|
|
8188
8430
|
sets.push("allowedTools = ?");
|
|
8189
8431
|
params.push(updates.allowedTools ?? null);
|
|
@@ -8295,7 +8537,7 @@ export interface SkillFilters {
|
|
|
8295
8537
|
* which is replaced with an empty string so the row still satisfies `Skill`.
|
|
8296
8538
|
*/
|
|
8297
8539
|
const SKILL_SLIM_COLUMNS =
|
|
8298
|
-
"id, name, description, type, scope, ownerAgentId, sourceUrl, sourceRepo, sourcePath, sourceBranch, sourceHash, isComplex, allowedTools, model, effort, context, agent, disableModelInvocation, userInvocable, version, isEnabled, createdAt, lastUpdatedAt, lastFetchedAt, '' as content";
|
|
8540
|
+
"id, name, description, type, scope, ownerAgentId, sourceUrl, sourceRepo, sourcePath, sourceBranch, sourceHash, isComplex, allowedTools, model, effort, context, agent, disableModelInvocation, userInvocable, version, isEnabled, systemDefault, createdAt, lastUpdatedAt, lastFetchedAt, '' as content";
|
|
8299
8541
|
|
|
8300
8542
|
export function listSkills(filters?: SkillFilters): Skill[] {
|
|
8301
8543
|
const columns = filters?.includeContent === false ? SKILL_SLIM_COLUMNS : "*";
|
|
@@ -8365,6 +8607,19 @@ export function installSkill(agentId: string, skillId: string): AgentSkill {
|
|
|
8365
8607
|
return rowToAgentSkill(row);
|
|
8366
8608
|
}
|
|
8367
8609
|
|
|
8610
|
+
export function getSystemDefaultSkills(): Skill[] {
|
|
8611
|
+
return getDb()
|
|
8612
|
+
.prepare<SkillRow, []>(
|
|
8613
|
+
"SELECT * FROM skills WHERE systemDefault = 1 AND isEnabled = 1 ORDER BY name ASC",
|
|
8614
|
+
)
|
|
8615
|
+
.all()
|
|
8616
|
+
.map(rowToSkill);
|
|
8617
|
+
}
|
|
8618
|
+
|
|
8619
|
+
export function installSystemDefaultSkillsForAgent(agentId: string): AgentSkill[] {
|
|
8620
|
+
return getSystemDefaultSkills().map((skill) => installSkill(agentId, skill.id));
|
|
8621
|
+
}
|
|
8622
|
+
|
|
8368
8623
|
export function uninstallSkill(agentId: string, skillId: string): boolean {
|
|
8369
8624
|
const result = getDb()
|
|
8370
8625
|
.prepare("DELETE FROM agent_skills WHERE agentId = ? AND skillId = ?")
|
|
@@ -8374,15 +8629,23 @@ export function uninstallSkill(agentId: string, skillId: string): boolean {
|
|
|
8374
8629
|
|
|
8375
8630
|
export function getAgentSkills(agentId: string, activeOnly = true): SkillWithInstallInfo[] {
|
|
8376
8631
|
const query = `
|
|
8377
|
-
SELECT s.*, as2.isActive, as2.installedAt
|
|
8632
|
+
SELECT s.*, as2.isActive, as2.installedAt, 0 as sourceRank,
|
|
8633
|
+
CASE WHEN s.type = 'personal' THEN 0 ELSE 1 END as typeRank
|
|
8378
8634
|
FROM skills s
|
|
8379
8635
|
JOIN agent_skills as2 ON s.id = as2.skillId
|
|
8380
8636
|
WHERE as2.agentId = ?
|
|
8381
8637
|
${activeOnly ? "AND as2.isActive = 1" : ""}
|
|
8382
8638
|
AND s.isEnabled = 1
|
|
8639
|
+
UNION ALL
|
|
8640
|
+
SELECT s.*, 1 as isActive, s.createdAt as installedAt, 1 as sourceRank,
|
|
8641
|
+
CASE WHEN s.type = 'personal' THEN 0 ELSE 1 END as typeRank
|
|
8642
|
+
FROM skills s
|
|
8643
|
+
WHERE s.systemDefault = 1
|
|
8644
|
+
AND s.isEnabled = 1
|
|
8383
8645
|
ORDER BY
|
|
8384
|
-
|
|
8385
|
-
|
|
8646
|
+
sourceRank,
|
|
8647
|
+
typeRank,
|
|
8648
|
+
name
|
|
8386
8649
|
`;
|
|
8387
8650
|
|
|
8388
8651
|
const rows = getDb().prepare<SkillWithInstallRow, [string]>(query).all(agentId);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE agent_tasks ADD COLUMN followUpConfig TEXT;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- 080_skill_system_defaults.sql
|
|
2
|
+
-- Adds a first-class marker for skills that are installed for every agent.
|
|
3
|
+
-- Forward migration: add nullable-safe column with default 0.
|
|
4
|
+
-- Reverse operation, if ever needed: ALTER TABLE skills DROP COLUMN systemDefault;
|
|
5
|
+
|
|
6
|
+
ALTER TABLE skills ADD COLUMN systemDefault INTEGER NOT NULL DEFAULT 0;
|
|
7
|
+
|
|
8
|
+
CREATE INDEX IF NOT EXISTS idx_skills_system_default ON skills(systemDefault);
|