@xrkseek/harness 0.5.14 → 0.5.19

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 (109) hide show
  1. package/node_modules/@xrkseek/core-agent-loop/dist/index.d.ts +1 -1
  2. package/node_modules/@xrkseek/core-agent-loop/dist/index.d.ts.map +1 -1
  3. package/node_modules/@xrkseek/core-agent-loop/dist/index.js +1 -1
  4. package/node_modules/@xrkseek/core-agent-loop/dist/index.js.map +1 -1
  5. package/node_modules/@xrkseek/core-agent-loop/dist/request-header-log.d.ts +10 -0
  6. package/node_modules/@xrkseek/core-agent-loop/dist/request-header-log.d.ts.map +1 -1
  7. package/node_modules/@xrkseek/core-agent-loop/dist/request-header-log.js +56 -7
  8. package/node_modules/@xrkseek/core-agent-loop/dist/request-header-log.js.map +1 -1
  9. package/node_modules/@xrkseek/core-session/dist/index.d.ts +1 -1
  10. package/node_modules/@xrkseek/core-session/dist/index.d.ts.map +1 -1
  11. package/node_modules/@xrkseek/core-session/dist/index.js +1 -1
  12. package/node_modules/@xrkseek/core-session/dist/index.js.map +1 -1
  13. package/node_modules/@xrkseek/core-session/dist/request-header.d.ts +8 -1
  14. package/node_modules/@xrkseek/core-session/dist/request-header.d.ts.map +1 -1
  15. package/node_modules/@xrkseek/core-session/dist/request-header.js +18 -9
  16. package/node_modules/@xrkseek/core-session/dist/request-header.js.map +1 -1
  17. package/node_modules/@xrkseek/core-session/dist/sqlite-store.d.ts.map +1 -1
  18. package/node_modules/@xrkseek/core-session/dist/sqlite-store.js +33 -0
  19. package/node_modules/@xrkseek/core-session/dist/sqlite-store.js.map +1 -1
  20. package/node_modules/@xrkseek/preset-harness/dist/preset.d.ts +9 -0
  21. package/node_modules/@xrkseek/preset-harness/dist/preset.d.ts.map +1 -1
  22. package/node_modules/@xrkseek/preset-harness/dist/preset.js +3 -1
  23. package/node_modules/@xrkseek/preset-harness/dist/preset.js.map +1 -1
  24. package/node_modules/@xrkseek/preset-harness/dist/tsconfig.tsbuildinfo +1 -1
  25. package/node_modules/@xrkseek/preset-minimal/dist/tsconfig.tsbuildinfo +1 -1
  26. package/node_modules/@xrkseek/preset-server/dist/preset.d.ts.map +1 -1
  27. package/node_modules/@xrkseek/preset-server/dist/preset.js +2 -1
  28. package/node_modules/@xrkseek/preset-server/dist/preset.js.map +1 -1
  29. package/node_modules/@xrkseek/preset-server/dist/tsconfig.tsbuildinfo +1 -1
  30. package/node_modules/@xrkseek/protocol/dist/index.d.ts +1 -1
  31. package/node_modules/@xrkseek/protocol/dist/index.d.ts.map +1 -1
  32. package/node_modules/@xrkseek/protocol/dist/index.js.map +1 -1
  33. package/node_modules/@xrkseek/protocol/dist/sidebar-previews.d.ts +16 -0
  34. package/node_modules/@xrkseek/protocol/dist/sidebar-previews.d.ts.map +1 -1
  35. package/node_modules/@xrkseek/protocol/dist/sidebar-previews.js.map +1 -1
  36. package/node_modules/@xrkseek/server-cron/dist/index.d.ts +1 -1
  37. package/node_modules/@xrkseek/server-cron/dist/index.d.ts.map +1 -1
  38. package/node_modules/@xrkseek/server-cron/dist/index.js.map +1 -1
  39. package/node_modules/@xrkseek/server-cron/dist/tools.d.ts +13 -1
  40. package/node_modules/@xrkseek/server-cron/dist/tools.d.ts.map +1 -1
  41. package/node_modules/@xrkseek/server-cron/dist/tools.js +35 -4
  42. package/node_modules/@xrkseek/server-cron/dist/tools.js.map +1 -1
  43. package/node_modules/@xrkseek/server-cron/dist/types.d.ts +5 -0
  44. package/node_modules/@xrkseek/server-cron/dist/types.d.ts.map +1 -1
  45. package/node_modules/@xrkseek/server-cron/dist/types.js.map +1 -1
  46. package/node_modules/@xrkseek/server-face/dist/adapt/history-paginate.d.ts +25 -0
  47. package/node_modules/@xrkseek/server-face/dist/adapt/history-paginate.d.ts.map +1 -1
  48. package/node_modules/@xrkseek/server-face/dist/adapt/history-paginate.js +132 -0
  49. package/node_modules/@xrkseek/server-face/dist/adapt/history-paginate.js.map +1 -1
  50. package/node_modules/@xrkseek/server-face/dist/adapt/index.d.ts +1 -1
  51. package/node_modules/@xrkseek/server-face/dist/adapt/index.d.ts.map +1 -1
  52. package/node_modules/@xrkseek/server-face/dist/adapt/index.js +1 -1
  53. package/node_modules/@xrkseek/server-face/dist/adapt/index.js.map +1 -1
  54. package/node_modules/@xrkseek/server-face/dist/adapt/tool-view.js +3 -4
  55. package/node_modules/@xrkseek/server-face/dist/adapt/tool-view.js.map +1 -1
  56. package/node_modules/@xrkseek/server-face/dist/adapt/wire-ids.d.ts +5 -0
  57. package/node_modules/@xrkseek/server-face/dist/adapt/wire-ids.d.ts.map +1 -1
  58. package/node_modules/@xrkseek/server-face/dist/adapt/wire-ids.js +20 -2
  59. package/node_modules/@xrkseek/server-face/dist/adapt/wire-ids.js.map +1 -1
  60. package/node_modules/@xrkseek/server-face/dist/agent-roster-store.d.ts +1 -1
  61. package/node_modules/@xrkseek/server-face/dist/agent-roster-store.d.ts.map +1 -1
  62. package/node_modules/@xrkseek/server-face/dist/agent-roster-store.js +6 -9
  63. package/node_modules/@xrkseek/server-face/dist/agent-roster-store.js.map +1 -1
  64. package/node_modules/@xrkseek/server-face/dist/handlers/session.d.ts.map +1 -1
  65. package/node_modules/@xrkseek/server-face/dist/handlers/session.js +27 -9
  66. package/node_modules/@xrkseek/server-face/dist/handlers/session.js.map +1 -1
  67. package/node_modules/@xrkseek/server-face/dist/index.d.ts +2 -1
  68. package/node_modules/@xrkseek/server-face/dist/index.d.ts.map +1 -1
  69. package/node_modules/@xrkseek/server-face/dist/index.js +2 -1
  70. package/node_modules/@xrkseek/server-face/dist/index.js.map +1 -1
  71. package/node_modules/@xrkseek/server-face/dist/presence-dressing.d.ts +7 -0
  72. package/node_modules/@xrkseek/server-face/dist/presence-dressing.d.ts.map +1 -1
  73. package/node_modules/@xrkseek/server-face/dist/presence-dressing.js +46 -7
  74. package/node_modules/@xrkseek/server-face/dist/presence-dressing.js.map +1 -1
  75. package/node_modules/@xrkseek/server-face/dist/projections/units/context-headers.d.ts.map +1 -1
  76. package/node_modules/@xrkseek/server-face/dist/projections/units/context-headers.js +6 -1
  77. package/node_modules/@xrkseek/server-face/dist/projections/units/context-headers.js.map +1 -1
  78. package/node_modules/@xrkseek/server-face/dist/runtime.d.ts.map +1 -1
  79. package/node_modules/@xrkseek/server-face/dist/runtime.js +2 -0
  80. package/node_modules/@xrkseek/server-face/dist/runtime.js.map +1 -1
  81. package/node_modules/@xrkseek/server-face/dist/session-status.d.ts +16 -0
  82. package/node_modules/@xrkseek/server-face/dist/session-status.d.ts.map +1 -1
  83. package/node_modules/@xrkseek/server-face/dist/session-status.js +39 -4
  84. package/node_modules/@xrkseek/server-face/dist/session-status.js.map +1 -1
  85. package/node_modules/@xrkseek/server-face/dist/settings-document.js +2 -2
  86. package/node_modules/@xrkseek/server-face/dist/settings-document.js.map +1 -1
  87. package/node_modules/@xrkseek/server-face/dist/status-snapshot-cache.d.ts +23 -0
  88. package/node_modules/@xrkseek/server-face/dist/status-snapshot-cache.d.ts.map +1 -0
  89. package/node_modules/@xrkseek/server-face/dist/status-snapshot-cache.js +95 -0
  90. package/node_modules/@xrkseek/server-face/dist/status-snapshot-cache.js.map +1 -0
  91. package/node_modules/@xrkseek/server-face/dist/subagent-tools.d.ts.map +1 -1
  92. package/node_modules/@xrkseek/server-face/dist/subagent-tools.js +427 -370
  93. package/node_modules/@xrkseek/server-face/dist/subagent-tools.js.map +1 -1
  94. package/node_modules/@xrkseek/server-host/dist/cron-http.d.ts +6 -8
  95. package/node_modules/@xrkseek/server-host/dist/cron-http.d.ts.map +1 -1
  96. package/node_modules/@xrkseek/server-host/dist/cron-http.js +104 -28
  97. package/node_modules/@xrkseek/server-host/dist/cron-http.js.map +1 -1
  98. package/node_modules/@xrkseek/server-host/dist/index.d.ts +9 -0
  99. package/node_modules/@xrkseek/server-host/dist/index.d.ts.map +1 -1
  100. package/node_modules/@xrkseek/server-host/dist/index.js +61 -3
  101. package/node_modules/@xrkseek/server-host/dist/index.js.map +1 -1
  102. package/node_modules/@xrkseek/server-host/dist/sidebar-face-bridge.d.ts.map +1 -1
  103. package/node_modules/@xrkseek/server-host/dist/sidebar-face-bridge.js +33 -8
  104. package/node_modules/@xrkseek/server-host/dist/sidebar-face-bridge.js.map +1 -1
  105. package/node_modules/@xrkseek/server-http/dist/dsh-compat/honest-http-catchall.js +1 -1
  106. package/node_modules/@xrkseek/server-http/dist/dsh-compat/honest-http-catchall.js.map +1 -1
  107. package/node_modules/@xrkseek/server-http/dist/sidebar/sidebar-face-bridge.d.ts +5 -1
  108. package/node_modules/@xrkseek/server-http/dist/sidebar/sidebar-face-bridge.d.ts.map +1 -1
  109. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { SUBAGENT_ROUTING_PROMPT_TEXT } from "@xrkseek/core-tools";
