@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.
Files changed (47) hide show
  1. package/openapi.json +72 -1
  2. package/package.json +3 -1
  3. package/src/be/db-queries/tracker.ts +21 -0
  4. package/src/be/db.ts +235 -14
  5. package/src/be/migrations/079_task_followup_config.sql +1 -0
  6. package/src/be/modelsdev-cache.json +77663 -74073
  7. package/src/cli.tsx +26 -0
  8. package/src/commands/context-preamble.ts +272 -0
  9. package/src/commands/e2b.ts +728 -0
  10. package/src/commands/resume-session.ts +35 -78
  11. package/src/commands/runner.ts +125 -13
  12. package/src/e2b/dispatch.ts +429 -0
  13. package/src/e2b/env.ts +206 -0
  14. package/src/heartbeat/heartbeat.ts +145 -30
  15. package/src/heartbeat/templates.ts +11 -7
  16. package/src/http/session-data.ts +8 -1
  17. package/src/http/tasks.ts +152 -3
  18. package/src/jira/sync.ts +4 -4
  19. package/src/linear/sync.ts +6 -5
  20. package/src/providers/claude-adapter.ts +10 -76
  21. package/src/providers/claude-managed-adapter.ts +61 -75
  22. package/src/providers/codex-adapter.ts +15 -18
  23. package/src/providers/codex-oauth/auth-json.ts +18 -1
  24. package/src/providers/codex-oauth/flow.ts +24 -1
  25. package/src/providers/types.ts +6 -0
  26. package/src/tasks/worker-follow-up.ts +162 -2
  27. package/src/telemetry.ts +11 -1
  28. package/src/tests/claude-adapter.test.ts +5 -27
  29. package/src/tests/claude-managed-adapter.test.ts +38 -52
  30. package/src/tests/codex-adapter.test.ts +6 -31
  31. package/src/tests/codex-oauth.test.ts +149 -3
  32. package/src/tests/codex-pool.test.ts +14 -3
  33. package/src/tests/e2b-dispatch.test.ts +330 -0
  34. package/src/tests/heartbeat-supersede-resume.test.ts +285 -0
  35. package/src/tests/heartbeat.test.ts +26 -16
  36. package/src/tests/prompt-template-remaining.test.ts +4 -0
  37. package/src/tests/resume-session.test.ts +42 -50
  38. package/src/tests/structured-output.test.ts +69 -0
  39. package/src/tests/task-completion-idempotency.test.ts +185 -2
  40. package/src/tests/task-supersede-resume.test.ts +722 -0
  41. package/src/tests/telemetry-init.test.ts +69 -0
  42. package/src/tests/vcs-tracking.test.ts +39 -0
  43. package/src/tools/send-task.ts +12 -1
  44. package/src/tools/store-progress.ts +2 -2
  45. package/src/tools/templates.ts +14 -2
  46. package/src/types.ts +46 -1
  47. package/src/workflows/executors/agent-task.ts +3 -0
