presubmit 0.1.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 (220) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +51 -0
  3. package/THIRD_PARTY_NOTICES.md +24 -0
  4. package/apps/web/dist/assets/FileCode-CZZH52o7.js +5 -0
  5. package/apps/web/dist/assets/FileCode-DovDO7Jh.css +1 -0
  6. package/apps/web/dist/assets/SettingsPage-CRvOwdjt.js +4 -0
  7. package/apps/web/dist/assets/SettingsPage-UbYSCwJj.css +1 -0
  8. package/apps/web/dist/assets/TerminalPanel-0sw-J6YS.css +1 -0
  9. package/apps/web/dist/assets/TerminalPanel-BqUjPWav.js +16 -0
  10. package/apps/web/dist/assets/index-BhqNNFja.js +54 -0
  11. package/apps/web/dist/assets/index-DD8VXy8H.css +1 -0
  12. package/apps/web/dist/assets/presubmit-mark-BBQ19Fmr.svg +7 -0
  13. package/apps/web/dist/assets/presubmit-mark-busy-CwBC-7on.svg +8 -0
  14. package/apps/web/dist/assets/react-vendor-wiHys0m2.js +9 -0
  15. package/apps/web/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
  16. package/apps/web/dist/index.html +6 -0
  17. package/apps/web/dist/service-worker.js +11 -0
  18. package/dist/apps/server/src/agent-builder.js +204 -0
  19. package/dist/apps/server/src/attach.js +163 -0
  20. package/dist/apps/server/src/attachments.js +86 -0
  21. package/dist/apps/server/src/browser-access.js +131 -0
  22. package/dist/apps/server/src/check-provenance.js +95 -0
  23. package/dist/apps/server/src/check-supervisor.js +112 -0
  24. package/dist/apps/server/src/checks.js +402 -0
  25. package/dist/apps/server/src/child-collaboration-guidance.js +2 -0
  26. package/dist/apps/server/src/child-sessions.js +191 -0
  27. package/dist/apps/server/src/cli.js +162 -0
  28. package/dist/apps/server/src/conversation-search.js +12 -0
  29. package/dist/apps/server/src/coordinator-messages.js +72 -0
  30. package/dist/apps/server/src/dev-auth.js +54 -0
  31. package/dist/apps/server/src/dev-server.js +201 -0
  32. package/dist/apps/server/src/dev-worker.js +48 -0
  33. package/dist/apps/server/src/diagnostics.js +105 -0
  34. package/dist/apps/server/src/fast-mode.js +8 -0
  35. package/dist/apps/server/src/folder-picker.js +54 -0
  36. package/dist/apps/server/src/git-blobs.js +62 -0
  37. package/dist/apps/server/src/git-handoff.js +127 -0
  38. package/dist/apps/server/src/history-sources.js +59 -0
  39. package/dist/apps/server/src/hub.js +159 -0
  40. package/dist/apps/server/src/internal-transport.js +38 -0
  41. package/dist/apps/server/src/local-embeddings.js +70 -0
  42. package/dist/apps/server/src/ollama-setup.js +56 -0
  43. package/dist/apps/server/src/planning.js +30 -0
  44. package/dist/apps/server/src/project-agents.js +276 -0
  45. package/dist/apps/server/src/project-memory.js +94 -0
  46. package/dist/apps/server/src/projects.js +106 -0
  47. package/dist/apps/server/src/queued-launch.js +13 -0
  48. package/dist/apps/server/src/reply-search.js +172 -0
  49. package/dist/apps/server/src/review-context.js +60 -0
  50. package/dist/apps/server/src/review-refresh.js +14 -0
  51. package/dist/apps/server/src/reviewers.js +75 -0
  52. package/dist/apps/server/src/search-settings.js +22 -0
  53. package/dist/apps/server/src/semantic-history.js +186 -0
  54. package/dist/apps/server/src/server.js +4338 -0
  55. package/dist/apps/server/src/session-changes.js +181 -0
  56. package/dist/apps/server/src/session-router-settings.js +198 -0
  57. package/dist/apps/server/src/session-workspaces.js +135 -0
  58. package/dist/apps/server/src/sessions.js +579 -0
  59. package/dist/apps/server/src/shared-auth.js +70 -0
  60. package/dist/apps/server/src/shell-evaluator-settings.js +53 -0
  61. package/dist/apps/server/src/store.js +203 -0
  62. package/dist/apps/server/src/tailscale-process.js +115 -0
  63. package/dist/apps/server/src/tailscale-serve-worker.js +33 -0
  64. package/dist/apps/server/src/tailscale-serve.js +276 -0
  65. package/dist/apps/server/src/task-capacity.js +92 -0
  66. package/dist/apps/server/src/task-graph.js +190 -0
  67. package/dist/apps/server/src/task-logs.js +74 -0
  68. package/dist/apps/server/src/task-numbers.js +25 -0
  69. package/dist/apps/server/src/task-orchestrator.js +1088 -0
  70. package/dist/apps/server/src/terminal.js +182 -0
  71. package/dist/apps/server/src/terminals.js +80 -0
  72. package/dist/apps/server/src/tool-safety.js +2 -0
  73. package/dist/apps/server/src/workspace-lock.js +140 -0
  74. package/dist/apps/server/src/workspace-watcher.js +65 -0
  75. package/dist/apps/server/src/workspace.js +517 -0
  76. package/dist/packages/agent-pi/src/agent-builder-tool.js +32 -0
  77. package/dist/packages/agent-pi/src/agent-capabilities.js +44 -0
  78. package/dist/packages/agent-pi/src/ask-user.js +86 -0
  79. package/dist/packages/agent-pi/src/auth.js +510 -0
  80. package/dist/packages/agent-pi/src/check-tools.js +72 -0
  81. package/dist/packages/agent-pi/src/command-policy.js +246 -0
  82. package/dist/packages/agent-pi/src/coordinator-tools.js +122 -0
  83. package/dist/packages/agent-pi/src/fast-mode.js +67 -0
  84. package/dist/packages/agent-pi/src/file-tools.js +163 -0
  85. package/dist/packages/agent-pi/src/history-tool.js +40 -0
  86. package/dist/packages/agent-pi/src/history.js +34 -0
  87. package/dist/packages/agent-pi/src/index.js +177 -0
  88. package/dist/packages/agent-pi/src/ipc.js +314 -0
  89. package/dist/packages/agent-pi/src/jev-session-router.js +408 -0
  90. package/dist/packages/agent-pi/src/jev-tools.js +71 -0
  91. package/dist/packages/agent-pi/src/local-model.js +277 -0
  92. package/dist/packages/agent-pi/src/memory-tools.js +119 -0
  93. package/dist/packages/agent-pi/src/model.js +25 -0
  94. package/dist/packages/agent-pi/src/planning.js +206 -0
  95. package/dist/packages/agent-pi/src/resource-loader.js +23 -0
  96. package/dist/packages/agent-pi/src/review-tools.js +158 -0
  97. package/dist/packages/agent-pi/src/review-transition.js +73 -0
  98. package/dist/packages/agent-pi/src/review.js +40 -0
  99. package/dist/packages/agent-pi/src/routing.js +27 -0
  100. package/dist/packages/agent-pi/src/session-setup.js +118 -0
  101. package/dist/packages/agent-pi/src/shell-context.js +51 -0
  102. package/dist/packages/agent-pi/src/shell-policy.js +281 -0
  103. package/dist/packages/agent-pi/src/shell-safety.js +205 -0
  104. package/dist/packages/agent-pi/src/shell.js +121 -0
  105. package/dist/packages/agent-pi/src/snapshot-permissions.js +12 -0
  106. package/dist/packages/agent-pi/src/spike.js +41 -0
  107. package/dist/packages/agent-pi/src/task-tools.js +53 -0
  108. package/dist/packages/agent-pi/src/types.js +1 -0
  109. package/dist/packages/agent-pi/src/usage.js +30 -0
  110. package/dist/packages/agent-pi/src/worker.js +602 -0
  111. package/dist/packages/protocol/src/activity-summary.js +77 -0
  112. package/dist/packages/protocol/src/agents.js +301 -0
  113. package/dist/packages/protocol/src/browser-access.js +1 -0
  114. package/dist/packages/protocol/src/checks.js +40 -0
  115. package/dist/packages/protocol/src/child-sessions.js +28 -0
  116. package/dist/packages/protocol/src/command-policy.js +3 -0
  117. package/dist/packages/protocol/src/coordinator.js +135 -0
  118. package/dist/packages/protocol/src/delegation.js +40 -0
  119. package/dist/packages/protocol/src/git-diff.js +71 -0
  120. package/dist/packages/protocol/src/git-handoff.js +1 -0
  121. package/dist/packages/protocol/src/index.js +1 -0
  122. package/dist/packages/protocol/src/memory.js +46 -0
  123. package/dist/packages/protocol/src/metrics.js +70 -0
  124. package/dist/packages/protocol/src/planning.js +138 -0
  125. package/dist/packages/protocol/src/retrieval.js +36 -0
  126. package/dist/packages/protocol/src/reviewers.js +17 -0
  127. package/dist/packages/protocol/src/search.js +16 -0
  128. package/dist/packages/protocol/src/session-changes.js +29 -0
  129. package/dist/packages/protocol/src/session-router.js +123 -0
  130. package/dist/packages/protocol/src/shell-evaluator.js +43 -0
  131. package/dist/packages/protocol/src/task-graph.js +1 -0
  132. package/dist/packages/protocol/src/token-usage.js +17 -0
  133. package/dist/packages/protocol/src/user-questions.js +106 -0
  134. package/package.json +71 -0
  135. package/vendor/lancedb/NODEJS_THIRD_PARTY_LICENSES.md +668 -0
  136. package/vendor/lancedb/RUST_THIRD_PARTY_LICENSES.html +14607 -0
  137. package/vendor/lancedb/dist/arrow.d.ts +296 -0
  138. package/vendor/lancedb/dist/arrow.js +1145 -0
  139. package/vendor/lancedb/dist/arrow_type.d.ts +9 -0
  140. package/vendor/lancedb/dist/arrow_type.js +29 -0
  141. package/vendor/lancedb/dist/connection.d.ts +508 -0
  142. package/vendor/lancedb/dist/connection.js +298 -0
  143. package/vendor/lancedb/dist/embedding/embedding_function.d.ts +103 -0
  144. package/vendor/lancedb/dist/embedding/embedding_function.js +192 -0
  145. package/vendor/lancedb/dist/embedding/index.d.ts +37 -0
  146. package/vendor/lancedb/dist/embedding/index.js +108 -0
  147. package/vendor/lancedb/dist/embedding/openai.d.ts +16 -0
  148. package/vendor/lancedb/dist/embedding/openai.js +81 -0
  149. package/vendor/lancedb/dist/embedding/registry.d.ts +94 -0
  150. package/vendor/lancedb/dist/embedding/registry.js +221 -0
  151. package/vendor/lancedb/dist/embedding/transformers.d.ts +36 -0
  152. package/vendor/lancedb/dist/embedding/transformers.js +110 -0
  153. package/vendor/lancedb/dist/header.d.ts +162 -0
  154. package/vendor/lancedb/dist/header.js +217 -0
  155. package/vendor/lancedb/dist/index.d.ts +246 -0
  156. package/vendor/lancedb/dist/index.js +176 -0
  157. package/vendor/lancedb/dist/indices.d.ts +721 -0
  158. package/vendor/lancedb/dist/indices.js +166 -0
  159. package/vendor/lancedb/dist/materialized_view.d.ts +69 -0
  160. package/vendor/lancedb/dist/materialized_view.js +112 -0
  161. package/vendor/lancedb/dist/merge.d.ts +104 -0
  162. package/vendor/lancedb/dist/merge.js +120 -0
  163. package/vendor/lancedb/dist/native.d.ts +1193 -0
  164. package/vendor/lancedb/dist/native.js +688 -0
  165. package/vendor/lancedb/dist/oauth.d.ts +66 -0
  166. package/vendor/lancedb/dist/oauth.js +15 -0
  167. package/vendor/lancedb/dist/otel.d.ts +26 -0
  168. package/vendor/lancedb/dist/otel.js +114 -0
  169. package/vendor/lancedb/dist/permutation.d.ts +143 -0
  170. package/vendor/lancedb/dist/permutation.js +184 -0
  171. package/vendor/lancedb/dist/query.d.ts +663 -0
  172. package/vendor/lancedb/dist/query.js +966 -0
  173. package/vendor/lancedb/dist/rerankers/index.d.ts +5 -0
  174. package/vendor/lancedb/dist/rerankers/index.js +19 -0
  175. package/vendor/lancedb/dist/rerankers/rrf.d.ts +14 -0
  176. package/vendor/lancedb/dist/rerankers/rrf.js +28 -0
  177. package/vendor/lancedb/dist/sanitize.d.ts +32 -0
  178. package/vendor/lancedb/dist/sanitize.js +544 -0
  179. package/vendor/lancedb/dist/scannable.d.ts +92 -0
  180. package/vendor/lancedb/dist/scannable.js +200 -0
  181. package/vendor/lancedb/dist/schema.d.ts +16 -0
  182. package/vendor/lancedb/dist/schema.js +387 -0
  183. package/vendor/lancedb/dist/table.d.ts +1069 -0
  184. package/vendor/lancedb/dist/table.js +498 -0
  185. package/vendor/lancedb/dist/util.d.ts +14 -0
  186. package/vendor/lancedb/dist/util.js +77 -0
  187. package/vendor/lancedb/license_header.txt +2 -0
  188. package/vendor/lancedb/package.json +56 -0
  189. package/vendor/transformers/LICENSE +202 -0
  190. package/vendor/transformers/dist/transformers.cjs +30844 -0
  191. package/vendor/transformers/dist/transformers.mjs +31404 -0
  192. package/vendor/transformers/package.json +55 -0
  193. package/vendor/transformers/types/backends/onnx.d.ts +30 -0
  194. package/vendor/transformers/types/configs.d.ts +85 -0
  195. package/vendor/transformers/types/env.d.ts +110 -0
  196. package/vendor/transformers/types/generation/configuration_utils.d.ts +320 -0
  197. package/vendor/transformers/types/generation/logits_process.d.ts +354 -0
  198. package/vendor/transformers/types/generation/logits_sampler.d.ts +51 -0
  199. package/vendor/transformers/types/generation/parameters.d.ts +47 -0
  200. package/vendor/transformers/types/generation/stopping_criteria.d.ts +81 -0
  201. package/vendor/transformers/types/generation/streamers.d.ts +81 -0
  202. package/vendor/transformers/types/models/whisper/common_whisper.d.ts +8 -0
  203. package/vendor/transformers/types/models/whisper/generation_whisper.d.ts +76 -0
  204. package/vendor/transformers/types/models.d.ts +3684 -0
  205. package/vendor/transformers/types/ops/registry.d.ts +11 -0
  206. package/vendor/transformers/types/pipelines.d.ts +2402 -0
  207. package/vendor/transformers/types/processors.d.ts +924 -0
  208. package/vendor/transformers/types/tokenizers.d.ts +990 -0
  209. package/vendor/transformers/types/transformers.d.ts +13 -0
  210. package/vendor/transformers/types/utils/audio.d.ts +130 -0
  211. package/vendor/transformers/types/utils/constants.d.ts +2 -0
  212. package/vendor/transformers/types/utils/core.d.ts +98 -0
  213. package/vendor/transformers/types/utils/data-structures.d.ts +236 -0
  214. package/vendor/transformers/types/utils/devices.d.ts +18 -0
  215. package/vendor/transformers/types/utils/dtypes.d.ts +19 -0
  216. package/vendor/transformers/types/utils/generic.d.ts +11 -0
  217. package/vendor/transformers/types/utils/hub.d.ts +152 -0
  218. package/vendor/transformers/types/utils/image.d.ts +119 -0
  219. package/vendor/transformers/types/utils/maths.d.ts +280 -0
  220. package/vendor/transformers/types/utils/tensor.d.ts +418 -0