2
2
  import { listPendingAdmits, readSessionEvents, } from "@xrkseek/core-session";
3
+ import { forgetRequestHeaderCache } from "@xrkseek/core-agent-loop";
3
4
  import { dispatchFaceMethod } from "./dispatch.js";
4
5
  import { formatChildOutcome, lastAssistantBodyText, lastModelRetryNotice, } from "./adapt/subagent-notice.js";
5
6
  import { boundChildAnswer, SUBAGENT_ANSWER_INLINE_BYTES, } from "./adapt/subagent-answer-bound.js";
@@ -42,7 +43,7 @@ export function resolveSubagentQuota(runtime, sessionId) {
42
43
  const profile = resolveAgentPresetProfile(presetId);
43
44
  if (profile.subagents.mode === "off") {
44
45
  const depth = subagentDepth(runtime, sessionId);
45
- const active = countActiveChildren(runtime, sessionId);
46
+ const active = countActiveChildren(runtime, sessionId) + pendingSpawnCount(sessionId);
46
47
  const delegated = runtime.subagents.listDelegated(sessionId).length;
47
48
  return {
48
49
  depth,
@@ -60,7 +61,7 @@ export function resolveSubagentQuota(runtime, sessionId) {
60
61
  ? Math.min(faceActive, profile.subagents.maxActiveChildren)
61
62
  : faceActive;
62
63
  const depth = subagentDepth(runtime, sessionId);
63
- const active = countActiveChildren(runtime, sessionId);
64
+ const active = countActiveChildren(runtime, sessionId) + pendingSpawnCount(sessionId);
64
65
  const delegated = runtime.subagents.listDelegated(sessionId).length;
65
66
  return {
66
67
  depth,
@@ -243,6 +244,25 @@ function countActiveChildren(runtime, parentSessionId) {
243
244
  }
244
245
  return n;
245
246
  }
247
+ /**
248
+ * In-flight spawn reservations (sync). Two parallel `subagent` settles both
249
+ * read `countActiveChildren` before either child's drain latches — without
250
+ * this, both can pass `active < maxActive` and overshoot the cap.
251
+ */
252
+ const pendingSpawnSlots = new Map();
253
+ function pendingSpawnCount(parentSessionId) {
254
+ return pendingSpawnSlots.get(parentSessionId) ?? 0;
255
+ }
256
+ function reserveSpawnSlot(parentSessionId) {
257
+ pendingSpawnSlots.set(parentSessionId, pendingSpawnCount(parentSessionId) + 1);
258
+ }
259
+ function releaseSpawnSlot(parentSessionId) {
260
+ const n = pendingSpawnCount(parentSessionId);
261
+ if (n <= 1)
262
+ pendingSpawnSlots.delete(parentSessionId);
263
+ else
264
+ pendingSpawnSlots.set(parentSessionId, n - 1);
265
+ }
246
266
  /**
247
267
  * Undo a half-made spawn. `session.create` / `session.fork` already made the
248
268
  * child durable and registered its link, so any failure between there and a
@@ -266,6 +286,7 @@ async function discardUnstartedChild(runtime, childSessionId) {
266
286
  catch {
267
287
  /* store without delete / already gone */
268
288
  }
289
+ forgetRequestHeaderCache(childSessionId);
269
290
  await Promise.resolve(runtime.invalidateAgent?.(childSessionId)).catch(() => undefined);
270
291
  }
271
292
  /**
@@ -455,6 +476,10 @@ function createSubagentTool(options) {
455
476
  },
456
477
  required: ["prompt"],
457
478
  },
479
+ // Without this, settle-batch treats every `subagent` call as an exclusive
480
+ // barrier — two tool calls in one turn run one-after-another, so Status
481
+ // shows "1 subagent" even when the model asked for parallel siblings.
482
+ isConcurrencySafe: () => true,
458
483
  presentCall: (args) => ({
459
484
  card: "generic",
460
485
  title: String(args.description?.trim() || "Subagent"),
@@ -558,6 +583,14 @@ function createSubagentTool(options) {
558
583
  isError: true,
559
584
  };
560
585
  }
586
+ reserveSpawnSlot(options.parentSessionId);
587
+ let externalSlotHeld = true;
588
+ const dropExternalSlot = () => {
589
+ if (!externalSlotHeld)
590
+ return;
591
+ externalSlotHeld = false;
592
+ releaseSpawnSlot(options.parentSessionId);
593
+ };
561
594
  try {
562
595
  const product = parseExternalAgentProduct(options.runtime.settingsNamespaces.view("external-agent").value);
563
596
  if (background && supportsExternalContinuable(runtimeKind)) {
@@ -632,6 +665,8 @@ function createSubagentTool(options) {
632
665
  ? { spawnImpl: options.externalSpawn }
633
666
  : {}),
634
667
  });
668
+ // Child is busy in externalAgents; pending slot no longer needed.
669
+ dropExternalSlot();
635
670
  return {
636
671
  content: [
637
672
  `Started background external subagent \`${childId}\` (${label} · ${runtimeKind}).`,
@@ -644,6 +679,9 @@ function createSubagentTool(options) {
644
679
  ].join("\n"),
645
680
  };
646
681
  }
682
+ // One-shot external has no Face child latch — do not hold the slot
683
+ // for the whole print turn (would starve parallel in-process spawns).
684
+ dropExternalSlot();
647
685
  const result = await runExternalAgentTurn({
648
686
  kind: runtimeKind,
649
687
  cwd: externalCwd,
@@ -667,6 +705,9 @@ function createSubagentTool(options) {
667
705
  : String(err);
668
706
  return { content: `subagent external: ${msg}`, isError: true };
669
707
  }
708
+ finally {
709
+ dropExternalSlot();
710
+ }
670
711
  }
671
712
  const depth = subagentDepth(options.runtime, options.parentSessionId);
672
713
  const { maxDepth, maxActive: maxActiveChildren, active } = caps();
@@ -682,426 +723,442 @@ function createSubagentTool(options) {
682
723
  isError: true,
683
724
  };
684
725
  }
685
- const background = a.run_in_background === true;
686
- const inherit = a.inherit_context === true;
687
- const label = String(a.description ?? "").trim() ||
688
- String(a.task_name ?? "").trim() ||
689
- (background ? "subagent" : "subagent-task");
690
- const linkMode = background ? "continuable" : "one-shot";
691
- const parentCwd = resolveSessionCwd(options.runtime, options.parentSessionId);
692
- const taskTitle = String(a.task_name ?? "").trim() || label;
693
- const taskIdHint = String(a.task_id ?? "").trim() || undefined;
694
- // Before anything is allocated: reusing a task_id another child owns
695
- // must not leave a worktree or a session behind.
696
- const taskIdError = taskIdConflict(options.runtime, taskIdHint);
697
- if (taskIdError) {
698
- return { content: taskIdError, isError: true };
699
- }
700
- let isolated = null;
701
- let managedLease = null;
702
- let worktreeSkip = "";
703
- if (a.worktree === true) {
704
- if (worktreeSkippedForRemote(options.runtime.remoteExecution)) {
705
- worktreeSkip = "worktree skipped: not a local terminal";
726
+ reserveSpawnSlot(options.parentSessionId);
727
+ let spawnSlotHeld = true;
728
+ const dropSpawnSlot = () => {
729
+ if (!spawnSlotHeld)
730
+ return;
731
+ spawnSlotHeld = false;
732
+ releaseSpawnSlot(options.parentSessionId);
733
+ };
734
+ try {
735
+ const background = a.run_in_background === true;
736
+ const inherit = a.inherit_context === true;
737
+ const label = String(a.description ?? "").trim() ||
738
+ String(a.task_name ?? "").trim() ||
739
+ (background ? "subagent" : "subagent-task");
740
+ const linkMode = background ? "continuable" : "one-shot";
741
+ const parentCwd = resolveSessionCwd(options.runtime, options.parentSessionId);
742
+ const taskTitle = String(a.task_name ?? "").trim() || label;
743
+ const taskIdHint = String(a.task_id ?? "").trim() || undefined;
744
+ // Before anything is allocated: reusing a task_id another child owns
745
+ // must not leave a worktree or a session behind.
746
+ const taskIdError = taskIdConflict(options.runtime, taskIdHint);
747
+ if (taskIdError) {
748
+ return { content: taskIdError, isError: true };
706
749
  }
707
- else {
708
- managedLease =
709
- options.runtime.managedWorktrees.allocate({
710
- parentCwd,
711
- parentSessionId: options.parentSessionId,
712
- }) ?? null;
713
- if (managedLease) {
714
- isolated = {
715
- path: managedLease.path,
716
- branch: managedLease.branch,
717
- repoRoot: managedLease.repoRoot,
718
- baseCommit: managedLease.baseCommit,
719
- };
750
+ let isolated = null;
751
+ let managedLease = null;
752
+ let worktreeSkip = "";
753
+ if (a.worktree === true) {
754
+ if (worktreeSkippedForRemote(options.runtime.remoteExecution)) {
755
+ worktreeSkip = "worktree skipped: not a local terminal";
720
756
  }
721
757
  else {
722
- // Fall back to one-shot create when the registry path fails.
723
- isolated = createSubagentWorktree(parentCwd);
724
- if (!isolated) {
725
- worktreeSkip =
726
- "worktree skipped: not a git repository or worktree add failed";
758
+ managedLease =
759
+ options.runtime.managedWorktrees.allocate({
760
+ parentCwd,
761
+ parentSessionId: options.parentSessionId,
762
+ }) ?? null;
763
+ if (managedLease) {
764
+ isolated = {
765
+ path: managedLease.path,
766
+ branch: managedLease.branch,
767
+ repoRoot: managedLease.repoRoot,
768
+ baseCommit: managedLease.baseCommit,
769
+ };
770
+ }
771
+ else {
772
+ // Fall back to one-shot create when the registry path fails.
773
+ isolated = createSubagentWorktree(parentCwd);
774
+ if (!isolated) {
775
+ worktreeSkip =
776
+ "worktree skipped: not a git repository or worktree add failed";
777
+ }
727
778
  }
728
779
  }
729
780
  }
730
- }
731
- let taskBody = prompt;
732
- if (outputSchema) {
733
- taskBody = appendOutputContract(taskBody, outputSchema);
734
- }
735
- taskBody = isolated
736
- ? `${taskBody}\n\n${worktreeContextNote(isolated)}`
737
- : taskBody;
738
- const openTask = (childSessionId) => {
739
- const task = options.runtime.agentTeamTasks.open({
740
- parentSessionId: options.parentSessionId,
741
- title: taskTitle,
742
- childSessionId,
743
- ...(spawnRole && spawnRole !== "default" ? { role: spawnRole } : {}),
744
- ...(taskIdHint ? { taskId: taskIdHint } : {}),
745
- });
781
+ let taskBody = prompt;
746
782
  if (outputSchema) {
747
- options.runtime.agentTeamTasks.setOutputSchema(task.id, outputSchema);
783
+ taskBody = appendOutputContract(taskBody, outputSchema);
748
784
  }
749
- if (managedLease) {
750
- options.runtime.managedWorktrees.bind(managedLease.id, {
785
+ taskBody = isolated
786
+ ? `${taskBody}\n\n${worktreeContextNote(isolated)}`
787
+ : taskBody;
788
+ const openTask = (childSessionId) => {
789
+ const task = options.runtime.agentTeamTasks.open({
790
+ parentSessionId: options.parentSessionId,
791
+ title: taskTitle,
751
792
  childSessionId,
752
- teamTaskId: task.id,
753
- });
754
- options.runtime.agentTeamTasks.bindWorktree(task.id, {
755
- path: managedLease.path,
756
- branch: managedLease.branch,
757
- id: managedLease.id,
758
- });
759
- }
760
- else if (isolated) {
761
- options.runtime.agentTeamTasks.bindWorktree(task.id, {
762
- path: isolated.path,
763
- branch: isolated.branch,
764
- });
765
- }
766
- return task;
767
- };
768
- const reclaimIsolated = (keep) => {
769
- if (worktreeSkip)
770
- return worktreeSkip;
771
- if (!isolated)
772
- return "";
773
- if (keep) {
774
- return `[worktree] ${isolated.path}\nbranch ${isolated.branch}\nleft in place (child still running)`;
775
- }
776
- if (managedLease) {
777
- return formatWorktreeResult(options.runtime.managedWorktrees.reclaim(managedLease.id) ??
778
- finalizeSubagentWorktree(isolated));
779
- }
780
- return formatWorktreeResult(finalizeSubagentWorktree(isolated));
781
- };
782
- const dropIsolated = () => {
783
- if (!isolated)
784
- return;
785
- if (managedLease) {
786
- options.runtime.managedWorktrees.reclaim(managedLease.id);
787
- }
788
- else {
789
- finalizeSubagentWorktree(isolated);
790
- }
791
- };
792
- /**
793
- * Codex `spawn_guard.rs` (RAII): a half-made spawn is torn down on
794
- * EVERY exit path, including an unexpected throw. Explicit
795
- * `discardUnstartedChild` calls cover the known failure branches; this
796
- * flag + finally is the backstop for anything they miss.
797
- */
798
- let started = false;
799
- const discardIfUnstarted = async () => {
800
- if (started || !childId)
801
- return;
802
- started = true;
803
- dropIsolated();
804
- await discardUnstartedChild(options.runtime, childId);
805
- };
806
- let childId;
807
- /** Fork seed size — bounds the root-evidence slice below. */
808
- let seedCut = 0;
809
- if (inherit) {
810
- // A transport reject (not an RpcResult error) here would strand the
811
- // worktree lease allocated above; catch keeps the cleanup total.
812
- let forked;
813
- try {
814
- forked = await dispatchFaceMethod(options.runtime, "session.fork", `tool-sa-fork-${Date.now()}`, {
815
- sessionId: options.parentSessionId,
816
- linkMode,
817
- label,
818
- ...(spawnRole && spawnRole !== "default"
819
- ? { role: spawnRole }
820
- : {}),
793
+ ...(spawnRole && spawnRole !== "default" ? { role: spawnRole } : {}),
794
+ ...(taskIdHint ? { taskId: taskIdHint } : {}),
821
795
  });
822
- }
823
- catch {
796
+ if (outputSchema) {
797
+ options.runtime.agentTeamTasks.setOutputSchema(task.id, outputSchema);
798
+ }
799
+ if (managedLease) {
800
+ options.runtime.managedWorktrees.bind(managedLease.id, {
801
+ childSessionId,
802
+ teamTaskId: task.id,
803
+ });
804
+ options.runtime.agentTeamTasks.bindWorktree(task.id, {
805
+ path: managedLease.path,
806
+ branch: managedLease.branch,
807
+ id: managedLease.id,
808
+ });
809
+ }
810
+ else if (isolated) {
811
+ options.runtime.agentTeamTasks.bindWorktree(task.id, {
812
+ path: isolated.path,
813
+ branch: isolated.branch,
814
+ });
815
+ }
816
+ return task;
817
+ };
818
+ const reclaimIsolated = (keep) => {
819
+ if (worktreeSkip)
820
+ return worktreeSkip;
821
+ if (!isolated)
822
+ return "";
823
+ if (keep) {
824
+ return `[worktree] ${isolated.path}\nbranch ${isolated.branch}\nleft in place (child still running)`;
825
+ }
826
+ if (managedLease) {
827
+ return formatWorktreeResult(options.runtime.managedWorktrees.reclaim(managedLease.id) ??
828
+ finalizeSubagentWorktree(isolated));
829
+ }
830
+ return formatWorktreeResult(finalizeSubagentWorktree(isolated));
831
+ };
832
+ const dropIsolated = () => {
833
+ if (!isolated)
834
+ return;
835
+ if (managedLease) {
836
+ options.runtime.managedWorktrees.reclaim(managedLease.id);
837
+ }
838
+ else {
839
+ finalizeSubagentWorktree(isolated);
840
+ }
841
+ };
842
+ /**
843
+ * Codex `spawn_guard.rs` (RAII): a half-made spawn is torn down on
844
+ * EVERY exit path, including an unexpected throw. Explicit
845
+ * `discardUnstartedChild` calls cover the known failure branches; this
846
+ * flag + finally is the backstop for anything they miss.
847
+ */
848
+ let started = false;
849
+ const discardIfUnstarted = async () => {
850
+ if (started || !childId)
851
+ return;
852
+ started = true;
824
853
  dropIsolated();
825
- return {
826
- content: "subagent seed failed: fork request errored",
827
- isError: true,
828
- };
829
- }
830
- if (!forked.result.ok) {
831
- // No completed turn yet — fall back to a fresh child (DSH fork omits seed).
832
- if (forked.result.error.code === "fork-unavailable") {
833
- const created = await dispatchFaceMethod(options.runtime, "session.create", `tool-sa-${Date.now()}`, {
834
- parentSessionId: options.parentSessionId,
854
+ await discardUnstartedChild(options.runtime, childId);
855
+ };
856
+ let childId;
857
+ /** Fork seed size — bounds the root-evidence slice below. */
858
+ let seedCut = 0;
859
+ if (inherit) {
860
+ // A transport reject (not an RpcResult error) here would strand the
861
+ // worktree lease allocated above; catch keeps the cleanup total.
862
+ let forked;
863
+ try {
864
+ forked = await dispatchFaceMethod(options.runtime, "session.fork", `tool-sa-fork-${Date.now()}`, {
865
+ sessionId: options.parentSessionId,
866
+ linkMode,
835
867
  label,
836
- mode: linkMode,
837
- ...memberSpawn,
838
868
  ...(spawnRole && spawnRole !== "default"
839
869
  ? { role: spawnRole }
840
870
  : {}),
841
871
  });
842
- if (!created.result.ok) {
872
+ }
873
+ catch {
874
+ dropIsolated();
875
+ return {
876
+ content: "subagent seed failed: fork request errored",
877
+ isError: true,
878
+ };
879
+ }
880
+ if (!forked.result.ok) {
881
+ // No completed turn yet — fall back to a fresh child (DSH fork omits seed).
882
+ if (forked.result.error.code === "fork-unavailable") {
883
+ const created = await dispatchFaceMethod(options.runtime, "session.create", `tool-sa-${Date.now()}`, {
884
+ parentSessionId: options.parentSessionId,
885
+ label,
886
+ mode: linkMode,
887
+ ...memberSpawn,
888
+ ...(spawnRole && spawnRole !== "default"
889
+ ? { role: spawnRole }
890
+ : {}),
891
+ });
892
+ if (!created.result.ok) {
893
+ dropIsolated();
894
+ return {
895
+ content: `subagent create failed: ${created.result.error.message}`,
896
+ isError: true,
897
+ };
898
+ }
899
+ childId = String(created.result.value.sessionId);
900
+ }
901
+ else {
843
902
  dropIsolated();
844
903
  return {
845
- content: `subagent create failed: ${created.result.error.message}`,
904
+ content: `subagent seed failed: ${forked.result.error.message}`,
846
905
  isError: true,
847
906
  };
848
907
  }
849
- childId = String(created.result.value.sessionId);
850
908
  }
851
909
  else {
910
+ childId = String(forked.result.value.sessionId);
911
+ // Durable seed boundary: every answer read of this child must
912
+ // slice from here, or the parent's last reply reads as its answer.
913
+ seedCut = Number(forked.result.value.eventCount ?? 0);
914
+ options.runtime.subagents.setSeedEventCount(childId, seedCut);
915
+ }
916
+ }
917
+ else {
918
+ const created = await dispatchFaceMethod(options.runtime, "session.create", `tool-sa-${Date.now()}`, {
919
+ parentSessionId: options.parentSessionId,
920
+ label,
921
+ mode: linkMode,
922
+ ...memberSpawn,
923
+ ...(spawnRole && spawnRole !== "default"
924
+ ? { role: spawnRole }
925
+ : {}),
926
+ });
927
+ if (!created.result.ok) {
852
928
  dropIsolated();
853
929
  return {
854
- content: `subagent seed failed: ${forked.result.error.message}`,
930
+ content: `subagent create failed: ${created.result.error.message}`,
855
931
  isError: true,
856
932
  };
857
933
  }
934
+ childId = String(created.result.value.sessionId);
858
935
  }
859
- else {
860
- childId = String(forked.result.value.sessionId);
861
- // Durable seed boundary: every answer read of this child must
862
- // slice from here, or the parent's last reply reads as its answer.
863
- seedCut = Number(forked.result.value.eventCount ?? 0);
864
- options.runtime.subagents.setSeedEventCount(childId, seedCut);
936
+ const modelPin = await applySubagentModelOverride(options.runtime, {
937
+ parentSessionId: options.parentSessionId,
938
+ childSessionId: childId,
939
+ ...(typeof a.provider === "string" ? { provider: a.provider } : {}),
940
+ ...(typeof a.model === "string" ? { model: a.model } : {}),
941
+ ...(typeof a.reasoning_effort === "string"
942
+ ? { reasoningEffort: a.reasoning_effort }
943
+ : {}),
944
+ });
945
+ if (!modelPin.ok) {
946
+ await discardIfUnstarted();
947
+ return { content: modelPin.message, isError: true };
865
948
  }
866
- }
867
- else {
868
- const created = await dispatchFaceMethod(options.runtime, "session.create", `tool-sa-${Date.now()}`, {
949
+ if (isolated) {
950
+ options.runtime.sessionCwds.set(childId, isolated.path);
951
+ // Worktree bind failure must not leak the child: swallow and let the
952
+ // prompt below decide the spawn's fate (or the catch backstop).
953
+ await Promise.resolve(options.runtime.invalidateAgent?.(childId)).catch(() => undefined);
954
+ }
955
+ const childPrompt = applySubagentSpawnPreamble({
956
+ prompt: taskBody,
869
957
  parentSessionId: options.parentSessionId,
870
- label,
958
+ childSessionId: childId,
871
959
  mode: linkMode,
872
- ...memberSpawn,
873
- ...(spawnRole && spawnRole !== "default"
874
- ? { role: spawnRole }
960
+ label,
961
+ ...(spawnRole ? { role: spawnRole } : {}),
962
+ inheritContext: inherit,
963
+ cwd: isolated?.path ?? parentCwd,
964
+ isolatedWorktree: isolated !== null,
965
+ ...(rosterMember
966
+ ? { memberId: rosterMember.id, inject: rosterMember.inject }
875
967
  : {}),
968
+ // The human's own asks: a child that only sees the parent's paraphrase
969
+ // drifts from what was actually authorized (Codex
970
+ // `control/user_authorization.rs`). A forked child already carries
971
+ // these in its seeded transcript, so only add them beyond the cut.
972
+ ...(() => {
973
+ const block = rootUserAuthorizationBlock({
974
+ parentEvents: readSessionEvents(options.runtime.store, options.parentSessionId),
975
+ ...(seedCut > 0 ? { sinceEventCount: seedCut } : {}),
976
+ });
977
+ return block ? { userAuthorization: block } : {};
978
+ })(),
876
979
  });
877
- if (!created.result.ok) {
878
- dropIsolated();
980
+ const task = openTask(childId);
981
+ const worktreeLine = reclaimIsolated;
982
+ // A transport-level reject (not an RpcResult error) must not leak the
983
+ // child: the catch backstop tears the half-made spawn down.
984
+ let prompted;
985
+ try {
986
+ prompted = await dispatchFaceMethod(options.runtime, "session.prompt", `tool-sa-p-${childId}`, {
987
+ sessionId: childId,
988
+ mode: "queue",
989
+ content: [{ type: "text", text: childPrompt }],
990
+ });
991
+ }
992
+ catch (err) {
993
+ await discardIfUnstarted();
994
+ const extra = worktreeLine(false);
995
+ const message = err instanceof Error ? err.message : String(err);
879
996
  return {
880
- content: `subagent create failed: ${created.result.error.message}`,
997
+ content: [`subagent prompt failed: ${message}`, extra]
998
+ .filter(Boolean)
999
+ .join("\n"),
881
1000
  isError: true,
882
1001
  };
883
1002
  }
884
- childId = String(created.result.value.sessionId);
885
- }
886
- const modelPin = await applySubagentModelOverride(options.runtime, {
887
- parentSessionId: options.parentSessionId,
888
- childSessionId: childId,
889
- ...(typeof a.provider === "string" ? { provider: a.provider } : {}),
890
- ...(typeof a.model === "string" ? { model: a.model } : {}),
891
- ...(typeof a.reasoning_effort === "string"
892
- ? { reasoningEffort: a.reasoning_effort }
893
- : {}),
894
- });
895
- if (!modelPin.ok) {
896
- await discardIfUnstarted();
897
- return { content: modelPin.message, isError: true };
898
- }
899
- if (isolated) {
900
- options.runtime.sessionCwds.set(childId, isolated.path);
901
- // Worktree bind failure must not leak the child: swallow and let the
902
- // prompt below decide the spawn's fate (or the catch backstop).
903
- await Promise.resolve(options.runtime.invalidateAgent?.(childId)).catch(() => undefined);
904
- }
905
- const childPrompt = applySubagentSpawnPreamble({
906
- prompt: taskBody,
907
- parentSessionId: options.parentSessionId,
908
- childSessionId: childId,
909
- mode: linkMode,
910
- label,
911
- ...(spawnRole ? { role: spawnRole } : {}),
912
- inheritContext: inherit,
913
- cwd: isolated?.path ?? parentCwd,
914
- isolatedWorktree: isolated !== null,
915
- ...(rosterMember
916
- ? { memberId: rosterMember.id, inject: rosterMember.inject }
917
- : {}),
918
- // The human's own asks: a child that only sees the parent's paraphrase
919
- // drifts from what was actually authorized (Codex
920
- // `control/user_authorization.rs`). A forked child already carries
921
- // these in its seeded transcript, so only add them beyond the cut.
922
- ...(() => {
923
- const block = rootUserAuthorizationBlock({
924
- parentEvents: readSessionEvents(options.runtime.store, options.parentSessionId),
925
- ...(seedCut > 0 ? { sinceEventCount: seedCut } : {}),
1003
+ if (!prompted.result.ok) {
1004
+ await discardIfUnstarted();
1005
+ const extra = worktreeLine(false);
1006
+ return {
1007
+ content: [`subagent prompt failed: ${prompted.result.error.message}`, extra]
1008
+ .filter(Boolean)
1009
+ .join("\n"),
1010
+ isError: true,
1011
+ };
1012
+ }
1013
+ // Spawn accepted — the child is no longer a half-made shell.
1014
+ started = true;
1015
+ // Drain (or busy latch) now counts in active; drop the pending slot so
1016
+ // a sibling parallel `subagent` is not double-charged during wait.
1017
+ dropSpawnSlot();
1018
+ if (background) {
1019
+ return {
1020
+ content: [
1021
+ `Started background subagent \`${childId}\` (${label}).`,
1022
+ `task ${task.id}` +
1023
+ (spawnRole && spawnRole !== "default" ? ` · role ${spawnRole}` : ""),
1024
+ inherit
1025
+ ? "Seeded with this session's completed turns (open turn excluded)."
1026
+ : undefined,
1027
+ outputSchema
1028
+ ? "OUTPUT CONTRACT attached; result will be schema-checked on idle."
1029
+ : undefined,
1030
+ childResumeHint(childId),
1031
+ "Use followup_task to wake a new task, send_message (delivery queue|steer) to continue, wait_agent for results, interrupt_agent (takeover true to pause for human) to stop, analytics for quota.",
1032
+ "Keep working; do not busy-poll.",
1033
+ worktreeLine(true) || undefined,
1034
+ ]
1035
+ .filter(Boolean)
1036
+ .join("\n"),
1037
+ };
1038
+ }
1039
+ try {
1040
+ await waitDrainIdle(options.runtime, childId, signal, foregroundWaitMs);
1041
+ }
1042
+ catch (err) {
1043
+ const message = err instanceof Error ? err.message : String(err);
1044
+ const timedOut = /timed out waiting/i.test(message);
1045
+ // Salvage before cancelling: `session.cancel` drops the chain, so a
1046
+ // child that had already written its answer would bill the parent for
1047
+ // nothing. A child that produced nothing is stuck, so it still gets
1048
+ // cancelled instead of left dangling.
1049
+ const owned = childOwnedEvents(options.runtime, childId);
1050
+ const salvaged = lastAssistantBodyText(owned);
1051
+ const retrying = timedOut
1052
+ ? lastModelRetryNotice(owned)
1053
+ : undefined;
1054
+ if (!salvaged) {
1055
+ await dispatchFaceMethod(options.runtime, "session.cancel", `tool-sa-c-${childId}`,
1056
+ // `parent`, not the default `user`: a parent that gave up waiting
1057
+ // is not a human hitting Stop. The old default stamped
1058
+ // turn/end `reason: user`, so every timed-out child read as "stopped
1059
+ // by the user" in the child log and in the completion notice.
1060
+ { sessionId: childId, cause: { kind: "parent" } }).catch(() => undefined);
1061
+ }
1062
+ options.runtime.agentTeamTasks.complete(task.id, {
1063
+ ok: false,
1064
+ preview: message,
926
1065
  });
927
- return block ? { userAuthorization: block } : {};
928
- })(),
929
- });
930
- const task = openTask(childId);
931
- const worktreeLine = reclaimIsolated;
932
- // A transport-level reject (not an RpcResult error) must not leak the
933
- // child: the catch backstop tears the half-made spawn down.
934
- let prompted;
935
- try {
936
- prompted = await dispatchFaceMethod(options.runtime, "session.prompt", `tool-sa-p-${childId}`, {
937
- sessionId: childId,
938
- mode: "queue",
939
- content: [{ type: "text", text: childPrompt }],
940
- });
941
- }
942
- catch (err) {
943
- await discardIfUnstarted();
944
- const extra = worktreeLine(false);
945
- const message = err instanceof Error ? err.message : String(err);
946
- return {
947
- content: [`subagent prompt failed: ${message}`, extra]
948
- .filter(Boolean)
949
- .join("\n"),
950
- isError: true,
951
- };
952
- }
953
- if (!prompted.result.ok) {
954
- await discardIfUnstarted();
955
- const extra = worktreeLine(false);
956
- return {
957
- content: [`subagent prompt failed: ${prompted.result.error.message}`, extra]
958
- .filter(Boolean)
959
- .join("\n"),
960
- isError: true,
961
- };
962
- }
963
- // Spawn accepted — the child is no longer a half-made shell.
964
- started = true;
965
- if (background) {
966
- return {
967
- content: [
968
- `Started background subagent \`${childId}\` (${label}).`,
969
- `task ${task.id}` +
970
- (spawnRole && spawnRole !== "default" ? ` · role ${spawnRole}` : ""),
971
- inherit
972
- ? "Seeded with this session's completed turns (open turn excluded)."
973
- : undefined,
974
- outputSchema
975
- ? "OUTPUT CONTRACT attached; result will be schema-checked on idle."
976
- : undefined,
977
- childResumeHint(childId),
978
- "Use followup_task to wake a new task, send_message (delivery queue|steer) to continue, wait_agent for results, interrupt_agent (takeover true to pause for human) to stop, analytics for quota.",
979
- "Keep working; do not busy-poll.",
980
- worktreeLine(true) || undefined,
981
- ]
982
- .filter(Boolean)
983
- .join("\n"),
984
- };
985
- }
986
- try {
987
- await waitDrainIdle(options.runtime, childId, signal, foregroundWaitMs);
988
- }
989
- catch (err) {
990
- const message = err instanceof Error ? err.message : String(err);
991
- const timedOut = /timed out waiting/i.test(message);
992
- // Salvage before cancelling: `session.cancel` drops the chain, so a
993
- // child that had already written its answer would bill the parent for
994
- // nothing. A child that produced nothing is stuck, so it still gets
995
- // cancelled instead of left dangling.
996
- const owned = childOwnedEvents(options.runtime, childId);
997
- const salvaged = lastAssistantBodyText(owned);
998
- const retrying = timedOut
999
- ? lastModelRetryNotice(owned)
1000
- : undefined;
1001
- if (!salvaged) {
1002
- await dispatchFaceMethod(options.runtime, "session.cancel", `tool-sa-c-${childId}`,
1003
- // `parent`, not the default `user`: a parent that gave up waiting
1004
- // is not a human hitting Stop. The old default stamped
1005
- // turn/end `reason: user`, so every timed-out child read as "stopped
1006
- // by the user" in the child log and in the completion notice.
1007
- { sessionId: childId, cause: { kind: "parent" } }).catch(() => undefined);
1066
+ const extra = worktreeLine(false);
1067
+ return {
1068
+ content: [
1069
+ salvaged
1070
+ ? `${message}, but the child finished work before the wait expired — its last answer is salvaged below.`
1071
+ : `subagent failed: ${message}`,
1072
+ salvaged || undefined,
1073
+ timedOut ? childResumeHint(childId) : undefined,
1074
+ retrying,
1075
+ extra,
1076
+ ]
1077
+ .filter(Boolean)
1078
+ .join("\n"),
1079
+ isError: true,
1080
+ };
1081
+ }
1082
+ const all = readSessionEvents(options.runtime.store, childId);
1083
+ // Never return seeded parent assistant text as the child's result.
1084
+ const link = options.runtime.subagents.getByChild(childId);
1085
+ const readOwned = () => subagentOwnedEvents(link, readSessionEvents(options.runtime.store, childId));
1086
+ let owned = subagentOwnedEvents(link, all);
1087
+ let text = lastAssistantBodyText(owned);
1088
+ let schemaValid;
1089
+ let schemaErrors;
1090
+ if (outputSchema) {
1091
+ // An empty body cannot satisfy the contract either: a silent skip
1092
+ // would hand the parent `ok: true` with an unvalidated (missing)
1093
+ // answer, so treat "no text" as one more schema failure.
1094
+ let checked = text
1095
+ ? validateOutputAgainstSchema(text, outputSchema)
1096
+ : { valid: false, errors: ["child produced no assistant text"] };
1097
+ if (!checked.valid) {
1098
+ const retry = await dispatchFaceMethod(options.runtime, "session.prompt", `tool-sa-schema-${childId}`, {
1099
+ sessionId: childId,
1100
+ mode: "queue",
1101
+ content: [
1102
+ {
1103
+ type: "text",
1104
+ text: buildOutputSchemaRetryMessage(checked.errors),
1105
+ },
1106
+ ],
1107
+ });
1108
+ if (retry.result.ok) {
1109
+ try {
1110
+ await waitDrainIdle(options.runtime, childId, signal);
1111
+ }
1112
+ catch {
1113
+ /* keep first answer */
1114
+ }
1115
+ owned = readOwned();
1116
+ text = lastAssistantBodyText(owned) || text;
1117
+ checked = text
1118
+ ? validateOutputAgainstSchema(text, outputSchema)
1119
+ : { valid: false, errors: ["child produced no assistant text"] };
1120
+ }
1121
+ }
1122
+ schemaValid = checked.valid;
1123
+ schemaErrors = checked.errors;
1008
1124
  }
1009
1125
  options.runtime.agentTeamTasks.complete(task.id, {
1010
- ok: false,
1011
- preview: message,
1126
+ // An unvalidated answer is not a successful delegation: the task
1127
+ // board must not read `completed` when the contract was skipped.
1128
+ ok: schemaValid !== false,
1129
+ ...(text ? { preview: text } : {}),
1130
+ ...(schemaValid !== undefined ? { schemaValid } : {}),
1131
+ ...(schemaErrors && schemaErrors.length > 0
1132
+ ? { schemaErrors }
1133
+ : {}),
1012
1134
  });
1013
1135
  const extra = worktreeLine(false);
1136
+ if (!text) {
1137
+ return {
1138
+ content: [`(subagent ${childId} finished with no assistant text)`, extra]
1139
+ .filter(Boolean)
1140
+ .join("\n"),
1141
+ };
1142
+ }
1143
+ const schemaLine = schemaValid === undefined
1144
+ ? undefined
1145
+ : schemaValid
1146
+ ? "schema_valid=true"
1147
+ : `schema_valid=false\n${(schemaErrors ?? []).map((e) => `- ${e}`).join("\n")}`;
1014
1148
  return {
1015
1149
  content: [
1016
- salvaged
1017
- ? `${message}, but the child finished work before the wait expired — its last answer is salvaged below.`
1018
- : `subagent failed: ${message}`,
1019
- salvaged || undefined,
1020
- timedOut ? childResumeHint(childId) : undefined,
1021
- retrying,
1150
+ boundChildAnswer(options.parentSessionId, childId, text),
1151
+ schemaLine,
1022
1152
  extra,
1023
1153
  ]
1024
1154
  .filter(Boolean)
1025
1155
  .join("\n"),
1026
- isError: true,
1156
+ ...(schemaValid === false ? { isError: true } : {}),
1027
1157
  };
1028
1158
  }
1029
- const all = readSessionEvents(options.runtime.store, childId);
1030
- // Never return seeded parent assistant text as the child's result.
1031
- const link = options.runtime.subagents.getByChild(childId);
1032
- const readOwned = () => subagentOwnedEvents(link, readSessionEvents(options.runtime.store, childId));
1033
- let owned = subagentOwnedEvents(link, all);
1034
- let text = lastAssistantBodyText(owned);
1035
- let schemaValid;
1036
- let schemaErrors;
1037
- if (outputSchema) {
1038
- // An empty body cannot satisfy the contract either: a silent skip
1039
- // would hand the parent `ok: true` with an unvalidated (missing)
1040
- // answer, so treat "no text" as one more schema failure.
1041
- let checked = text
1042
- ? validateOutputAgainstSchema(text, outputSchema)
1043
- : { valid: false, errors: ["child produced no assistant text"] };
1044
- if (!checked.valid) {
1045
- const retry = await dispatchFaceMethod(options.runtime, "session.prompt", `tool-sa-schema-${childId}`, {
1046
- sessionId: childId,
1047
- mode: "queue",
1048
- content: [
1049
- {
1050
- type: "text",
1051
- text: buildOutputSchemaRetryMessage(checked.errors),
1052
- },
1053
- ],
1054
- });
1055
- if (retry.result.ok) {
1056
- try {
1057
- await waitDrainIdle(options.runtime, childId, signal);
1058
- }
1059
- catch {
1060
- /* keep first answer */
1061
- }
1062
- owned = readOwned();
1063
- text = lastAssistantBodyText(owned) || text;
1064
- checked = text
1065
- ? validateOutputAgainstSchema(text, outputSchema)
1066
- : { valid: false, errors: ["child produced no assistant text"] };
1067
- }
1068
- }
1069
- schemaValid = checked.valid;
1070
- schemaErrors = checked.errors;
1071
- }
1072
- options.runtime.agentTeamTasks.complete(task.id, {
1073
- // An unvalidated answer is not a successful delegation: the task
1074
- // board must not read `completed` when the contract was skipped.
1075
- ok: schemaValid !== false,
1076
- ...(text ? { preview: text } : {}),
1077
- ...(schemaValid !== undefined ? { schemaValid } : {}),
1078
- ...(schemaErrors && schemaErrors.length > 0
1079
- ? { schemaErrors }
1080
- : {}),
1081
- });
1082
- const extra = worktreeLine(false);
1083
- if (!text) {
1084
- return {
1085
- content: [`(subagent ${childId} finished with no assistant text)`, extra]
1086
- .filter(Boolean)
1087
- .join("\n"),
1088
- };
1159
+ finally {
1160
+ dropSpawnSlot();
1089
1161
  }
1090
- const schemaLine = schemaValid === undefined
1091
- ? undefined
1092
- : schemaValid
1093
- ? "schema_valid=true"
1094
- : `schema_valid=false\n${(schemaErrors ?? []).map((e) => `- ${e}`).join("\n")}`;
1095
- return {
1096
- content: [
1097
- boundChildAnswer(options.parentSessionId, childId, text),
1098
- schemaLine,
1099
- extra,
1100
- ]
1101
- .filter(Boolean)
1102
- .join("\n"),
1103
- ...(schemaValid === false ? { isError: true } : {}),
1104
- };
1105
1162
  },
1106
1163
  };
1107
1164
  }