@@ -208,7 +208,7 @@ describe("Heartbeat Triage", () => {
208
208
  // ==========================================================================
209
209
 
210
210
  describe("Code-Level Triage", () => {
211
- test("auto-fails stalled task with no active session", async () => {
211
+ test("auto-supersedes stalled task with no active session (DES-523)", async () => {
212
212
  const agent = createAgent({ name: "dead-worker", isLead: false, status: "busy" });
213
213
  const task = createTaskExtended("Stalled task", { agentId: agent.id });
214
214
  startTask(task.id);
@@ -219,17 +219,21 @@ describe("Heartbeat Triage", () => {
219
219
 
220
220
  const findings = await codeLevelTriage();
221
221
 
222
- expect(findings.autoFailedTasks.length).toBe(1);
223
- expect(findings.autoFailedTasks[0]!.taskId).toBe(task.id);
222
+ expect(findings.autoResumedTasks.length).toBe(1);
223
+ expect(findings.autoResumedTasks[0]!.taskId).toBe(task.id);
224
+ expect(findings.autoResumedTasks[0]!.reason).toContain("no active session");
225
+ expect(findings.autoFailedTasks.length).toBe(0);
224
226
  expect(findings.stalledTasks.length).toBe(0);
225
227
 
226
- // Verify task is actually failed in DB
228
+ // Verify task is superseded (not failed) the resume task carries the work forward.
229
+ // `failureReason` is unset on superseded tasks; the supersede reason lives on the log
230
+ // entry and on `findings.autoResumedTasks[].reason` (checked above).
227
231
  const updated = getTaskById(task.id);
228
- expect(updated?.status).toBe("failed");
229
- expect(updated?.failureReason).toContain("no active session");
232
+ expect(updated?.status).toBe("superseded");
233
+ expect(updated?.failureReason).toBeFalsy();
230
234
  });
231
235
 
232
- test("auto-fails stalled task with stale session heartbeat", async () => {
236
+ test("auto-supersedes stalled task with stale session heartbeat (DES-523)", async () => {
233
237
  const agent = createAgent({ name: "crashed-worker", isLead: false, status: "busy" });
234
238
  const task = createTaskExtended("Stalled task", { agentId: agent.id });
235
239
  startTask(task.id);
@@ -250,14 +254,18 @@ describe("Heartbeat Triage", () => {
250
254
 
251
255
  const findings = await codeLevelTriage();
252
256
 
253
- expect(findings.autoFailedTasks.length).toBe(1);
254
- expect(findings.autoFailedTasks[0]!.taskId).toBe(task.id);
257
+ expect(findings.autoResumedTasks.length).toBe(1);
258
+ expect(findings.autoResumedTasks[0]!.taskId).toBe(task.id);
259
+ expect(findings.autoResumedTasks[0]!.reason).toContain("stale");
260
+ expect(findings.autoFailedTasks.length).toBe(0);
255
261
  expect(findings.stalledTasks.length).toBe(0);
256
262
 
257
- // Verify task is failed and session is deleted
263
+ // Verify task is superseded and session is deleted.
264
+ // `failureReason` is unset on superseded tasks; the supersede reason lives on the log
265
+ // entry and on `findings.autoResumedTasks[].reason` (checked above).
258
266
  const updated = getTaskById(task.id);
259
- expect(updated?.status).toBe("failed");
260
- expect(updated?.failureReason).toContain("stale");
267
+ expect(updated?.status).toBe("superseded");
268
+ expect(updated?.failureReason).toBeFalsy();
261
269
 
262
270
  const session = getActiveSessionForTask(task.id);
263
271
  expect(session).toBeNull();
@@ -361,7 +369,7 @@ describe("Heartbeat Triage", () => {
361
369
  expect(findings.stalledTasks.length).toBe(0);
362
370
  });
363
371
 
364
- test("sets agent to idle after auto-failing its only task", async () => {
372
+ test("sets agent to idle after auto-superseding its only task", async () => {
365
373
  const agent = createAgent({ name: "dead-worker", isLead: false, status: "busy" });
366
374
  const task = createTaskExtended("Stalled task", { agentId: agent.id });
367
375
  startTask(task.id);
@@ -371,7 +379,8 @@ describe("Heartbeat Triage", () => {
371
379
 
372
380
  await codeLevelTriage();
373
381
 
374
- // Agent should be set to idle since it has no more active tasks
382
+ // Agent should be set to idle since the parent task is terminal (superseded)
383
+ // and the resume follow-up was routed to the unassigned pool (worker is "dead").
375
384
  const agents = getDb().query("SELECT status FROM agents WHERE id = ?").get(agent.id) as {
376
385
  status: string;
377
386
  };
@@ -404,7 +413,7 @@ describe("Heartbeat Triage", () => {
404
413
  expect(tasks.length).toBe(1);
405
414
  });
406
415
 
407
- test("auto-fails stalled task with no session during sweep", async () => {
416
+ test("auto-supersedes stalled task with no session during sweep", async () => {
408
417
  const worker = createAgent({ name: "dead-worker", isLead: false, status: "busy" });
409
418
  const task = createTaskExtended("Stalled no-session", { agentId: worker.id });
410
419
  startTask(task.id);
@@ -415,7 +424,8 @@ describe("Heartbeat Triage", () => {
415
424
  await runHeartbeatSweep();
416
425
 
417
426
  const updated = getTaskById(task.id);
418
- expect(updated?.status).toBe("failed");
427
+ // DES-523: heartbeat sweep now creates a resume follow-up instead of silently failing.
428
+ expect(updated?.status).toBe("superseded");
419
429
  });
420
430
 
421
431
  test("cleans stale sessions even when preflight gate bails", async () => {
@@ -324,6 +324,7 @@ describe("Task lifecycle — backward compatibility", () => {
324
324
  test("task.worker.completed produces expected output", () => {
325
325
  const result = resolveTemplate("task.worker.completed", {
326
326
  agent_name: "Worker-1",
327
+ creator_agent: "creator-agent-1",
327
328
  task_desc: "Fix the login page CSS",
328
329
  output_summary: "Fixed the CSS alignment issue on the login form",
329
330
  task_id: "task-abc-123",
@@ -334,6 +335,7 @@ describe("Task lifecycle — backward compatibility", () => {
334
335
  const expected = `Worker task completed \u2014 review needed.
335
336
 
336
337
  Agent: Worker-1
338
+ Original task created by agent creator-agent-1
337
339
  Task: "Fix the login page CSS"
338
340
 
339
341
  Output:
@@ -352,6 +354,7 @@ Use \`get-task-details\` with taskId "task-abc-123" for full details.`;
352
354
  test("task.worker.failed produces expected output", () => {
353
355
  const result = resolveTemplate("task.worker.failed", {
354
356
  agent_name: "Worker-2",
357
+ creator_agent: "creator-agent-2",
355
358
  task_desc: "Deploy to production",
356
359
  failure_reason: "Docker build failed: missing dependency",
357
360
  task_id: "task-def-456",
@@ -362,6 +365,7 @@ Use \`get-task-details\` with taskId "task-abc-123" for full details.`;
362
365
  const expected = `Worker task failed \u2014 action needed.
363
366
 
364
367
  Agent: Worker-2
368
+ Original task created by agent creator-agent-2
365
369
  Task: "Deploy to production"
366
370
 
367
371
  Failure reason: Docker build failed: missing dependency
@@ -1,8 +1,12 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { resolveResumeSession } from "../commands/resume-session";
2
+ import { RESUME_DEPRECATED_REASON, resolveResumeSession } from "../commands/resume-session";
3
3
 
4
- describe("resolveResumeSession", () => {
5
- test("allows local Claude resume for UUID session ids", () => {
4
+ // Native resume was deprecated in the 2026-05-28 plan. resolveResumeSession
5
+ // is now an observability shim it records the candidates that would have
6
+ // been resume targets in the old world, but never returns a resumeSessionId.
7
+
8
+ describe("resolveResumeSession (observability shim)", () => {
9
+ test("returns no resumeSessionId for any candidate", () => {
6
10
  const resolution = resolveResumeSession("claude", [
7
11
  {
8
12
  source: "task",
@@ -11,28 +15,18 @@ describe("resolveResumeSession", () => {
11
15
  },
12
16
  ]);
13
17
 
14
- expect(resolution.resumeSessionId).toBe("69dbe5a1-1130-45eb-983f-58a7a13c9c3c");
15
- expect(resolution.source).toBe("task");
16
- expect(resolution.provider).toBe("claude");
17
- expect(resolution.skipped).toEqual([]);
18
+ expect(resolution.resumeSessionId).toBeUndefined();
19
+ expect(resolution.source).toBeUndefined();
20
+ expect(resolution.provider).toBeUndefined();
18
21
  });
19
22
 
20
- test("rejects non-UUID ids for local Claude resume", () => {
23
+ test("records every non-empty candidate in skipped with the deprecation reason", () => {
21
24
  const resolution = resolveResumeSession("claude", [
22
25
  {
23
26
  source: "task",
24
- sessionId: "ses_19c145de3ffeD9qLlntj8SRO28",
27
+ sessionId: "69dbe5a1-1130-45eb-983f-58a7a13c9c3c",
25
28
  provider: "claude",
26
29
  },
27
- ]);
28
-
29
- expect(resolution.resumeSessionId).toBeUndefined();
30
- expect(resolution.skipped).toHaveLength(1);
31
- expect(resolution.skipped[0]?.reason).toBe("Claude CLI --resume requires a UUID session id");
32
- });
33
-
34
- test("normalizes legacy managed Claude rows to claude-managed", () => {
35
- const resolution = resolveResumeSession("claude-managed", [
36
30
  {
37
31
  source: "parent",
38
32
  sessionId: "sesn_resume_xyz",
@@ -41,53 +35,51 @@ describe("resolveResumeSession", () => {
41
35
  },
42
36
  ]);
43
37
 
44
- expect(resolution.resumeSessionId).toBe("sesn_resume_xyz");
45
- expect(resolution.source).toBe("parent");
46
- expect(resolution.provider).toBe("claude-managed");
38
+ expect(resolution.skipped).toHaveLength(2);
39
+ for (const entry of resolution.skipped) {
40
+ expect(entry.reason).toBe(RESUME_DEPRECATED_REASON);
41
+ }
42
+ expect(resolution.skipped[0]?.source).toBe("task");
43
+ expect(resolution.skipped[0]?.sessionId).toBe("69dbe5a1-1130-45eb-983f-58a7a13c9c3c");
44
+ expect(resolution.skipped[1]?.source).toBe("parent");
45
+ expect(resolution.skipped[1]?.sessionId).toBe("sesn_resume_xyz");
47
46
  });
48
47
 
49
- test("skips mismatched provider sessions and falls back to parent", () => {
48
+ test("ignores candidates with empty / whitespace-only sessionId", () => {
50
49
  const resolution = resolveResumeSession("claude", [
51
- {
52
- source: "task",
53
- sessionId: "thread-codex",
54
- provider: "codex",
55
- },
56
- {
57
- source: "parent",
58
- sessionId: "69dbe5a1-1130-45eb-983f-58a7a13c9c3c",
59
- provider: "claude",
60
- },
50
+ { source: "task", sessionId: undefined },
51
+ { source: "task", sessionId: null },
52
+ { source: "task", sessionId: "" },
53
+ { source: "parent", sessionId: " " },
61
54
  ]);
62
55
 
63
- expect(resolution.resumeSessionId).toBe("69dbe5a1-1130-45eb-983f-58a7a13c9c3c");
64
- expect(resolution.source).toBe("parent");
65
- expect(resolution.skipped).toHaveLength(1);
66
- expect(resolution.skipped[0]?.reason).toContain("does not match current provider");
56
+ expect(resolution.skipped).toEqual([]);
57
+ expect(resolution.resumeSessionId).toBeUndefined();
67
58
  });
68
59
 
69
- test("rejects legacy unknown non-UUID Claude session ids", () => {
60
+ test("preserves the candidate provider in the skipped entry for observability", () => {
70
61
  const resolution = resolveResumeSession("claude", [
71
62
  {
72
63
  source: "task",
73
- sessionId: "ses_19c145de3ffeD9qLlntj8SRO28",
64
+ sessionId: "thread-codex",
65
+ provider: "codex",
74
66
  },
75
67
  ]);
76
68
 
77
- expect(resolution.resumeSessionId).toBeUndefined();
78
- expect(resolution.skipped[0]?.reason).toBe("legacy Claude resume requires a UUID session id");
69
+ expect(resolution.skipped).toHaveLength(1);
70
+ expect(resolution.skipped[0]?.provider).toBe("codex");
71
+ expect(resolution.skipped[0]?.reason).toBe(RESUME_DEPRECATED_REASON);
79
72
  });
80
73
 
81
- test("does not resume providers without runner resume support", () => {
82
- const resolution = resolveResumeSession("pi", [
83
- {
84
- source: "task",
85
- sessionId: "pi-session",
86
- provider: "pi",
87
- },
88
- ]);
74
+ test("currentProvider is ignored same skipped output regardless", () => {
75
+ const candidates = [
76
+ { source: "task" as const, sessionId: "abc-123", provider: "claude" as const },
77
+ ];
78
+ const a = resolveResumeSession("claude", candidates);
79
+ const b = resolveResumeSession("pi", candidates);
80
+ const c = resolveResumeSession("codex", candidates);
89
81
 
90
- expect(resolution.resumeSessionId).toBeUndefined();
91
- expect(resolution.skipped[0]?.reason).toBe("provider pi does not support runner resume");
82
+ expect(a.skipped).toEqual(b.skipped);
83
+ expect(b.skipped).toEqual(c.skipped);
92
84
  });
93
85
  });
@@ -253,6 +253,29 @@ describe("AgentTaskConfigSchema — outputSchema", () => {
253
253
  expect(parsed.data.outputSchema).toEqual(config.outputSchema);
254
254
  }
255
255
  });
256
+
257
+ test("accepts followUpConfig in config", async () => {
258
+ const { AgentTaskExecutor } = await import("../workflows/executors/agent-task");
259
+ const executor = new AgentTaskExecutor({
260
+ // biome-ignore lint/suspicious/noExplicitAny: mock DB for test
261
+ db: {} as any,
262
+ eventBus: { emit: () => {}, on: () => {}, off: () => {} },
263
+ interpolate: (t: string) => t,
264
+ });
265
+
266
+ const config = {
267
+ template: "Count files",
268
+ followUpConfig: {
269
+ onCompleted: "post the URL",
270
+ onFailed: "page Taras",
271
+ },
272
+ };
273
+ const parsed = executor.configSchema.safeParse(config);
274
+ expect(parsed.success).toBe(true);
275
+ if (parsed.success) {
276
+ expect(parsed.data.followUpConfig).toEqual(config.followUpConfig);
277
+ }
278
+ });
256
279
  });
257
280
 
258
281
  // ─── Workflow agent-task → DB with outputSchema ──────────────
@@ -305,4 +328,50 @@ describe("Workflow agent-task creates task with outputSchema", () => {
305
328
  expect(task).toBeDefined();
306
329
  expect(task!.outputSchema).toEqual(schema);
307
330
  });
331
+
332
+ test("followUpConfig flows from executor config to created task", async () => {
333
+ const { AgentTaskExecutor } = await import("../workflows/executors/agent-task");
334
+ const db = await import("../be/db");
335
+
336
+ const executor = new AgentTaskExecutor({
337
+ db: db as typeof import("../be/db"),
338
+ eventBus: { emit: () => {}, on: () => {}, off: () => {} },
339
+ interpolate: (t: string) => t,
340
+ });
341
+
342
+ const wf = createWorkflow({
343
+ name: "test-follow-up-config",
344
+ definition: { nodes: [], edges: [] },
345
+ });
346
+ const run = createWorkflowRun({ id: crypto.randomUUID(), workflowId: wf.id });
347
+ const step = createWorkflowRunStep({
348
+ id: crypto.randomUUID(),
349
+ runId: run.id,
350
+ nodeId: "n1",
351
+ nodeType: "agent-task",
352
+ });
353
+
354
+ const followUpConfig = { disabled: true };
355
+
356
+ const result = await executor.run({
357
+ config: {
358
+ template: "Summarize the document",
359
+ followUpConfig,
360
+ },
361
+ context: {},
362
+ meta: {
363
+ runId: run.id,
364
+ stepId: step.id,
365
+ nodeId: "n1",
366
+ workflowId: wf.id,
367
+ dryRun: false,
368
+ },
369
+ });
370
+
371
+ expect(result.status).toBe("success");
372
+ const taskId = (result as { correlationId?: string }).correlationId!;
373
+ const task = getTaskById(taskId);
374
+ expect(task).toBeDefined();
375
+ expect(task!.followUpConfig).toEqual(followUpConfig);
376
+ });
308
377
  });
@@ -8,6 +8,7 @@ import {
8
8
  createTaskExtended,
9
9
  failTask,
10
10
  getDb,
11
+ getLeadAgent,
11
12
  getLogsByTaskId,
12
13
  getTaskById,
13
14
  initDb,
@@ -362,7 +363,7 @@ describe("worker task follow-up creation", () => {
362
363
  slackThreadTs: "1700000000.000001",
363
364
  slackUserId: "U123",
364
365
  });
365
- startTask(task.id, worker.id);
366
+ startTask(task.id);
366
367
 
367
368
  const completed = completeTask(task.id, "Worker output");
368
369
  expect(completed).not.toBeNull();
@@ -382,6 +383,152 @@ describe("worker task follow-up creation", () => {
382
383
  expect(rows[0]!.slackThreadTs).toBe("1700000000.000001");
383
384
  expect(rows[0]!.slackUserId).toBe("U123");
384
385
  expect(rows[0]!.task).toContain("Worker output");
386
+ expect(rows[0]!.task).not.toContain("{{follow_up_instructions}}");
387
+ });
388
+
389
+ test("skips lead follow-up when followUpConfig disables it", () => {
390
+ createAgent({
391
+ name: "follow-up-lead-disabled",
392
+ isLead: true,
393
+ status: "idle",
394
+ capabilities: [],
395
+ });
396
+ const worker = createAgent({
397
+ name: "follow-up-worker-disabled",
398
+ isLead: false,
399
+ status: "idle",
400
+ capabilities: [],
401
+ });
402
+ const task = createTaskExtended("Silent worker task", {
403
+ agentId: worker.id,
404
+ followUpConfig: { disabled: true },
405
+ });
406
+ startTask(task.id);
407
+
408
+ const completed = completeTask(task.id, "Worker output");
409
+ expect(completed).not.toBeNull();
410
+
411
+ const followUp = createWorkerTaskFollowUp({
412
+ task: completed!,
413
+ status: "completed",
414
+ output: "Worker output",
415
+ });
416
+
417
+ expect(followUp).toBeNull();
418
+ expect(listFollowUpTasks(task.id)).toHaveLength(0);
419
+ });
420
+
421
+ test("injects onCompleted instructions into completed follow-up", () => {
422
+ createAgent({
423
+ name: "follow-up-lead-completed-instructions",
424
+ isLead: true,
425
+ status: "idle",
426
+ capabilities: [],
427
+ });
428
+ const worker = createAgent({
429
+ name: "follow-up-worker-completed-instructions",
430
+ isLead: false,
431
+ status: "idle",
432
+ capabilities: [],
433
+ });
434
+ const task = createTaskExtended("Worker task with completed instructions", {
435
+ agentId: worker.id,
436
+ creatorAgentId: worker.id,
437
+ followUpConfig: { onCompleted: "post the URL" },
438
+ });
439
+ startTask(task.id);
440
+
441
+ const completed = completeTask(task.id, "Worker output");
442
+ expect(completed).not.toBeNull();
443
+
444
+ const followUp = createWorkerTaskFollowUp({
445
+ task: completed!,
446
+ status: "completed",
447
+ output: "Worker output",
448
+ });
449
+
450
+ expect(followUp).not.toBeNull();
451
+ const rows = listFollowUpTasks(task.id);
452
+ expect(rows).toHaveLength(1);
453
+ expect(rows[0]!.task).toContain(`Original task created by agent ${worker.id}`);
454
+ expect(rows[0]!.task).toContain("Additional instructions from the task creator:");
455
+ expect(rows[0]!.task).toContain("post the URL");
456
+ });
457
+
458
+ test("injects only onFailed instructions into failed follow-up", () => {
459
+ createAgent({
460
+ name: "follow-up-lead-failed-instructions",
461
+ isLead: true,
462
+ status: "idle",
463
+ capabilities: [],
464
+ });
465
+ const worker = createAgent({
466
+ name: "follow-up-worker-failed-instructions",
467
+ isLead: false,
468
+ status: "idle",
469
+ capabilities: [],
470
+ });
471
+ const task = createTaskExtended("Worker task with failed instructions", {
472
+ agentId: worker.id,
473
+ creatorAgentId: worker.id,
474
+ followUpConfig: { onCompleted: "post the URL", onFailed: "page Taras" },
475
+ });
476
+ startTask(task.id);
477
+
478
+ const failed = failTask(task.id, "boom");
479
+ expect(failed).not.toBeNull();
480
+
481
+ const followUp = createWorkerTaskFollowUp({
482
+ task: failed!,
483
+ status: "failed",
484
+ failureReason: "boom",
485
+ });
486
+
487
+ expect(followUp).not.toBeNull();
488
+ const rows = listFollowUpTasks(task.id);
489
+ expect(rows).toHaveLength(1);
490
+ expect(rows[0]!.task).toContain(`Original task created by agent ${worker.id}`);
491
+ expect(rows[0]!.task).toContain("page Taras");
492
+ expect(rows[0]!.task).not.toContain("post the URL");
493
+ });
494
+
495
+ test("inherits followUpConfig from parent task when child has no override", () => {
496
+ createAgent({
497
+ name: "follow-up-lead-inheritance",
498
+ isLead: true,
499
+ status: "idle",
500
+ capabilities: [],
501
+ });
502
+ const worker = createAgent({
503
+ name: "follow-up-worker-inheritance",
504
+ isLead: false,
505
+ status: "idle",
506
+ capabilities: [],
507
+ });
508
+ const parent = createTaskExtended("Parent task", {
509
+ agentId: worker.id,
510
+ followUpConfig: { disabled: true },
511
+ });
512
+ const child = createTaskExtended("Child task", {
513
+ agentId: worker.id,
514
+ parentTaskId: parent.id,
515
+ });
516
+ startTask(child.id);
517
+
518
+ const fetchedChild = getTaskById(child.id);
519
+ expect(fetchedChild!.followUpConfig).toEqual({ disabled: true });
520
+
521
+ const completed = completeTask(child.id, "Child output");
522
+ expect(completed).not.toBeNull();
523
+
524
+ const followUp = createWorkerTaskFollowUp({
525
+ task: completed!,
526
+ status: "completed",
527
+ output: "Child output",
528
+ });
529
+
530
+ expect(followUp).toBeNull();
531
+ expect(listFollowUpTasks(child.id)).toHaveLength(0);
385
532
  });
386
533
 
387
534
  test("does not create follow-up for lead-owned task", () => {
@@ -392,7 +539,7 @@ describe("worker task follow-up creation", () => {
392
539
  capabilities: [],
393
540
  });
394
541
  const task = createTaskExtended("Lead task", { agentId: lead.id });
395
- startTask(task.id, lead.id);
542
+ startTask(task.id);
396
543
 
397
544
  const completed = completeTask(task.id, "Lead output");
398
545
  expect(completed).not.toBeNull();
@@ -406,4 +553,40 @@ describe("worker task follow-up creation", () => {
406
553
  expect(followUp).toBeNull();
407
554
  expect(listFollowUpTasks(task.id)).toHaveLength(0);
408
555
  });
556
+
557
+ test("marks original creator as you when lead created the worker task", () => {
558
+ const lead =
559
+ getLeadAgent() ??
560
+ createAgent({
561
+ name: "follow-up-lead-creator-you",
562
+ isLead: true,
563
+ status: "idle",
564
+ capabilities: [],
565
+ });
566
+ const worker = createAgent({
567
+ name: "follow-up-worker-creator-you",
568
+ isLead: false,
569
+ status: "idle",
570
+ capabilities: [],
571
+ });
572
+ const task = createTaskExtended("Worker task created by lead", {
573
+ agentId: worker.id,
574
+ creatorAgentId: lead.id,
575
+ });
576
+ startTask(task.id);
577
+
578
+ const completed = completeTask(task.id, "Worker output");
579
+ expect(completed).not.toBeNull();
580
+
581
+ const followUp = createWorkerTaskFollowUp({
582
+ task: completed!,
583
+ status: "completed",
584
+ output: "Worker output",
585
+ });
586
+
587
+ expect(followUp).not.toBeNull();
588
+ const rows = listFollowUpTasks(task.id);
589
+ expect(rows).toHaveLength(1);
590
+ expect(rows[0]!.task).toContain(`Original task created by agent ${lead.id} (you)`);
591
+ });
409
592
  });