@@ -0,0 +1,1088 @@
1
+ import { taskLogs } from './task-logs.js';
2
+ import { parseFastMode } from './fast-mode.js';
3
+ import { childCollaborationGuidance } from './child-collaboration-guidance.js';
4
+ import { createHash, randomUUID } from 'node:crypto';
5
+ import { isDeepStrictEqual } from 'node:util';
6
+ import { reasoningLevels, } from '../../../packages/protocol/src/index.js';
7
+ import { emptyDelegationState, taskQueuedMessages, } from '../../../packages/protocol/src/delegation.js';
8
+ import { attachmentIds, prepareAttachments } from './attachments.js';
9
+ const live = (a) => ['starting', 'running'].includes(a.status);
10
+ const unfinished = (a) => ['queued', 'starting', 'running', 'waiting', 'needs_review'].includes(a.status);
11
+ const text = (value, name, max = 16000) => {
12
+ if (typeof value !== 'string' || !value.trim() || value.length > max)
13
+ throw new Error(`Invalid ${name}.`);
14
+ return value;
15
+ };
16
+ const message = (e) => (e instanceof Error ? e.message : 'Task operation failed.');
17
+ // Display numbers are server-owned metadata stripped by proposal parsing. Compare every
18
+ // brief field structurally, ignoring only metadata and undefined values omitted by JSON IPC.
19
+ const comparableTask = (task) => Object.fromEntries(Object.entries(task).filter(([key, value]) => key !== 'displayNumber' && value !== undefined));
20
+ /** Project-owned journal and dispatcher. Git/child stores are reconciled, not part of a fictitious cross-store transaction. */
21
+ export function createTaskOrchestrator(options) {
22
+ const { store, runtime, capacity } = options;
23
+ const records = store.get('delegations') ?? {};
24
+ let closing = false;
25
+ let ticking = false;
26
+ let commandTail = Promise.resolve();
27
+ const creating = new Set();
28
+ const get = (id) => (records[id] ??= { ...emptyDelegationState(), attempts: [] });
29
+ const owner = (id) => `${store.directory}:${id}`;
30
+ const save = (id, note, bumpVersion = true) => {
31
+ const record = get(id);
32
+ if (bumpVersion)
33
+ record.version++;
34
+ store.transaction(() => {
35
+ store.put('delegations', records);
36
+ if (note)
37
+ store.event({
38
+ type: 'delegation',
39
+ text: note,
40
+ data: { coordinatorId: id, version: record.version },
41
+ });
42
+ });
43
+ runtime.notify(id, note);
44
+ for (const attempt of record.attempts)
45
+ runtime.notify(attempt.sessionId);
46
+ };
47
+ // Never automatically repeat a prompt whose external effects might already have happened.
48
+ for (const [id, record] of Object.entries(records)) {
49
+ // Recovery gates pending work, not a new launch after an idle restart.
50
+ // Preserve existing pause/stop flags even when no attempts remain active.
51
+ if (record.attempts.some((attempt) => unfinished(attempt) || attempt.replies.length)) {
52
+ record.paused = true;
53
+ record.agentLaunchPaused = true;
54
+ }
55
+ for (const attempt of record.attempts) {
56
+ delete attempt.integrationConfirmed;
57
+ attempt.approvals = [];
58
+ attempt.pendingApprovals = 0;
59
+ attempt.resumeRequested = false;
60
+ attempt.userRequestedCapacity = false;
61
+ if (attempt.replies.length)
62
+ attempt.queuePaused = true;
63
+ if (attempt.status === 'needs_review') {
64
+ attempt.status = 'done';
65
+ attempt.completedAt ??= attempt.acceptedAt ?? new Date().toISOString();
66
+ }
67
+ if (attempt.status === 'queued')
68
+ attempt.blockedReason = undefined;
69
+ if (live(attempt)) {
70
+ attempt.status = 'interrupted';
71
+ attempt.error = 'Work was interrupted. Send a follow-up to continue.';
72
+ }
73
+ }
74
+ save(id);
75
+ }
76
+ const view = (id) => {
77
+ const record = get(id);
78
+ return {
79
+ ...record,
80
+ hostLimit: capacity.limit,
81
+ concurrency: capacity.limit,
82
+ attempts: record.attempts.map(({ brief, replies, cancelRequested, interruptForReply, userRequestedCapacity, dependencyAttempts, integrationConfirmed, activeRunId, requestId, pendingHandoffs, ...attempt }) => ({
83
+ ...attempt,
84
+ pendingReplies: replies.length,
85
+ queuedReplies: replies,
86
+ queuePaused: !!replies.length &&
87
+ (attempt.queuePaused ||
88
+ (record.paused && !attempt.resumeRequested) ||
89
+ ['failed', 'interrupted', 'cancelled'].includes(attempt.status)),
90
+ })),
91
+ };
92
+ };
93
+ // A follow-up snapshot depends on its target, not unrelated task progress. Include
94
+ // the run/queue identities so repeated questions and accepted answers invalidate it.
95
+ const followUpToken = (id, attemptId) => {
96
+ const record = get(id);
97
+ const attempt = record.attempts.find((a) => a.id === attemptId);
98
+ return createHash('sha256')
99
+ .update(JSON.stringify([
100
+ record.paused,
101
+ record.agentLaunchPaused,
102
+ attempt && [
103
+ attempt.id,
104
+ attempt.sessionId,
105
+ record.attempts.findLast((a) => a.taskId === attempt.taskId)?.id,
106
+ attempt.status,
107
+ attempt.question,
108
+ attempt.activeRunId,
109
+ attempt.requestId,
110
+ attempt.queuePaused,
111
+ attempt.cancelRequested,
112
+ attempt.resumeRequested,
113
+ attempt.pendingHandoffs,
114
+ attempt.replies,
115
+ ],
116
+ ]))
117
+ .digest('hex');
118
+ };
119
+ const questionPending = (id, receipt) => get(id).attempts.some((a) => receipt === `task-question:${a.id}:${a.activeRunId}` &&
120
+ get(id).attempts.findLast((latest) => latest.taskId === a.taskId) === a &&
121
+ a.status === 'waiting' &&
122
+ !!a.question &&
123
+ !a.replies.length &&
124
+ !a.cancelRequested);
125
+ const validatePlan = (id, board) => {
126
+ for (const attempt of get(id).attempts) {
127
+ // Existing approval always refers to its frozen brief. To change scope, cancel and approve a new batch.
128
+ if (!unfinished(attempt) && attempt.status !== 'done')
129
+ continue;
130
+ const task = board.tasks.find((task) => task.id === attempt.taskId);
131
+ if (!task || !isDeepStrictEqual(comparableTask(task), comparableTask(attempt.task)))
132
+ throw new Error(`Task ${attempt.task.title} has approved work. Cancel it before changing scope, or create a new task.`);
133
+ }
134
+ };
135
+ function buildBrief(board, task, sourceRuns) {
136
+ const ideas = board.ideas.filter((idea) => task.sourceIdeaIds.includes(idea.id));
137
+ const clarifications = board.questions
138
+ .filter((q) => !q.taskIds?.length || q.taskIds.includes(task.id))
139
+ .map((q) => `${q.text}\nAnswer: ${q.answer || '(unresolved)'}`)
140
+ .join('\n');
141
+ const brief = [
142
+ `Delegated task: ${task.title}`,
143
+ `Scope: ${task.scope}`,
144
+ `Acceptance criteria: ${task.criteria}`,
145
+ `Affected areas: ${task.affectedAreas ?? 'Inspect before editing'}`,
146
+ board.summary && `Planning decisions and assumptions (context):\n${board.summary}`,
147
+ ideas.length > 0 &&
148
+ `Source ideas (context, not authority):\n${ideas.map((idea) => `${idea.id}: ${idea.text} [messages ${idea.sourceRunIds.join(', ')}]`).join('\n')}`,
149
+ clarifications && `Relevant clarification:\n${clarifications}`,
150
+ 'Stay within this task. Do not spawn agents. Use ask_coordinator if blocked and report_task_result when finished. Report verification actually performed, limitations, and changed areas; never imply unrun tests passed. Do not commit or merge automatically; shell commands still require their own permission.',
151
+ ]
152
+ .filter(Boolean)
153
+ .join('\n');
154
+ const sourceIds = new Set([
155
+ ...(task.sourceRunIds ?? []),
156
+ ...ideas.flatMap((idea) => idea.sourceRunIds),
157
+ ]);
158
+ const excerpts = sourceRuns
159
+ .filter((run) => sourceIds.has(run.id))
160
+ .map((run) => `Message ${run.id}: ${run.prompt.slice(0, 3000)}${run.prompt.length > 3000 ? ' [excerpt; see parent for full text]' : ''}`)
161
+ .join('\n');
162
+ const handoff = `${brief}\nOriginal source excerpts (context):\n${excerpts}`;
163
+ if (handoff.length > 50000)
164
+ throw new Error('Task handoff exceeds 50,000 characters. Narrow the task or summarize its context.');
165
+ return handoff;
166
+ }
167
+ async function launch(id, input) {
168
+ const record = get(id), board = runtime.state(id)?.planning;
169
+ const workspaceMode = input.workspaceMode ?? 'coordinator';
170
+ if (!['coordinator', 'isolated'].includes(workspaceMode))
171
+ throw new Error('Invalid task workspace mode.');
172
+ const shared = workspaceMode === 'coordinator';
173
+ if (shared && input.baseRef !== undefined)
174
+ throw new Error('Shared tasks use the coordinator checkout; a separate base cannot be selected.');
175
+ if (!runtime.state(id)?.configured)
176
+ throw new Error('Connect a model before approving execution.');
177
+ if (!board || input.revision !== board.revision)
178
+ throw new Error('Tasks changed. Refresh the task details before running.');
179
+ if (!Array.isArray(input.taskIds) ||
180
+ !input.taskIds.length ||
181
+ input.taskIds.length > 50 ||
182
+ new Set(input.taskIds).size !== input.taskIds.length)
183
+ throw new Error('Select distinct tasks to launch.');
184
+ if (record.attempts.length + input.taskIds.length > 100)
185
+ throw new Error('This coordinator has reached 100 execution attempts. Start another coordinator.');
186
+ const tasks = input.taskIds.map((taskId) => {
187
+ const task = board.tasks.find((task) => task.id === taskId);
188
+ if (!task || task.status !== 'ready' || !task.criteria.trim())
189
+ throw new Error('Only ready tasks with acceptance criteria can launch.');
190
+ if (board.questions.some((q) => q.blocking && !q.answer.trim() && (!q.taskIds?.length || q.taskIds.includes(task.id))))
191
+ throw new Error(`Answer blocking questions for ${task.title} first.`);
192
+ if (record.attempts.some((a) => a.taskId === task.id && (unfinished(a) || a.status === 'done')))
193
+ throw new Error('Task already has approved or accepted work.');
194
+ if (!shared && !options.isGit && task.kind !== 'research' && input.localAllowed !== true)
195
+ throw new Error('Writing in a non-Git project requires explicit Local approval.');
196
+ return task;
197
+ });
198
+ for (const task of tasks) {
199
+ for (const dependency of task.dependencies ?? []) {
200
+ if (!tasks.some((t) => t.id === dependency) &&
201
+ !record.attempts.some((a) => a.taskId === dependency && (unfinished(a) || a.status === 'done')))
202
+ throw new Error('Include prerequisite tasks in this batch or approve them first.');
203
+ }
204
+ }
205
+ const baseCommit = !shared && options.isGit
206
+ ? await runtime.resolveBase(input.baseRef === undefined ? 'HEAD' : text(input.baseRef, 'base', 200))
207
+ : undefined;
208
+ // Resolution awaited filesystem work: the planner may have changed the board meanwhile.
209
+ if (runtime.state(id)?.planning?.revision !== board.revision)
210
+ throw new Error('Tasks changed before launch. Refresh the task details before running.');
211
+ const coordinator = runtime.state(id);
212
+ const defaults = {
213
+ model: coordinator.sessionPreferences?.model ?? coordinator.model,
214
+ reasoningLevel: coordinator.sessionPreferences?.reasoningLevel,
215
+ fastMode: coordinator.sessionPreferences?.fastMode,
216
+ permissionMode: shared ? coordinator.permissionMode : 'ask',
217
+ };
218
+ const overrides = new Map();
219
+ if (input.taskSettings !== undefined) {
220
+ if (!Array.isArray(input.taskSettings) || input.taskSettings.length > tasks.length)
221
+ throw new Error('Invalid task settings.');
222
+ for (const settings of input.taskSettings) {
223
+ if (!settings ||
224
+ typeof settings !== 'object' ||
225
+ !tasks.some((task) => task.id === settings.taskId) ||
226
+ overrides.has(settings.taskId))
227
+ throw new Error('Settings must refer to distinct tasks in this batch.');
228
+ if (!['ask', 'auto'].includes(settings.permissionMode))
229
+ throw new Error('Invalid task permission mode.');
230
+ const model = settings.model === undefined ? defaults.model : text(settings.model, 'model', 256);
231
+ const available = coordinator.openaiAuth?.models?.find((item) => item.id === model);
232
+ if (settings.model !== undefined && !available)
233
+ throw new Error('This model is unavailable for the current connection.');
234
+ if (settings.reasoningLevel !== undefined &&
235
+ (!reasoningLevels.includes(settings.reasoningLevel) ||
236
+ !available?.reasoningLevels?.includes(settings.reasoningLevel)))
237
+ throw new Error('This reasoning level is unavailable for the selected model.');
238
+ overrides.set(settings.taskId, {
239
+ model,
240
+ reasoningLevel: settings.reasoningLevel,
241
+ fastMode: parseFastMode(settings.fastMode, available),
242
+ permissionMode: settings.permissionMode,
243
+ });
244
+ }
245
+ }
246
+ // Validate inherited settings too, before any attempts are persisted.
247
+ for (const task of tasks) {
248
+ const settings = overrides.get(task.id) ?? defaults;
249
+ parseFastMode(settings.fastMode, coordinator.openaiAuth?.models?.find((item) => item.id === settings.model));
250
+ }
251
+ const resolved = new Map(overrides);
252
+ if (options.routeTask) {
253
+ await Promise.all(tasks
254
+ .filter((task) => !overrides.has(task.id))
255
+ .map(async (task) => {
256
+ const routed = await options.routeTask({
257
+ task,
258
+ models: coordinator.openaiAuth?.models ?? [],
259
+ }, (trace) => runtime.activity?.(id, trace));
260
+ if (!routed)
261
+ return;
262
+ const available = coordinator.openaiAuth?.models?.find((model) => model.id === routed.model);
263
+ if (!available)
264
+ return;
265
+ if (routed.reasoningLevel !== undefined &&
266
+ !available.reasoningLevels?.includes(routed.reasoningLevel))
267
+ return;
268
+ resolved.set(task.id, {
269
+ model: routed.model,
270
+ reasoningLevel: routed.reasoningLevel,
271
+ fastMode: routed.model === defaults.model ? defaults.fastMode : false,
272
+ permissionMode: defaults.permissionMode,
273
+ });
274
+ }));
275
+ if (runtime.state(id)?.planning?.revision !== board.revision)
276
+ throw new Error('Tasks changed while routing. Refresh the task details before running.');
277
+ }
278
+ const batch = {
279
+ id: randomUUID(),
280
+ boardRevision: board.revision,
281
+ taskIds: [...input.taskIds],
282
+ createdAt: new Date().toISOString(),
283
+ baseCommit,
284
+ concurrency: capacity.limit,
285
+ localAllowed: input.localAllowed === true,
286
+ };
287
+ const attempts = tasks.map((task) => ({
288
+ id: randomUUID(),
289
+ sessionId: randomUUID(),
290
+ batchId: batch.id,
291
+ taskId: task.id,
292
+ task: structuredClone(task),
293
+ ...(resolved.get(task.id) ?? defaults),
294
+ workspaceMode,
295
+ boardRevision: board.revision,
296
+ execution: task.execution ?? 'session',
297
+ kind: task.kind ?? 'implementation',
298
+ status: 'queued',
299
+ createdAt: batch.createdAt,
300
+ baseCommit,
301
+ pendingReplies: 0,
302
+ brief: buildBrief(board, task, runtime.state(id).runs),
303
+ replies: [],
304
+ dependencyAttempts: [],
305
+ }));
306
+ for (const attempt of attempts) {
307
+ attempt.dependencyAttempts = (attempt.task.dependencies ?? []).map((dep) => attempts.find((a) => a.taskId === dep)?.id ??
308
+ record.attempts.findLast((a) => a.taskId === dep && (unfinished(a) || a.status === 'done')).id);
309
+ }
310
+ record.batches.push(batch);
311
+ record.attempts.push(...attempts);
312
+ record.concurrency = capacity.limit;
313
+ if (input.type === 'delegation_agent_launch') {
314
+ // A fresh parent launch authorizes only this batch. Do not release older
315
+ // queued work, including work paused by a previous server's recovery.
316
+ for (const attempt of attempts)
317
+ attempt.resumeRequested = true;
318
+ record.paused =
319
+ record.paused && (!!record.agentLaunchPaused || record.attempts.length > attempts.length);
320
+ }
321
+ else
322
+ record.paused = false;
323
+ }
324
+ async function stopAttempt(attempt, discardReplies = true) {
325
+ if (!unfinished(attempt))
326
+ throw new Error('Only unfinished tasks can be stopped.');
327
+ attempt.cancelRequested = true;
328
+ attempt.interruptForReply = false;
329
+ attempt.resumeRequested = false;
330
+ attempt.userRequestedCapacity = false;
331
+ if (discardReplies)
332
+ attempt.replies = attempt.replies.filter((reply) => attempt.pendingHandoffs?.includes(reply.id));
333
+ attempt.queuePaused = true;
334
+ attempt.approvals = [];
335
+ attempt.pendingApprovals = 0;
336
+ if (live(attempt)) {
337
+ if (runtime.state(attempt.sessionId)?.runs.some((run) => run.status === 'running'))
338
+ await runtime.stop(attempt.sessionId);
339
+ // starting creation observes cancelRequested before submitting the prompt.
340
+ }
341
+ else {
342
+ attempt.status = 'cancelled';
343
+ attempt.question = undefined;
344
+ attempt.pendingApprovals = 0;
345
+ }
346
+ }
347
+ async function prepareReply(input) {
348
+ const prompt = text(input.prompt, 'task reply', 200000);
349
+ const attachments = attachmentIds(input.attachments);
350
+ if (attachments.length && !options.attachmentDirectory)
351
+ throw new Error('Task attachments are unavailable.');
352
+ const attachmentDetails = attachments.length
353
+ ? (await prepareAttachments(options.attachmentDirectory, attachments)).records
354
+ : [];
355
+ return {
356
+ id: input.id,
357
+ prompt,
358
+ attachments,
359
+ attachmentDetails,
360
+ createdAt: new Date().toISOString(),
361
+ };
362
+ }
363
+ async function resumeReplies(attempt, interrupt = false) {
364
+ if (!attempt.replies.length)
365
+ throw new Error('There are no queued messages.');
366
+ if (attempt.cancelRequested && live(attempt))
367
+ attempt.interruptForReply = true;
368
+ attempt.cancelRequested = false;
369
+ attempt.resumeRequested = true;
370
+ attempt.queuePaused = false;
371
+ if (!live(attempt))
372
+ attempt.status = 'queued';
373
+ attempt.acceptedAt = undefined;
374
+ attempt.completedAt = undefined;
375
+ attempt.error = undefined;
376
+ attempt.result = undefined;
377
+ attempt.question = undefined;
378
+ if (interrupt && live(attempt)) {
379
+ attempt.interruptForReply = true;
380
+ if (runtime.state(attempt.sessionId)?.runs.some((run) => run.status === 'running'))
381
+ await runtime.stop(attempt.sessionId);
382
+ }
383
+ }
384
+ async function executeCommand(id, input, validate) {
385
+ validate?.();
386
+ if (closing)
387
+ throw new Error('Server is stopping.');
388
+ const requestId = text(input.id, 'request ID', 128);
389
+ const key = `delegation:${id}:${requestId}`;
390
+ const digest = JSON.stringify({ ...input, version: undefined, followUpToken: undefined });
391
+ const cached = store.get(key);
392
+ if (cached) {
393
+ if (cached.digest !== digest)
394
+ throw new Error('Request ID already used for a different task action.');
395
+ return cached.response;
396
+ }
397
+ const record = get(id);
398
+ const validateFollowUp = () => {
399
+ const attempt = record.attempts.find((a) => a.id === input.attemptId);
400
+ if (!attempt || record.attempts.findLast((a) => a.taskId === attempt.taskId) !== attempt)
401
+ throw new Error('Child attempt not found or has a newer attempt. Refresh child sessions.');
402
+ if (record.paused ||
403
+ attempt.queuePaused ||
404
+ attempt.cancelRequested ||
405
+ ['cancelled', 'failed', 'interrupted'].includes(attempt.status))
406
+ throw new Error('This task is paused or stopped. The user must resume it before coordinator follow-ups.');
407
+ if (input.followUpToken !== undefined
408
+ ? input.followUpToken !== followUpToken(id, attempt.id)
409
+ : input.version !== record.version)
410
+ throw new Error('Task state changed. Refresh before taking this action.');
411
+ };
412
+ if (input.type === 'delegation_reply' && input.coordinatorFollowUp === true)
413
+ validateFollowUp();
414
+ else if (input.version !== record.version)
415
+ throw new Error('Task state changed. Refresh before taking this action.');
416
+ if (!runtime.state(id)?.planning)
417
+ throw new Error('Task delegation is available in standalone sessions.');
418
+ let launchAcceptance;
419
+ if (input.type === 'delegation_task_settings') {
420
+ const state = runtime.state(id);
421
+ if (input.revision !== state.planning.revision)
422
+ throw new Error('Tasks changed. Refresh before changing settings.');
423
+ if (!state.planning.tasks.some((task) => task.id === input.taskId))
424
+ throw new Error('Task not found.');
425
+ if (record.attempts.some((a) => a.taskId === input.taskId))
426
+ throw new Error('Task already launched; its settings are frozen.');
427
+ const settings = input.settings;
428
+ if (settings !== null) {
429
+ if (!settings ||
430
+ Object.keys(settings).some((key) => !['model', 'reasoningLevel', 'fastMode', 'permissionMode'].includes(key)) ||
431
+ !['ask', 'auto'].includes(settings.permissionMode))
432
+ throw new Error('Invalid task settings.');
433
+ const model = settings.model === undefined ? undefined : text(settings.model, 'model', 256);
434
+ const available = state.openaiAuth?.models?.find((item) => item.id === (model ?? state.model));
435
+ if (model !== undefined && !available)
436
+ throw new Error('This model is unavailable for the current connection.');
437
+ if (settings.reasoningLevel !== undefined &&
438
+ !available?.reasoningLevels?.includes(settings.reasoningLevel))
439
+ throw new Error('This reasoning level is unavailable for the selected model.');
440
+ record.taskSettings = {
441
+ ...record.taskSettings,
442
+ [input.taskId]: {
443
+ model,
444
+ reasoningLevel: settings.reasoningLevel,
445
+ fastMode: parseFastMode(settings.fastMode, available),
446
+ permissionMode: settings.permissionMode,
447
+ },
448
+ };
449
+ }
450
+ else if (record.taskSettings)
451
+ delete record.taskSettings[input.taskId];
452
+ }
453
+ else if (input.type === 'delegation_agent_launch') {
454
+ const board = runtime.state(id).planning;
455
+ if (input.revision !== board.revision)
456
+ throw new Error('Tasks changed. Read get_tasks again and preserve current edits.');
457
+ const selected = new Map();
458
+ const visiting = new Set();
459
+ const visit = (taskId, prerequisite = false) => {
460
+ if (selected.has(taskId))
461
+ return;
462
+ if (visiting.has(taskId))
463
+ throw new Error('Task prerequisites contain a cycle. Update tasks first.');
464
+ const task = board.tasks.find((task) => task.id === taskId);
465
+ if (!task || task.status !== 'ready' || !task.criteria.trim())
466
+ throw new Error(`Task ${taskId} is missing, deferred, or not ready. Read get_tasks and resolve its scope first.`);
467
+ if (board.questions.some((q) => q.blocking && !q.answer.trim() && (!q.taskIds?.length || q.taskIds.includes(taskId))))
468
+ throw new Error(`Answer blocking questions for ${task.title} first.`);
469
+ const previous = record.attempts.findLast((attempt) => attempt.taskId === taskId);
470
+ if (previous) {
471
+ if (prerequisite &&
472
+ (!record.paused ||
473
+ previous.resumeRequested ||
474
+ previous.status === 'done' ||
475
+ live(previous)) &&
476
+ !previous.queuePaused &&
477
+ !previous.cancelRequested &&
478
+ (unfinished(previous) || previous.status === 'done'))
479
+ return;
480
+ throw new Error(`Task ${taskId} already has attempt ${previous.id} (${previous.status}). Inspect progress; stopped work requires explicit user action, and completed work is not rerun.`);
481
+ }
482
+ visiting.add(taskId);
483
+ for (const dependency of task.dependencies ?? [])
484
+ visit(dependency, true);
485
+ visiting.delete(taskId);
486
+ selected.set(taskId, task);
487
+ };
488
+ for (const taskId of input.taskIds)
489
+ visit(taskId);
490
+ if (record.attempts.length + selected.size > 100)
491
+ throw new Error('Attempt capacity reached (100). Start another coordinator.');
492
+ const before = record.attempts.length;
493
+ await launch(id, {
494
+ ...input,
495
+ taskIds: [...selected.keys()],
496
+ workspaceMode: 'coordinator',
497
+ concurrency: capacity.limit,
498
+ taskSettings: [...selected.keys()]
499
+ .filter((taskId) => record.taskSettings?.[taskId])
500
+ .map((taskId) => ({ taskId, ...record.taskSettings[taskId] })),
501
+ });
502
+ launchAcceptance = {
503
+ acceptance: 'queued',
504
+ requestKey: input.requestKey,
505
+ attempts: record.attempts
506
+ .slice(before)
507
+ .map((a) => ({ taskId: a.taskId, attemptId: a.id, sessionId: a.sessionId })),
508
+ };
509
+ }
510
+ else if (input.type === 'delegation_launch') {
511
+ await launch(id, input);
512
+ record.agentLaunchPaused = false;
513
+ }
514
+ else if (input.type === 'delegation_pause') {
515
+ record.agentLaunchPaused = true;
516
+ record.paused = true;
517
+ for (const attempt of record.attempts) {
518
+ attempt.resumeRequested = false;
519
+ attempt.userRequestedCapacity = false;
520
+ }
521
+ }
522
+ else if (input.type === 'delegation_resume') {
523
+ if (!runtime.state(id)?.configured)
524
+ throw new Error('Reconnect a model before resuming dispatch.');
525
+ record.paused = false;
526
+ record.agentLaunchPaused = false;
527
+ for (const attempt of record.attempts.filter((item) => item.status === 'queued'))
528
+ attempt.queuePaused = false;
529
+ }
530
+ else if (input.type === 'delegation_stop_all') {
531
+ record.agentLaunchPaused = true;
532
+ record.paused = true;
533
+ for (const attempt of record.attempts.filter(unfinished))
534
+ await stopAttempt(attempt);
535
+ }
536
+ else {
537
+ const attempt = record.attempts.find((a) => a.id === input.attemptId);
538
+ if (!attempt)
539
+ throw new Error('Task attempt not found in this coordinator.');
540
+ if (attempt.pendingHandoffs?.length &&
541
+ ([
542
+ 'delegation_reply',
543
+ 'delegation_resume_replies',
544
+ 'delegation_discard_replies',
545
+ 'delegation_retry',
546
+ ].includes(input.type) ||
547
+ ([
548
+ 'delegation_edit_reply',
549
+ 'delegation_reorder_reply',
550
+ 'delegation_remove_reply',
551
+ 'delegation_send_reply',
552
+ ].includes(input.type) &&
553
+ attempt.pendingHandoffs.includes(input.messageId))))
554
+ throw new Error('Retry the pending session handoff from its message menu first.');
555
+ if ([
556
+ 'delegation_reply',
557
+ 'delegation_retry',
558
+ 'delegation_edit_reply',
559
+ 'delegation_reorder_reply',
560
+ 'delegation_remove_reply',
561
+ 'delegation_send_reply',
562
+ 'delegation_resume_replies',
563
+ ].includes(input.type) &&
564
+ record.attempts.findLast((a) => a.taskId === attempt.taskId) !== attempt)
565
+ throw new Error('This task has a newer attempt. Continue in its conversation.');
566
+ if (input.type === 'delegation_stop')
567
+ await stopAttempt(attempt, false);
568
+ else if (input.type === 'delegation_reply') {
569
+ const reply = await prepareReply(input);
570
+ validate?.();
571
+ if (input.coordinatorFollowUp === true)
572
+ validateFollowUp();
573
+ if (attempt.replies.length >= 20)
574
+ throw new Error('Queue is full (20 messages per task).');
575
+ if (input.interrupt === true)
576
+ attempt.replies.unshift(reply);
577
+ else
578
+ attempt.replies.push(reply);
579
+ await resumeReplies(attempt, input.interrupt === true);
580
+ if (input.userRequested === true)
581
+ attempt.userRequestedCapacity = true;
582
+ }
583
+ else if (input.type === 'delegation_edit_reply') {
584
+ const reply = attempt.replies.find((item) => item.id === input.messageId);
585
+ if (!reply)
586
+ throw new Error('Queued message no longer exists.');
587
+ if (input.expectedPrompt !== reply.prompt)
588
+ throw new Error('Queued message changed. Reopen it to edit the latest version.');
589
+ if (input.attachments !== undefined &&
590
+ JSON.stringify(input.expectedAttachments) !== JSON.stringify(reply.attachments ?? []))
591
+ throw new Error('Queued attachments changed. Reopen the message to edit the latest version.');
592
+ const updated = await prepareReply({
593
+ ...input,
594
+ attachments: input.attachments ?? reply.attachments,
595
+ });
596
+ // A child may finish preparing its first turn while attachment validation awaits IO.
597
+ if (!attempt.replies.includes(reply))
598
+ throw new Error('Queued message no longer exists.');
599
+ Object.assign(reply, updated, { id: reply.id, createdAt: reply.createdAt });
600
+ }
601
+ else if (input.type === 'delegation_reorder_reply') {
602
+ const expectedIds = input.expectedIds;
603
+ if (!Array.isArray(expectedIds) ||
604
+ expectedIds.length !== attempt.replies.length ||
605
+ expectedIds.some((id, index) => id !== attempt.replies[index]?.id))
606
+ throw new Error('Queue order changed. Reopen the queue before moving this message.');
607
+ const fromIndex = attempt.replies.findIndex((item) => item.id === input.messageId);
608
+ if (fromIndex < 0)
609
+ throw new Error('Queued message no longer exists.');
610
+ if (!Number.isInteger(input.toIndex) ||
611
+ input.toIndex < 0 ||
612
+ input.toIndex >= attempt.replies.length)
613
+ throw new Error('Invalid queue position.');
614
+ const [reply] = attempt.replies.splice(fromIndex, 1);
615
+ attempt.replies.splice(input.toIndex, 0, reply);
616
+ }
617
+ else if (input.type === 'delegation_remove_reply' ||
618
+ input.type === 'delegation_send_reply') {
619
+ const index = attempt.replies.findIndex((item) => item.id === input.messageId);
620
+ if (index < 0)
621
+ throw new Error('Queued message no longer exists.');
622
+ const [reply] = attempt.replies.splice(index, 1);
623
+ if (input.type === 'delegation_remove_reply')
624
+ attempt.userRequestedCapacity = false;
625
+ if (input.type === 'delegation_send_reply') {
626
+ attempt.replies.unshift(reply);
627
+ await resumeReplies(attempt, true);
628
+ if (input.userRequested === true)
629
+ attempt.userRequestedCapacity = true;
630
+ }
631
+ }
632
+ else if (input.type === 'delegation_resume_replies') {
633
+ if (!runtime.state(id)?.configured)
634
+ throw new Error('Reconnect a model before resuming this task.');
635
+ await resumeReplies(attempt);
636
+ if (input.userRequested === true)
637
+ attempt.userRequestedCapacity = true;
638
+ }
639
+ else if (input.type === 'delegation_discard_replies') {
640
+ if (!['failed', 'interrupted', 'cancelled'].includes(attempt.status))
641
+ throw new Error('Only terminal attempts can discard unexecuted replies.');
642
+ store.put(`discarded-replies:${attempt.id}:${record.version}`, attempt.replies);
643
+ attempt.replies = [];
644
+ }
645
+ else if (input.type === 'delegation_accept') {
646
+ // Older clients may still acknowledge an already completed task.
647
+ if (attempt.status !== 'done')
648
+ throw new Error('Task has not completed.');
649
+ }
650
+ else if (input.type === 'delegation_retry') {
651
+ if (!['failed', 'interrupted', 'cancelled'].includes(attempt.status))
652
+ throw new Error('Only failed, interrupted, or cancelled tasks can be retried.');
653
+ if (runtime.state(attempt.sessionId)?.runs.some((r) => r.status === 'running') ||
654
+ live(attempt))
655
+ throw new Error('Stop the previous worker before retrying.');
656
+ if (record.attempts.length >= 100)
657
+ throw new Error('Attempt limit reached.');
658
+ const retry = {
659
+ ...structuredClone(attempt),
660
+ id: randomUUID(),
661
+ sessionId: randomUUID(),
662
+ status: 'queued',
663
+ createdAt: new Date().toISOString(),
664
+ cancelRequested: false,
665
+ replies: [],
666
+ pendingHandoffs: [],
667
+ resumeRequested: false,
668
+ userRequestedCapacity: false,
669
+ activeRunId: undefined,
670
+ requestId: undefined,
671
+ error: undefined,
672
+ result: undefined,
673
+ question: undefined,
674
+ branch: undefined,
675
+ workspace: undefined,
676
+ outputCommit: undefined,
677
+ dirty: undefined,
678
+ usage: undefined,
679
+ latestActivity: undefined,
680
+ logs: undefined,
681
+ logsTruncated: undefined,
682
+ completedAt: undefined,
683
+ queuePaused: false,
684
+ pendingApprovals: 0,
685
+ approvals: [],
686
+ acceptedAt: undefined,
687
+ };
688
+ record.attempts.push(retry);
689
+ for (const pending of record.attempts.filter((a) => a.status === 'queued'))
690
+ pending.dependencyAttempts = pending.dependencyAttempts.map((dep) => dep === attempt.id ? retry.id : dep);
691
+ }
692
+ else if (input.type === 'delegation_reconcile') {
693
+ if (attempt.status !== 'interrupted')
694
+ throw new Error('Only interrupted attempts need reconciliation.');
695
+ const state = runtime.state(attempt.sessionId), run = state?.runs.at(-1);
696
+ if (!confirmedCompletion(attempt, state))
697
+ throw new Error('No confirmed completed run. Inspect external effects and use explicit Retry if appropriate.');
698
+ attempt.activeRunId = run.id;
699
+ await finish(attempt, state);
700
+ }
701
+ else if (input.type === 'delegation_base') {
702
+ if (attempt.workspaceMode === 'coordinator')
703
+ throw new Error('Shared tasks use the coordinator checkout; a separate base cannot be selected.');
704
+ if (attempt.status !== 'queued' || attempt.workspace || !options.isGit)
705
+ throw new Error('Choose a committed base before the child workspace is created.');
706
+ // Optional explicit override. Otherwise dispatch uses the commit pinned at approval,
707
+ // regardless of uncommitted or unmerged prerequisite changes.
708
+ attempt.baseCommit = await runtime.resolveBase(text(input.baseRef, 'base', 200));
709
+ }
710
+ else
711
+ throw new Error('Unknown delegation command.');
712
+ }
713
+ const response = launchAcceptance ?? { ok: true };
714
+ store.transaction(() => {
715
+ record.version++;
716
+ store.put('delegations', records);
717
+ store.put(key, { digest, response });
718
+ store.event({
719
+ type: 'delegation',
720
+ text: input.type,
721
+ data: { coordinatorId: id, version: record.version },
722
+ });
723
+ });
724
+ runtime.notify(id, input.type.replace('delegation_', 'Task action: '));
725
+ for (const attempt of record.attempts)
726
+ runtime.notify(attempt.sessionId);
727
+ return response;
728
+ }
729
+ function confirmedCompletion(attempt, state) {
730
+ const run = state?.runs.at(-1);
731
+ return (run?.status === 'completed' && !!attempt.requestId && run.taskRequestId === attempt.requestId);
732
+ }
733
+ async function finish(attempt, state) {
734
+ const run = state.runs.at(-1);
735
+ if (!run || run.status === 'running')
736
+ return;
737
+ if (run.status === 'completed')
738
+ attempt.error = undefined;
739
+ if (attempt.cancelRequested)
740
+ attempt.status = 'cancelled';
741
+ else if (attempt.replies.length &&
742
+ (run.status === 'completed' || (!attempt.queuePaused && attempt.interruptForReply))) {
743
+ attempt.status = 'queued';
744
+ attempt.interruptForReply = false;
745
+ attempt.question = undefined;
746
+ attempt.result = undefined;
747
+ }
748
+ else if (run.status === 'completed') {
749
+ attempt.result = run.taskResult ?? {
750
+ summary: state.events
751
+ .filter((e) => e.runId === run.id && e.type === 'text')
752
+ .map((e) => e.text ?? '')
753
+ .join('')
754
+ .slice(-8000) || 'Run completed without a structured result.',
755
+ changedAreas: 'Inspect the child workspace.',
756
+ verification: 'No structured verification report was provided.',
757
+ unresolved: 'Review against the acceptance criteria.',
758
+ };
759
+ attempt.question = run.taskQuestion;
760
+ attempt.status = run.taskQuestion ? 'waiting' : 'done';
761
+ if (attempt.status === 'done')
762
+ attempt.completedAt = new Date().toISOString();
763
+ try {
764
+ const info = await runtime.inspect(attempt.sessionId);
765
+ attempt.outputCommit = info.commit;
766
+ attempt.dirty = info.dirty;
767
+ }
768
+ catch {
769
+ /* Workspace may have been removed. The child remains inspectable as unavailable. */
770
+ }
771
+ }
772
+ else {
773
+ attempt.status =
774
+ run.status === 'stopped'
775
+ ? 'cancelled'
776
+ : run.status === 'interrupted'
777
+ ? 'interrupted'
778
+ : 'failed';
779
+ attempt.error = run.error;
780
+ }
781
+ if (['failed', 'interrupted', 'cancelled'].includes(attempt.status)) {
782
+ attempt.queuePaused = true;
783
+ attempt.question = undefined;
784
+ attempt.pendingApprovals = 0;
785
+ }
786
+ attempt.approvals = [];
787
+ attempt.pendingApprovals = 0;
788
+ if (attempt.status !== 'queued')
789
+ attempt.resumeRequested = false;
790
+ capacity.release(owner(attempt.sessionId));
791
+ }
792
+ async function dispatch(id, attempt) {
793
+ const record = get(id);
794
+ attempt.status = 'starting';
795
+ attempt.blockedReason = undefined;
796
+ save(id, `Starting ${attempt.task.title}`);
797
+ try {
798
+ if (!attempt.workspace) {
799
+ const created = await runtime.create(attempt.sessionId, attempt.task.title, {
800
+ coordinatorId: id,
801
+ taskId: attempt.taskId,
802
+ attemptId: attempt.id,
803
+ execution: attempt.execution,
804
+ kind: attempt.kind,
805
+ baseCommit: attempt.baseCommit,
806
+ model: attempt.model,
807
+ reasoningLevel: attempt.reasoningLevel,
808
+ fastMode: attempt.fastMode,
809
+ workspaceMode: attempt.workspaceMode,
810
+ permissionMode: attempt.permissionMode,
811
+ });
812
+ const child = runtime.state(attempt.sessionId);
813
+ if (!child)
814
+ throw new Error('Created child is unavailable. Inspect before retrying.');
815
+ attempt.workspace = child.workspace;
816
+ attempt.branch =
817
+ created?.branch ??
818
+ (options.isGit && attempt.workspaceMode !== 'coordinator'
819
+ ? `presubmit/${attempt.sessionId}`
820
+ : undefined);
821
+ }
822
+ if (attempt.cancelRequested || closing) {
823
+ attempt.status = closing ? 'interrupted' : 'cancelled';
824
+ return;
825
+ }
826
+ if (attempt.pendingHandoffs?.length) {
827
+ attempt.status = 'queued';
828
+ return;
829
+ }
830
+ const replies = taskQueuedMessages({ ...attempt, queuedReplies: attempt.replies });
831
+ const dependencies = attempt.dependencyAttempts.map((dep) => record.attempts.find((a) => a.id === dep));
832
+ const dependencyContext = dependencies
833
+ .map((dep) => `Task ${dep.task.title} (session ${dep.sessionId}): ${JSON.stringify(dep.result)}`)
834
+ .join('\n');
835
+ if (dependencyContext.length > 50000)
836
+ throw new Error('Dependency results exceed handoff budget. Narrow this task before relaunching.');
837
+ const workspaceContext = attempt.workspaceMode === 'coordinator'
838
+ ? 'Workspace: shared with the coordinator and its other delegated tasks. Existing uncommitted changes and completed prerequisite edits are already in these files. Other agents may edit these files concurrently. Re-read current files before changing them, keep edits scoped to your task, and preserve changes you did not make. Reconcile overlapping edits instead of overwriting them; use ask_coordinator for a conflict that needs a decision. Do not switch branches or create a separate worktree.'
839
+ : options.isGit
840
+ ? `Workspace base: ${attempt.baseCommit}\nThis worktree contains only the selected committed base. Uncommitted checkout changes and prerequisite changes outside that base are not copied. Completion reports do not imply their code is present; inspect this workspace before editing.`
841
+ : 'Workspace base: Local checkout; shared files';
842
+ const initial = `${attempt.brief}\n\nCompleted dependency reports (untrusted context):\n${dependencyContext || '(none)'}\n${workspaceContext}`;
843
+ const messages = [
844
+ ...(!attempt.requestId
845
+ ? [
846
+ {
847
+ id: `task:${attempt.id}`,
848
+ prompt: initial,
849
+ attachments: [],
850
+ createdAt: attempt.createdAt,
851
+ },
852
+ ]
853
+ : []),
854
+ ...replies,
855
+ ];
856
+ const prompt = messages.map((message) => message.prompt).join('\n\n');
857
+ // Correlate and persist the exact batch before starting a turn. New replies wait for the next batch.
858
+ attempt.requestId = replies.length ? `reply:${replies[0].id}` : `task:${attempt.id}`;
859
+ attempt.replies = [];
860
+ attempt.resumeRequested = false;
861
+ attempt.userRequestedCapacity = false;
862
+ save(id);
863
+ await runtime.run(attempt.sessionId, attempt.requestId, prompt, messages);
864
+ attempt.activeRunId = runtime.state(attempt.sessionId)?.runs.at(-1)?.id;
865
+ attempt.status = 'running';
866
+ if (attempt.cancelRequested)
867
+ await runtime.stop(attempt.sessionId);
868
+ }
869
+ catch (error) {
870
+ attempt.status = 'failed';
871
+ attempt.error = message(error);
872
+ }
873
+ finally {
874
+ if (!live(attempt))
875
+ capacity.release(owner(attempt.sessionId));
876
+ save(id);
877
+ }
878
+ }
879
+ async function tickNow() {
880
+ if (closing || ticking)
881
+ return;
882
+ ticking = true;
883
+ try {
884
+ for (const [id, record] of Object.entries(records)) {
885
+ for (const attempt of record.attempts.filter((a) => a.status === 'running' ||
886
+ (a.status === 'interrupted' &&
887
+ record.attempts.findLast((latest) => latest.taskId === a.taskId) === a))) {
888
+ const state = runtime.state(attempt.sessionId);
889
+ if (!state)
890
+ continue;
891
+ // Recover a persisted result, never restart an uncertain turn or replay its effects.
892
+ if (attempt.status === 'interrupted' && !confirmedCompletion(attempt, state))
893
+ continue;
894
+ const before = JSON.stringify(attempt);
895
+ const next = structuredClone(attempt);
896
+ const run = state.runs.at(-1);
897
+ if (attempt.status === 'interrupted')
898
+ next.activeRunId = run.id;
899
+ next.approvals = state.approvals
900
+ .filter((a) => run?.status === 'running' && a.status === 'pending' && a.runId === run.id)
901
+ .map((approval) => ({ ...approval }));
902
+ next.pendingApprovals = next.approvals.length;
903
+ next.usage = state.runs.reduce((sum, r) => ({
904
+ input: sum.input + (r.usage?.input ?? 0),
905
+ output: sum.output + (r.usage?.output ?? 0),
906
+ cacheRead: sum.cacheRead + (r.usage?.cacheRead ?? 0),
907
+ cacheWrite: sum.cacheWrite + (r.usage?.cacheWrite ?? 0),
908
+ }), { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
909
+ next.logs = taskLogs(state.events);
910
+ next.logsTruncated =
911
+ state.events.length >= 500 ||
912
+ state.events.filter((event) => (event.text || event.toolName || event.type.startsWith('tool_')) &&
913
+ !['command', 'task_progress'].includes(event.type)).length > next.logs.length ||
914
+ next.logs.some((event) => !!event.data?.textTruncated);
915
+ next.latestActivity = state.events
916
+ .filter((e) => e.text && e.type !== 'command' && e.type !== 'reasoning')
917
+ .at(-1)
918
+ ?.text?.slice(-500);
919
+ next.question = run?.taskQuestion;
920
+ await finish(next, state);
921
+ if (before !== JSON.stringify(next)) {
922
+ const semantic = next.status !== attempt.status || next.question !== attempt.question;
923
+ Object.assign(attempt, next);
924
+ save(id, next.status !== 'running' ? `${next.task.title}: ${next.status}` : undefined, semantic);
925
+ }
926
+ }
927
+ // A progress broadcast updates browsers but cannot wake an idle coordinator.
928
+ // Use the parent's durable, idempotent queue after the child finishes asking.
929
+ for (const attempt of record.attempts) {
930
+ if (!runtime.report ||
931
+ record.paused ||
932
+ attempt.queuePaused ||
933
+ attempt.status !== 'waiting' ||
934
+ !attempt.question ||
935
+ !attempt.activeRunId)
936
+ continue;
937
+ const receipt = `task-question:${attempt.id}:${attempt.activeRunId}`;
938
+ if (store.get(receipt))
939
+ continue;
940
+ try {
941
+ await runtime.report(id, receipt, `Task ${attempt.task.title} is waiting for an answer (attemptId: ${attempt.id}, sessionId: ${attempt.sessionId}).\n` +
942
+ `${childCollaborationGuidance}\nAttributed child question:\n${attempt.question}\n\n` +
943
+ 'Inspect get_child_sessions for current status, version and followUpToken; re-evaluate whether this question is still unresolved and the target eligible. Resolve routine questions within authorized scope yourself; ask the user only for essential missing intent. Use send_child_message with a stable requestKey to queue the answer. Reuse the key after uncertain responses; never resend an accepted reply for an old notification. Accepted/queued is not running/done and does not prove delivery or execution. Do not resume explicitly paused/stopped/failed/interrupted work.');
944
+ store.put(receipt, true);
945
+ }
946
+ catch {
947
+ // Keep the waiting question pending when the parent is unavailable or full.
948
+ }
949
+ }
950
+ for (const attempt of record.attempts.filter((a) => a.status === 'queued')) {
951
+ if (attempt.pendingHandoffs?.length ||
952
+ attempt.queuePaused ||
953
+ (record.paused && !attempt.resumeRequested))
954
+ continue;
955
+ // Removing the last follow-up must not create an empty turn or repeat the old brief.
956
+ if (attempt.requestId && !attempt.replies.length) {
957
+ const child = runtime.state(attempt.sessionId);
958
+ if (child?.runs.at(-1)?.status === 'running')
959
+ continue;
960
+ if (child?.runs.length)
961
+ await finish(attempt, child);
962
+ else {
963
+ attempt.status = 'interrupted';
964
+ attempt.error = 'The previous task run is unavailable.';
965
+ }
966
+ save(id);
967
+ continue;
968
+ }
969
+ const dependencies = attempt.dependencyAttempts.map((dep) => record.attempts.find((a) => a.id === dep));
970
+ const reason = runtime
971
+ .state(attempt.sessionId)
972
+ ?.runs.some((run) => run.status === 'running')
973
+ ? 'Waiting for the previous turn to finish.'
974
+ : dependencies.some((a) => a?.status !== 'done')
975
+ ? 'Waiting for dependencies to finish.'
976
+ : undefined;
977
+ if (reason) {
978
+ if (attempt.blockedReason !== reason) {
979
+ attempt.blockedReason = reason;
980
+ save(id);
981
+ }
982
+ continue;
983
+ }
984
+ if (!capacity.acquire(owner(attempt.sessionId), owner(id), attempt.userRequestedCapacity)) {
985
+ const reason = capacity.blockedReason(owner(attempt.sessionId), owner(id));
986
+ if (attempt.blockedReason !== reason) {
987
+ attempt.blockedReason = reason;
988
+ save(id);
989
+ }
990
+ continue;
991
+ }
992
+ const promise = dispatch(id, attempt);
993
+ creating.add(promise);
994
+ void promise.finally(() => creating.delete(promise));
995
+ }
996
+ }
997
+ }
998
+ finally {
999
+ ticking = false;
1000
+ }
1001
+ }
1002
+ const tick = () => {
1003
+ const operation = commandTail.then(tickNow);
1004
+ commandTail = operation.catch(() => { });
1005
+ return operation;
1006
+ };
1007
+ async function handoffQueued(id, input, send) {
1008
+ if (closing)
1009
+ throw new Error('Server is stopping.');
1010
+ const key = `task-handoff:${id}:${text(input.id, 'request ID', 128)}`;
1011
+ const digest = JSON.stringify(input);
1012
+ const receipt = store.get(key);
1013
+ if (receipt) {
1014
+ if (receipt !== digest)
1015
+ throw new Error('Request ID already used for a different handoff.');
1016
+ return;
1017
+ }
1018
+ const record = get(id);
1019
+ const attempt = record.attempts.find((item) => item.id === input.attemptId);
1020
+ if (!attempt || record.attempts.findLast((item) => item.taskId === attempt.taskId) !== attempt)
1021
+ throw new Error('This task has a newer attempt. Continue in its conversation.');
1022
+ const message = taskQueuedMessages({ ...attempt, queuedReplies: attempt.replies }).find((item) => item.id === input.launch.message.id);
1023
+ if (!message)
1024
+ throw new Error('Queued message no longer exists; check its destination session.');
1025
+ if (JSON.stringify(message) !== JSON.stringify(input.launch.message))
1026
+ throw new Error('Queued message changed. Reopen its menu to launch the latest version.');
1027
+ attempt.pendingHandoffs = [...new Set([...(attempt.pendingHandoffs ?? []), message.id])];
1028
+ save(id);
1029
+ try {
1030
+ await send();
1031
+ }
1032
+ catch (error) {
1033
+ attempt.queuePaused = true;
1034
+ save(id);
1035
+ throw error;
1036
+ }
1037
+ attempt.replies = attempt.replies.filter((reply) => reply.id !== message.id);
1038
+ attempt.pendingHandoffs = attempt.pendingHandoffs.filter((item) => item !== message.id);
1039
+ record.version++;
1040
+ store.transaction(() => {
1041
+ store.put(key, digest);
1042
+ store.put('delegations', records);
1043
+ });
1044
+ runtime.notify(id);
1045
+ for (const child of record.attempts)
1046
+ runtime.notify(child.sessionId);
1047
+ }
1048
+ const timer = setInterval(() => {
1049
+ void tick().catch(() => {
1050
+ /* Durable intents stay visible; the next tick reconciles state. */
1051
+ });
1052
+ }, 250);
1053
+ timer.unref();
1054
+ return {
1055
+ bridge(id) {
1056
+ return {
1057
+ view: () => view(id),
1058
+ followUpToken: (attemptId) => followUpToken(id, attemptId),
1059
+ questionPending: (receipt) => questionPending(id, receipt),
1060
+ validatePlan: (board) => validatePlan(id, board),
1061
+ command: (input, validate) => {
1062
+ const next = commandTail.then(() => executeCommand(id, input, validate));
1063
+ commandTail = next.catch(() => { });
1064
+ return next;
1065
+ },
1066
+ handoffQueued: (input, send) => {
1067
+ const next = commandTail.then(() => handoffQueued(id, input, send));
1068
+ commandTail = next.catch(() => { });
1069
+ return next;
1070
+ },
1071
+ };
1072
+ },
1073
+ view,
1074
+ active: () => Object.values(records).some((record) => record.attempts.some((a) => live(a) || ((!record.paused || a.resumeRequested) && a.status === 'queued'))),
1075
+ busy: (id) => Object.entries(records).some(([key, record]) => record.attempts.some((a) => (!id || key === id || a.sessionId === id) &&
1076
+ ['queued', 'starting', 'running', 'waiting'].includes(a.status))),
1077
+ tick,
1078
+ async close() {
1079
+ closing = true;
1080
+ clearInterval(timer);
1081
+ await commandTail;
1082
+ await Promise.allSettled(creating);
1083
+ for (const record of Object.values(records))
1084
+ for (const attempt of record.attempts)
1085
+ capacity.release(owner(attempt.sessionId));
1086
+ },
1087
+ };
1088
+ }