@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.
Files changed (89) hide show
  1. package/README.md +2 -1
  2. package/openapi.json +84 -1
  3. package/package.json +7 -5
  4. package/src/be/db-queries/tracker.ts +21 -0
  5. package/src/be/db.ts +284 -21
  6. package/src/be/migrations/079_task_followup_config.sql +1 -0
  7. package/src/be/migrations/080_skill_system_defaults.sql +8 -0
  8. package/src/be/modelsdev-cache.json +77652 -73973
  9. package/src/be/seed/registry.ts +3 -2
  10. package/src/be/seed-skills/index.ts +172 -0
  11. package/src/cli.tsx +55 -0
  12. package/src/commands/context-preamble.ts +272 -0
  13. package/src/commands/e2b-stack-wizard.tsx +394 -0
  14. package/src/commands/e2b.ts +2027 -0
  15. package/src/commands/onboard/dashboard-url.ts +29 -0
  16. package/src/commands/onboard/steps/post-dashboard.tsx +3 -1
  17. package/src/commands/onboard.tsx +3 -1
  18. package/src/commands/resume-session.ts +35 -78
  19. package/src/commands/runner.ts +126 -13
  20. package/src/e2b/dispatch.ts +645 -0
  21. package/src/e2b/env.ts +206 -0
  22. package/src/heartbeat/heartbeat.ts +145 -30
  23. package/src/heartbeat/templates.ts +11 -7
  24. package/src/http/memory.ts +13 -1
  25. package/src/http/session-data.ts +8 -1
  26. package/src/http/skills.ts +53 -0
  27. package/src/http/tasks.ts +152 -3
  28. package/src/http/webhooks.ts +75 -0
  29. package/src/integrations/kapso/client.ts +82 -0
  30. package/src/jira/sync.ts +4 -4
  31. package/src/linear/sync.ts +6 -5
  32. package/src/memory/automatic-task-gate.ts +47 -0
  33. package/src/prompts/base-prompt.ts +16 -1
  34. package/src/prompts/session-templates.ts +51 -0
  35. package/src/providers/claude-adapter.ts +29 -76
  36. package/src/providers/claude-managed-adapter.ts +61 -75
  37. package/src/providers/codex-adapter.ts +37 -18
  38. package/src/providers/codex-oauth/auth-json.ts +18 -1
  39. package/src/providers/codex-oauth/flow.ts +24 -1
  40. package/src/providers/ctx-mode-env.ts +10 -0
  41. package/src/providers/opencode-adapter.ts +50 -1
  42. package/src/providers/types.ts +6 -0
  43. package/src/slack/blocks.ts +12 -4
  44. package/src/slack/watcher.ts +3 -3
  45. package/src/tasks/worker-follow-up.ts +162 -2
  46. package/src/telemetry.ts +25 -2
  47. package/src/templates.d.ts +4 -0
  48. package/src/tests/base-prompt.test.ts +41 -0
  49. package/src/tests/claude-adapter.test.ts +87 -24
  50. package/src/tests/claude-managed-adapter.test.ts +38 -52
  51. package/src/tests/codex-adapter.test.ts +95 -31
  52. package/src/tests/codex-oauth.test.ts +149 -3
  53. package/src/tests/codex-pool.test.ts +14 -3
  54. package/src/tests/e2b-dispatch.test.ts +922 -0
  55. package/src/tests/heartbeat-supersede-resume.test.ts +285 -0
  56. package/src/tests/heartbeat.test.ts +26 -16
  57. package/src/tests/http-api-integration.test.ts +113 -0
  58. package/src/tests/kapso-client.test.ts +74 -1
  59. package/src/tests/kapso-inbound.test.ts +60 -2
  60. package/src/tests/opencode-adapter.test.ts +95 -0
  61. package/src/tests/prompt-template-remaining.test.ts +4 -0
  62. package/src/tests/prompt-template-session.test.ts +4 -2
  63. package/src/tests/resume-session.test.ts +42 -50
  64. package/src/tests/self-improvement.test.ts +89 -0
  65. package/src/tests/skill-update-scope.test.ts +88 -1
  66. package/src/tests/slack-blocks.test.ts +15 -0
  67. package/src/tests/structured-output.test.ts +69 -0
  68. package/src/tests/system-default-skills.test.ts +119 -0
  69. package/src/tests/task-completion-idempotency.test.ts +185 -2
  70. package/src/tests/task-supersede-resume.test.ts +722 -0
  71. package/src/tests/telemetry-init.test.ts +155 -0
  72. package/src/tests/vcs-tracking.test.ts +39 -0
  73. package/src/tools/send-task.ts +12 -1
  74. package/src/tools/skills/skill-delete.ts +14 -0
  75. package/src/tools/skills/skill-update.ts +14 -0
  76. package/src/tools/store-progress.ts +21 -7
  77. package/src/tools/templates.ts +14 -2
  78. package/src/types.ts +47 -1
  79. package/src/workflows/executors/agent-task.ts +3 -0
  80. package/templates/skills/artifacts/config.json +1 -0
  81. package/templates/skills/kv-storage/config.json +1 -0
  82. package/templates/skills/pages/config.json +1 -0
  83. package/templates/skills/scheduled-task-resilience/config.json +1 -0
  84. package/templates/skills/swarm-scripts/SKILL.md +91 -0
  85. package/templates/skills/swarm-scripts/config.json +14 -0
  86. package/templates/skills/swarm-scripts/content.md +86 -0
  87. package/templates/skills/workflow-iterate/config.json +1 -0
  88. package/templates/skills/workflow-structured-output/config.json +1 -0
  89. 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 (["completed", "failed", "cancelled"].includes(oldTask.status)) {
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-completed/failed task for this VCS entity.
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 (["completed", "failed", "cancelled"].includes(oldTask.status)) {
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 (["completed", "failed", "cancelled"].includes(oldTask.status)) {
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
- const terminalStatuses = ["completed", "failed", "cancelled"];
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 completed/failed/cancelled tasks — only active or recently created
2582
- conditions.push("status NOT IN ('completed', 'failed', 'cancelled')");
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 & { isActive: number; installedAt: string };
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
- CASE WHEN s.type = 'personal' THEN 0 ELSE 1 END,
8385
- s.name
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);