@sema-agent/core 1.451.0 → 2.0.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 (191) hide show
  1. package/dist/agents/agent-transcript-tool.d.ts +15 -0
  2. package/dist/agents/agent-transcript-tool.js +122 -0
  3. package/dist/agents/cascade.js +5 -8
  4. package/dist/agents/observer.js +3 -0
  5. package/dist/agents/repair-loop.d.ts +1 -1
  6. package/dist/agents/repair-loop.js +1 -1
  7. package/dist/agents/retain-ledger.d.ts +60 -0
  8. package/dist/agents/retain-ledger.js +225 -0
  9. package/dist/agents/roster-store.d.ts +1 -0
  10. package/dist/agents/roster-store.js +9 -0
  11. package/dist/agents/send-message-tool.d.ts +41 -0
  12. package/dist/agents/send-message-tool.js +469 -0
  13. package/dist/agents/subagent.d.ts +33 -108
  14. package/dist/agents/subagent.js +40 -813
  15. package/dist/agents/teacher.d.ts +0 -1
  16. package/dist/agents/teacher.js +0 -9
  17. package/dist/bin/sema-tb.js +3 -3
  18. package/dist/brain/anthropic.js +15 -40
  19. package/dist/brain/openai.js +15 -40
  20. package/dist/brain/reasoning.d.ts +1 -1
  21. package/dist/brain/stream-engine.js +2 -2
  22. package/dist/brain/stream-shared.d.ts +17 -0
  23. package/dist/brain/stream-shared.js +50 -0
  24. package/dist/brain/timeout.d.ts +1 -1
  25. package/dist/brain/timeout.js +1 -1
  26. package/dist/config/catalog.js +1 -3
  27. package/dist/config/defaults.d.ts +6 -0
  28. package/dist/config/defaults.js +6 -0
  29. package/dist/core/ask-question.d.ts +1 -1
  30. package/dist/core/ask-question.js +5 -5
  31. package/dist/core/background-agent-store.d.ts +2 -0
  32. package/dist/core/background-agent-store.js +5 -1
  33. package/dist/core/background-shell.d.ts +1 -1
  34. package/dist/core/checkpoint-store.d.ts +0 -1
  35. package/dist/core/exec-gate.d.ts +1 -1
  36. package/dist/core/exec-output-tail.d.ts +19 -1
  37. package/dist/core/exec-output-tail.js +44 -5
  38. package/dist/core/file-snapshot-store.d.ts +1 -1
  39. package/dist/core/fs-write-gate-policy.d.ts +1 -1
  40. package/dist/core/git-worktree-env.d.ts +1 -1
  41. package/dist/core/hooks.d.ts +1 -1
  42. package/dist/core/image-downsample.d.ts +44 -0
  43. package/dist/core/image-downsample.js +97 -0
  44. package/dist/core/lsp-session.d.ts +1 -0
  45. package/dist/core/lsp-session.js +24 -6
  46. package/dist/core/lsp.d.ts +11 -1
  47. package/dist/core/lsp.js +63 -6
  48. package/dist/core/mailbox-store.d.ts +3 -2
  49. package/dist/core/mailbox-store.js +19 -4
  50. package/dist/core/mcp.d.ts +3 -45
  51. package/dist/core/mcp.js +2 -97
  52. package/dist/core/media-byte-cap.d.ts +1 -1
  53. package/dist/core/memory-recall.d.ts +0 -12
  54. package/dist/core/memory-recall.js +0 -222
  55. package/dist/core/memory.d.ts +0 -6
  56. package/dist/core/memory.js +0 -88
  57. package/dist/core/message-utils.d.ts +1 -1
  58. package/dist/core/oracle-isolation.d.ts +1 -1
  59. package/dist/core/remote-env.d.ts +1 -1
  60. package/dist/core/roles.d.ts +1 -1
  61. package/dist/core/runner/active-skill-scope.d.ts +1 -1
  62. package/dist/core/runner/assemble-result.d.ts +0 -2
  63. package/dist/core/runner/memory-consolidation.d.ts +1 -1
  64. package/dist/core/runner/prepare-memory.d.ts +16 -0
  65. package/dist/core/runner/prepare-memory.js +191 -0
  66. package/dist/core/runner/prepare-task.d.ts +4 -28
  67. package/dist/core/runner/prepare-task.js +29 -208
  68. package/dist/core/runner/prompt-suggestions.d.ts +1 -1
  69. package/dist/core/runner/runtask.d.ts +2 -3
  70. package/dist/core/runner/runtask.js +337 -529
  71. package/dist/core/runner/session-rule-policy.d.ts +1 -1
  72. package/dist/core/runner/synthetic-tools.d.ts +5 -5
  73. package/dist/core/runner/synthetic-tools.js +4 -4
  74. package/dist/core/runner/tool-disclosure.d.ts +3 -3
  75. package/dist/core/runner/tool-disclosure.js +2 -2
  76. package/dist/core/runner/tool-output-projection.d.ts +6 -0
  77. package/dist/core/runner/tool-output-projection.js +81 -0
  78. package/dist/core/runtime.d.ts +1 -1
  79. package/dist/core/scheduler.d.ts +1 -1
  80. package/dist/core/sensitive-path-policy.d.ts +1 -1
  81. package/dist/core/session-reconcile.d.ts +1 -1
  82. package/dist/core/session-reconcile.js +5 -2
  83. package/dist/core/session-store.d.ts +2 -1
  84. package/dist/core/session-store.js +2 -1
  85. package/dist/core/session.d.ts +0 -1
  86. package/dist/core/task-notification.d.ts +1 -0
  87. package/dist/core/task-registry-agent.d.ts +115 -0
  88. package/dist/core/task-registry-agent.js +1163 -0
  89. package/dist/core/task-registry-monitor.d.ts +13 -0
  90. package/dist/core/task-registry-monitor.js +347 -0
  91. package/dist/core/task-registry-shared.d.ts +294 -0
  92. package/dist/core/task-registry-shared.js +271 -0
  93. package/dist/core/task-registry-workflow.d.ts +5 -0
  94. package/dist/core/task-registry-workflow.js +143 -0
  95. package/dist/core/task-registry.d.ts +54 -213
  96. package/dist/core/task-registry.js +190 -1766
  97. package/dist/core/task-tool-shape.d.ts +32 -0
  98. package/dist/core/task-tool-shape.js +83 -0
  99. package/dist/core/tool-policy.d.ts +1 -2
  100. package/dist/core/tool-result-budget.d.ts +1 -1
  101. package/dist/core/tool-result-store.d.ts +2 -2
  102. package/dist/core/tool-result-store.js +3 -3
  103. package/dist/core/tools.d.ts +7 -3
  104. package/dist/core/tools.js +3 -8
  105. package/dist/core/trace.d.ts +2 -2
  106. package/dist/core/types.d.ts +1 -29
  107. package/dist/core/workflow-journal-store.d.ts +2 -0
  108. package/dist/core/workflow-journal-store.js +14 -0
  109. package/dist/core/workflow-run-store-contract.d.ts +1 -1
  110. package/dist/core/workflow-run-store-contract.js +19 -19
  111. package/dist/engine/execution-env/node-execution-env.d.ts +14 -0
  112. package/dist/engine/execution-env/node-execution-env.js +184 -44
  113. package/dist/engine/lsp/frame-decoder.d.ts +1 -1
  114. package/dist/engine/lsp/frame-decoder.js +1 -1
  115. package/dist/engine/lsp/node-lsp-manager.d.ts +4 -2
  116. package/dist/engine/lsp/node-lsp-manager.js +22 -5
  117. package/dist/engine/lsp/stdio-lsp-transport.d.ts +1 -1
  118. package/dist/engine/lsp/stdio-lsp-transport.js +19 -8
  119. package/dist/index.d.ts +18 -16
  120. package/dist/index.js +15 -13
  121. package/dist/internal/harness-types.d.ts +7 -0
  122. package/dist/internal/harness-types.js +1 -0
  123. package/dist/internal/harness.d.ts +1 -7
  124. package/dist/internal/harness.js +1 -0
  125. package/dist/orchestration/goal.js +2 -2
  126. package/dist/orchestration/run-workflow-tool.d.ts +2 -1
  127. package/dist/orchestration/run-workflow-tool.js +29 -6
  128. package/dist/orchestration/workflow-types.d.ts +193 -0
  129. package/dist/orchestration/workflow-types.js +54 -0
  130. package/dist/orchestration/workflow.d.ts +3 -184
  131. package/dist/orchestration/workflow.js +87 -64
  132. package/dist/prompt-assembly/assemble.d.ts +3 -3
  133. package/dist/prompt-assembly/assemble.js +0 -16
  134. package/dist/prompt-assembly/explain.d.ts +1 -1
  135. package/dist/prompt-assembly/tool-catalog.d.ts +1 -1
  136. package/dist/prompts/default.d.ts +1 -6
  137. package/dist/prompts/default.js +3 -11
  138. package/dist/scenarios/env.d.ts +1 -1
  139. package/dist/scenarios/full-body.d.ts +0 -3
  140. package/dist/scenarios/full-body.js +0 -2
  141. package/dist/stores/cc/mailbox-store.js +6 -1
  142. package/dist/stores/file/background-agent-store.d.ts +2 -15
  143. package/dist/stores/file/background-agent-store.js +33 -102
  144. package/dist/stores/file/checkpoint-store.d.ts +1 -14
  145. package/dist/stores/file/checkpoint-store.js +47 -119
  146. package/dist/stores/file/file-snapshot-store.d.ts +1 -1
  147. package/dist/stores/file/mailbox-store.d.ts +1 -1
  148. package/dist/stores/file/mailbox-store.js +9 -7
  149. package/dist/stores/file/shared-ledger.d.ts +35 -0
  150. package/dist/stores/file/shared-ledger.js +94 -0
  151. package/dist/stores/file/workflow-run-store.d.ts +1 -14
  152. package/dist/stores/file/workflow-run-store.js +24 -93
  153. package/dist/tools/fs/bash-readonly-classifier.d.ts +8 -0
  154. package/dist/tools/fs/bash-readonly-classifier.js +166 -0
  155. package/dist/tools/fs/encoding.d.ts +5 -0
  156. package/dist/tools/fs/encoding.js +6 -0
  157. package/dist/tools/fs/fs-bash.d.ts +31 -0
  158. package/dist/tools/fs/fs-bash.js +672 -0
  159. package/dist/tools/fs/fs-pdf.d.ts +22 -0
  160. package/dist/tools/fs/fs-pdf.js +236 -0
  161. package/dist/tools/fs/fs-read.d.ts +8 -0
  162. package/dist/tools/fs/fs-read.js +286 -0
  163. package/dist/tools/fs/fs-search-tools.d.ts +5 -0
  164. package/dist/tools/fs/fs-search-tools.js +173 -0
  165. package/dist/tools/fs/fs-shared.d.ts +55 -0
  166. package/dist/tools/fs/fs-shared.js +144 -0
  167. package/dist/tools/fs/fs-write.d.ts +8 -0
  168. package/dist/tools/fs/fs-write.js +423 -0
  169. package/dist/tools/fs/index.d.ts +12 -65
  170. package/dist/tools/fs/index.js +26 -2026
  171. package/dist/tools/fs/notebook.d.ts +43 -0
  172. package/dist/tools/fs/notebook.js +141 -0
  173. package/dist/tools/fs/pdf.d.ts +1 -1
  174. package/dist/tools/fs/repo-map.d.ts +3 -3
  175. package/dist/tools/fs/repo-map.js +3 -3
  176. package/dist/tools/fs/safety.d.ts +1 -1
  177. package/dist/tools/fs/search.d.ts +1 -1
  178. package/dist/tools/fs/search.js +141 -12
  179. package/dist/tools/gitea-issue.js +4 -2
  180. package/dist/tools/monitor.d.ts +1 -1
  181. package/dist/tools/monitor.js +12 -8
  182. package/dist/tools/scheduler-tools.d.ts +1 -1
  183. package/dist/tools/scheduler-tools.js +16 -16
  184. package/dist/tools/task-list.js +34 -12
  185. package/dist/tools/web.d.ts +4 -2
  186. package/dist/tools/web.js +106 -20
  187. package/dist/tools/worktree.d.ts +1 -1
  188. package/dist/tools/worktree.js +14 -14
  189. package/package.json +1 -1
  190. package/dist/core/dynamic-recall.d.ts +0 -32
  191. package/dist/core/dynamic-recall.js +0 -76
@@ -0,0 +1,1163 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { uuidv7 } from "../internal/harness.js";
3
+ import { canAccessAgentRecord, BackgroundAgentStoreError, } from "./background-agent-store.js";
4
+ import { shutdownDebug } from "./shutdown-debug.js";
5
+ import { delimitUntrusted } from "./untrusted-text.js";
6
+ import { mintCompletionId, commitCompletionIdIfEmpty, clipTaskOutput, assertOwnership, sleepPollStep, alreadyTerminalStopNote, canAccess, normalizeAgentName, closestName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR, } from "./task-registry-shared.js";
7
+ export function ensureDurableHeartbeatLane(core) {
8
+ if (core.durableHeartbeatTimer !== undefined)
9
+ return;
10
+ const t = setInterval(() => {
11
+ let liveLanes = 0;
12
+ for (const h of core.handles.values()) {
13
+ if (h.type !== "background_agent")
14
+ continue;
15
+ const bh = h;
16
+ if (!bh.durable || bh.durable.poisoned)
17
+ continue;
18
+ if (bh.status === "running") {
19
+ liveLanes++;
20
+ durableAgentWriteLane(bh, {});
21
+ }
22
+ else if (bh.durable.flushFailed) {
23
+ if ((bh.durable.redrives = (bh.durable.redrives ?? 0) + 1) > 60) {
24
+ bh.durable.poisoned = true;
25
+ process.emitWarning(`sema durable-agents: terminal flush for ${bh.durable.record.handle} unrecoverable after 60 heartbeat re-drives — durable writes for this row stop here`);
26
+ continue;
27
+ }
28
+ liveLanes++;
29
+ durableAgentWriteLane(bh, {});
30
+ }
31
+ }
32
+ if (liveLanes === 0) {
33
+ clearInterval(t);
34
+ core.durableHeartbeatTimer = undefined;
35
+ }
36
+ }, DURABLE_AGENT_HEARTBEAT_MS);
37
+ t.unref?.();
38
+ core.durableHeartbeatTimer = t;
39
+ }
40
+ export function durableAgentWriteLane(handle, patch, clear) {
41
+ const lane = handle.durable;
42
+ if (!lane || lane.poisoned)
43
+ return;
44
+ lane.chain = lane.chain.then(async () => {
45
+ if (lane.poisoned)
46
+ return;
47
+ Object.assign(lane.record, patch);
48
+ if (clear !== undefined)
49
+ for (const k of clear)
50
+ delete lane.record[k];
51
+ lane.record.updatedAt = Date.now();
52
+ const poison = (why) => {
53
+ lane.poisoned = true;
54
+ process.emitWarning(`sema durable-agents: ${why} for ${lane.record.handle} — durable writes for this row stop here`);
55
+ };
56
+ const adoptOwnRow = async () => {
57
+ try {
58
+ const live = await lane.store.get(lane.record.handle, lane.record.scope);
59
+ if (!live)
60
+ return "gone";
61
+ if (live.writerId !== lane.record.writerId)
62
+ return "foreign";
63
+ if (live.writerEpoch !== lane.record.writerEpoch)
64
+ return "foreign";
65
+ lane.record.rev = live.rev;
66
+ lane.written = true;
67
+ return "adopted";
68
+ }
69
+ catch {
70
+ return "unreadable";
71
+ }
72
+ };
73
+ for (let attempt = 0; attempt < 3; attempt++) {
74
+ if (lane.poisoned)
75
+ return;
76
+ if (attempt > 0) {
77
+ await new Promise((r) => {
78
+ const t = setTimeout(r, 50 * attempt);
79
+ t.unref?.();
80
+ });
81
+ }
82
+ try {
83
+ if (!lane.written) {
84
+ await lane.store.put(structuredClone(lane.record));
85
+ lane.written = true;
86
+ lane.flushFailed = false;
87
+ lane.redrives = 0;
88
+ return;
89
+ }
90
+ const ok = await lane.store.update(lane.record.handle, lane.record.scope, structuredClone(lane.record), {
91
+ rev: lane.record.rev,
92
+ });
93
+ if (ok) {
94
+ lane.record.rev += 1;
95
+ lane.flushFailed = false;
96
+ lane.redrives = 0;
97
+ return;
98
+ }
99
+ const verdict = await adoptOwnRow();
100
+ if (lane.poisoned)
101
+ return;
102
+ if (verdict === "adopted")
103
+ continue;
104
+ if (verdict === "unreadable")
105
+ continue;
106
+ poison(verdict === "gone" ? "row reaped mid-write" : "CAS lost to a foreign writer");
107
+ return;
108
+ }
109
+ catch (e) {
110
+ if (e instanceof BackgroundAgentStoreError && e.code === "agent_record.already_exists") {
111
+ const verdict = await adoptOwnRow();
112
+ if (lane.poisoned)
113
+ return;
114
+ if (verdict === "adopted")
115
+ continue;
116
+ if (verdict === "unreadable")
117
+ continue;
118
+ if (verdict === "gone")
119
+ continue;
120
+ poison("handle collision (foreign durable row left untouched)");
121
+ return;
122
+ }
123
+ shutdownDebug(`durable-agents write failed for ${lane.record.handle} (attempt ${attempt + 1}/3)`, { error: String(e) });
124
+ }
125
+ }
126
+ lane.flushFailed = true;
127
+ });
128
+ }
129
+ export function durableAgentArmedLane(core, id) {
130
+ const h = core.handles.get(id);
131
+ if (!h || h.type !== "background_agent")
132
+ return false;
133
+ const lane = h.durable;
134
+ return lane !== undefined && !lane.poisoned;
135
+ }
136
+ export function durableAgentRowProbeLane(core, id) {
137
+ const h = core.handles.get(id);
138
+ if (!h || h.type !== "background_agent")
139
+ return undefined;
140
+ const lane = h.durable;
141
+ if (!lane)
142
+ return undefined;
143
+ return async () => {
144
+ await lane.chain.catch(() => undefined);
145
+ return lane.written && !lane.poisoned && !lane.flushFailed;
146
+ };
147
+ }
148
+ export function beginDurableClaimLane(core, id) {
149
+ const live = core.handles.get(id);
150
+ if (live !== undefined && (live.status === "running" || live.status === "pending"))
151
+ return false;
152
+ if (core.reapingHandles.has(id) || core.claimingHandles.has(id))
153
+ return false;
154
+ core.claimingHandles.add(id);
155
+ return true;
156
+ }
157
+ export function endDurableClaimLane(core, id) {
158
+ core.claimingHandles.delete(id);
159
+ }
160
+ export async function reapDurableAgentsLane(core, scope, deps, policy) {
161
+ const now = policy.now ?? Date.now();
162
+ if (policy.staleRunningMaxAgeMs !== undefined) {
163
+ await deps.store.reap(scope, now, { staleRunningMaxAgeMs: policy.staleRunningMaxAgeMs });
164
+ }
165
+ if (policy.maxAgeMs === undefined && policy.keep === undefined)
166
+ return { rowsReaped: 0, sessionsReleased: 0, skippedNoSessions: 0 };
167
+ const terminal = (await deps.store.listByScope(scope)).filter((r) => r.status !== "running" && r.status !== "parked");
168
+ terminal.sort((a, b) => b.spawnedAt - a.spawnedAt);
169
+ const doomed = new Map();
170
+ if (policy.maxAgeMs !== undefined) {
171
+ for (const r of terminal) {
172
+ if ((r.settledAt ?? r.spawnedAt) < now - policy.maxAgeMs)
173
+ doomed.set(r.handle, r);
174
+ }
175
+ }
176
+ if (policy.keep !== undefined) {
177
+ for (const r of terminal.slice(Math.max(0, policy.keep)))
178
+ doomed.set(r.handle, r);
179
+ }
180
+ let rowsReaped = 0;
181
+ let sessionsReleased = 0;
182
+ let skippedNoSessions = 0;
183
+ for (const r of doomed.values()) {
184
+ if (core.claimingHandles.has(r.handle))
185
+ continue;
186
+ core.reapingHandles.add(r.handle);
187
+ try {
188
+ let live;
189
+ try {
190
+ live = await deps.store.get(r.handle, scope);
191
+ }
192
+ catch {
193
+ continue;
194
+ }
195
+ if (!live || live.status === "running" || live.status === "parked")
196
+ continue;
197
+ const inProc = core.handles.get(r.handle);
198
+ if (inProc !== undefined && (inProc.status === "running" || inProc.status === "pending" || inProc.status === "parked"))
199
+ continue;
200
+ if (live.sessionId !== undefined && deps.sessions === undefined) {
201
+ skippedNoSessions++;
202
+ continue;
203
+ }
204
+ try {
205
+ if (!(await deps.store.delete(r.handle, scope, { rev: live.rev })))
206
+ continue;
207
+ }
208
+ catch {
209
+ continue;
210
+ }
211
+ rowsReaped++;
212
+ if (deps.mailbox !== undefined) {
213
+ try {
214
+ await deps.mailbox.drop(scope, r.handle);
215
+ }
216
+ catch {
217
+ }
218
+ }
219
+ if (live.sessionId !== undefined && deps.sessions !== undefined) {
220
+ try {
221
+ await deps.sessions.unpin?.(live.sessionId);
222
+ await deps.sessions.release(live.sessionId);
223
+ sessionsReleased++;
224
+ }
225
+ catch {
226
+ }
227
+ }
228
+ }
229
+ finally {
230
+ core.reapingHandles.delete(r.handle);
231
+ }
232
+ }
233
+ return { rowsReaped, sessionsReleased, skippedNoSessions };
234
+ }
235
+ export function releaseDurableTranscriptAnchorLane(core, id) {
236
+ const handle = core.handles.get(id);
237
+ if (!handle || handle.type !== "background_agent")
238
+ return;
239
+ durableAgentWriteLane(handle, {}, ["sessionId"]);
240
+ }
241
+ export function bindBackgroundAgentSessionLane(core, id, sessionId) {
242
+ const handle = core.handles.get(id);
243
+ if (!handle || handle.type !== "background_agent")
244
+ return;
245
+ handle.sessionId = sessionId;
246
+ durableAgentWriteLane(handle, { sessionId });
247
+ }
248
+ export function registerBackgroundAgentLane(core, input) {
249
+ assertOwnership(input, "registerBackgroundAgent");
250
+ if (input.id !== undefined && !DURABLE_AGENT_HANDLE_RE.test(input.id)) {
251
+ throw new Error(`TaskRegistry: background-agent id ${JSON.stringify(input.id)} does not match the a<hex> handle form`);
252
+ }
253
+ const id = input.id ?? core.mintTaskId("background_agent");
254
+ if (input.durableSeed !== undefined) {
255
+ const prior = core.handles.get(id);
256
+ if (prior !== undefined) {
257
+ const parkedHandoff = prior.status === "parked" && input.initialStatus === "parked";
258
+ if (prior.type !== "background_agent" || prior.status === "running" || prior.status === "pending" || (prior.status === "parked" && !parkedHandoff)) {
259
+ throw new Error(`TaskRegistry: task id already registered: ${id}`);
260
+ }
261
+ const priorLane = prior.durable;
262
+ if (priorLane !== undefined)
263
+ priorLane.poisoned = true;
264
+ core.handles.delete(id);
265
+ }
266
+ }
267
+ if (core.handles.has(id))
268
+ throw new Error(`TaskRegistry: task id already registered: ${id}`);
269
+ const now = input.now ?? Date.now();
270
+ core.handles.set(id, {
271
+ id,
272
+ type: "background_agent",
273
+ status: input.initialStatus ?? "running",
274
+ ...(input.parkedCheckpointToken !== undefined ? { parkedCheckpointToken: input.parkedCheckpointToken } : {}),
275
+ ...(input.resolveParkedStop !== undefined ? { resolveParkedStop: input.resolveParkedStop } : {}),
276
+ ...(input.sessionScoped ? { sessionScoped: true } : {}),
277
+ ...(input.description !== undefined ? { description: input.description } : {}),
278
+ ...(input.name !== undefined ? { name: input.name } : {}),
279
+ ...(input.agentType !== undefined ? { agentType: input.agentType } : {}),
280
+ ...(input.parentTaskId !== undefined ? { parentTaskId: input.parentTaskId } : {}),
281
+ ...(input.parentSessionId !== undefined ? { parentSessionId: input.parentSessionId } : {}),
282
+ ...(input.rootSessionId !== undefined ? { rootSessionId: input.rootSessionId } : {}),
283
+ ...(input.deliveryChannel !== undefined ? { channelState: input.deliveryChannel } : {}),
284
+ ...(input.retainedContinuation !== undefined ? { retainedContinuation: input.retainedContinuation } : {}),
285
+ ...(input.owner !== undefined ? { owner: input.owner } : {}),
286
+ ...(input.scope !== undefined ? { scope: input.scope } : {}),
287
+ ...(input.toolUseId !== undefined ? { toolUseId: input.toolUseId } : {}),
288
+ cycleSeq: input.durableSeed !== undefined ? (input.durableSeed.record.seq ?? 1) : 1,
289
+ createdAt: now,
290
+ updatedAt: now,
291
+ abort: input.abort,
292
+ });
293
+ if (input.store) {
294
+ const handle = core.handles.get(id);
295
+ if (input.durableSeed !== undefined) {
296
+ const seeded = structuredClone(input.durableSeed.record);
297
+ seeded.rev = input.durableSeed.rev;
298
+ handle.durable = {
299
+ store: input.store,
300
+ record: seeded,
301
+ chain: Promise.resolve(),
302
+ written: true,
303
+ poisoned: false,
304
+ flushFailed: false,
305
+ };
306
+ ensureDurableHeartbeatLane(core);
307
+ return id;
308
+ }
309
+ handle.durable = {
310
+ store: input.store,
311
+ record: {
312
+ handle: id,
313
+ scope: input.scope,
314
+ owner: input.owner,
315
+ sessionScoped: input.sessionScoped === true,
316
+ ...(input.parentSessionId !== undefined ? { parentSessionId: input.parentSessionId } : {}),
317
+ ...(input.rootSessionId !== undefined ? { rootSessionId: input.rootSessionId } : {}),
318
+ ...(input.parentTaskId !== undefined ? { parentTaskId: input.parentTaskId } : {}),
319
+ ...(input.name !== undefined ? { name: input.name } : {}),
320
+ ...(input.agentType !== undefined ? { agentType: input.agentType } : {}),
321
+ ...(input.description !== undefined ? { description: input.description } : {}),
322
+ ...(input.model !== undefined ? { model: input.model } : {}),
323
+ ...(input.teamName !== undefined ? { teamName: input.teamName } : {}),
324
+ writerId: core.writerId,
325
+ writerEpoch: 1,
326
+ spawnedAt: now,
327
+ updatedAt: now,
328
+ status: "running",
329
+ rev: 0,
330
+ },
331
+ chain: Promise.resolve(),
332
+ written: false,
333
+ poisoned: false,
334
+ flushFailed: false,
335
+ };
336
+ durableAgentWriteLane(handle, {});
337
+ ensureDurableHeartbeatLane(core);
338
+ }
339
+ return id;
340
+ }
341
+ export function parkBackgroundAgentLane(core, id, park) {
342
+ const handle = core.handles.get(id);
343
+ if (!handle || handle.type !== "background_agent")
344
+ return undefined;
345
+ if (handle.status !== "running")
346
+ return undefined;
347
+ handle.status = "parked";
348
+ handle.parkedCheckpointToken = park.checkpointToken;
349
+ handle.resolveParkedStop = park.resolveStop;
350
+ handle.notify = undefined;
351
+ for (const [, , qResolve] of handle.preAttachQueue ?? [])
352
+ qResolve?.({ ok: false, reason: "not_running" });
353
+ handle.preAttachQueue = undefined;
354
+ for (const r of [...(handle.inFlightDirect ?? [])])
355
+ r({ ok: false, reason: "not_running" });
356
+ handle.inFlightDirect = undefined;
357
+ handle.updatedAt = Date.now();
358
+ if (park.seq !== undefined)
359
+ handle.cycleSeq = park.seq;
360
+ core.pokeBgQuiescence(handle.owner);
361
+ durableAgentWriteLane(handle, {
362
+ status: "parked",
363
+ parkedCheckpointToken: park.checkpointToken,
364
+ parkedAt: Date.now(),
365
+ ...(park.seq !== undefined ? { seq: park.seq } : {}),
366
+ }, ["parkClaimId"]);
367
+ return "parked";
368
+ }
369
+ export async function reconcileParkedAgentsLane(core, stores, scope, now, opts) {
370
+ let liveFailed = 0;
371
+ for (const handle of core.handles.values()) {
372
+ if (handle.type !== "background_agent" || handle.status !== "parked")
373
+ continue;
374
+ if (handle.scope !== undefined && handle.scope !== scope)
375
+ continue;
376
+ const token = handle.parkedCheckpointToken;
377
+ let state;
378
+ if (token === undefined)
379
+ state = "missing";
380
+ else {
381
+ try {
382
+ const cp = await stores.checkpointStore.get(token);
383
+ state = cp === null ? "missing" : cp.status;
384
+ }
385
+ catch {
386
+ state = "unreadable";
387
+ }
388
+ }
389
+ if (state !== "expired" && state !== "missing")
390
+ continue;
391
+ if (handle.status !== "parked")
392
+ continue;
393
+ handle.status = "failed";
394
+ handle.error = `approval checkpoint ${state} while parked (reconciled)`;
395
+ handle.parkedCheckpointToken = undefined;
396
+ handle.resolveParkedStop = undefined;
397
+ handle.updatedAt = now;
398
+ mintCompletionId(handle);
399
+ core.pokeBgQuiescence(handle.owner);
400
+ durableAgentWriteLane(handle, { status: "failed", error: handle.error, settledAt: now, stoppedBy: "system", completionId: handle.completionId }, ["parkedCheckpointToken", "parkClaimId", "parkedAt"]);
401
+ liveFailed++;
402
+ }
403
+ const { reconcileParkedAgents: storeReconcile } = await import("./background-agent-store.js");
404
+ const liveHandles = new Set();
405
+ for (const h of core.handles.values())
406
+ if (h.type === "background_agent")
407
+ liveHandles.add(h.id);
408
+ const storeHalf = await storeReconcile(stores, scope, now, {
409
+ ...(opts?.staleClaimMaxAgeMs !== undefined ? { staleClaimMaxAgeMs: opts.staleClaimMaxAgeMs } : {}),
410
+ excludeHandles: liveHandles,
411
+ });
412
+ return { failed: storeHalf.failed + liveFailed, rolledBack: storeHalf.rolledBack };
413
+ }
414
+ export async function claimParkedAgentLane(stores, handle, access) {
415
+ if (access.scope === undefined)
416
+ return { ok: false, reason: "not_found" };
417
+ let row;
418
+ try {
419
+ row = await stores.agentStore.get(handle, access.scope);
420
+ }
421
+ catch {
422
+ return { ok: false, reason: "store_unreachable" };
423
+ }
424
+ if (!row || !canAccessAgentRecord(row, access))
425
+ return { ok: false, reason: "not_found" };
426
+ if (row.status !== "parked")
427
+ return { ok: false, reason: "not_parked" };
428
+ const token = row.parkedCheckpointToken;
429
+ if (token === undefined || row.sessionId === undefined)
430
+ return { ok: false, reason: "binding_broken" };
431
+ let cp;
432
+ try {
433
+ cp = (await stores.checkpointStore.get(token));
434
+ }
435
+ catch {
436
+ return { ok: false, reason: "store_unreachable" };
437
+ }
438
+ if (cp === null || cp.sessionId !== row.sessionId)
439
+ return { ok: false, reason: "binding_broken" };
440
+ if (cp.status !== "pending")
441
+ return { ok: false, reason: "checkpoint_not_pending" };
442
+ const claimId = randomBytes(8).toString("hex");
443
+ const reserved = structuredClone(row);
444
+ reserved.parkClaimId = claimId;
445
+ reserved.updatedAt = Date.now();
446
+ let won;
447
+ try {
448
+ won = await stores.agentStore.updateIf(handle, access.scope, reserved, {
449
+ rev: row.rev,
450
+ status: "parked",
451
+ parkedCheckpointToken: token,
452
+ parkClaimId: null,
453
+ });
454
+ }
455
+ catch {
456
+ return { ok: false, reason: "store_unreachable" };
457
+ }
458
+ if (!won)
459
+ return { ok: false, reason: "claim_lost" };
460
+ return {
461
+ ok: true,
462
+ ticket: { handle, scope: access.scope, token, claimId, reservedRev: row.rev + 1, row: structuredClone(row) },
463
+ };
464
+ }
465
+ export async function rollbackParkedClaimLane(core, stores, ticket) {
466
+ let cpStatus;
467
+ try {
468
+ const cp = (await stores.checkpointStore.get(ticket.token));
469
+ cpStatus = cp === null ? "missing" : cp.status;
470
+ }
471
+ catch {
472
+ return "retry";
473
+ }
474
+ const now = Date.now();
475
+ const rollbackCompletionIdCandidate = uuidv7();
476
+ const settleLocalParkedHandle = (error) => {
477
+ const h = core.handles.get(ticket.handle);
478
+ if (h !== undefined && h.type === "background_agent" && h.status === "parked") {
479
+ h.status = "failed";
480
+ h.error = error;
481
+ h.parkedCheckpointToken = undefined;
482
+ h.resolveParkedStop = undefined;
483
+ h.updatedAt = now;
484
+ commitCompletionIdIfEmpty(h, rollbackCompletionIdCandidate);
485
+ core.pokeBgQuiescence(h.owner);
486
+ }
487
+ };
488
+ let liveRow;
489
+ try {
490
+ liveRow = await stores.agentStore.get(ticket.handle, ticket.scope);
491
+ }
492
+ catch {
493
+ return "retry";
494
+ }
495
+ if (!liveRow || liveRow.parkClaimId !== ticket.claimId)
496
+ return "lost";
497
+ if (cpStatus === "pending") {
498
+ const restored = structuredClone(liveRow);
499
+ delete restored.parkClaimId;
500
+ restored.updatedAt = now;
501
+ restored.writerEpoch = (liveRow.writerEpoch ?? 0) + 1;
502
+ const liveHandle = core.handles.get(ticket.handle);
503
+ if (liveHandle?.type === "background_agent" &&
504
+ liveHandle.durable !== undefined &&
505
+ liveHandle.durable.record.writerId === liveRow.writerId &&
506
+ (liveHandle.durable.record.writerEpoch ?? 0) === (liveRow.writerEpoch ?? 0)) {
507
+ liveHandle.durable.record.writerEpoch = restored.writerEpoch;
508
+ }
509
+ try {
510
+ const won = await stores.agentStore.updateIf(ticket.handle, ticket.scope, restored, {
511
+ rev: liveRow.rev,
512
+ status: "parked",
513
+ parkedCheckpointToken: ticket.token,
514
+ parkClaimId: ticket.claimId,
515
+ });
516
+ if (!won)
517
+ return "lost";
518
+ try {
519
+ const cp2 = (await stores.checkpointStore.get(ticket.token));
520
+ if (cp2 !== null && cp2.status === "pending")
521
+ return "rolled_back";
522
+ const post = await stores.agentStore.get(ticket.handle, ticket.scope);
523
+ if (post && post.status === "parked") {
524
+ const term = structuredClone(post);
525
+ term.status = "failed";
526
+ term.error = `approval checkpoint ${cp2 === null ? "missing" : cp2.status} under the resume claim (claim rollback)`;
527
+ term.stoppedBy = "system";
528
+ term.settledAt = now;
529
+ term.updatedAt = now;
530
+ term.completionId = rollbackCompletionIdCandidate;
531
+ delete term.parkedCheckpointToken;
532
+ delete term.parkClaimId;
533
+ delete term.parkedAt;
534
+ if (await stores.agentStore.updateIf(ticket.handle, ticket.scope, term, { rev: post.rev, status: "parked" })) {
535
+ settleLocalParkedHandle(term.error);
536
+ return "failed";
537
+ }
538
+ }
539
+ return "lost";
540
+ }
541
+ catch {
542
+ return "rolled_back";
543
+ }
544
+ }
545
+ catch {
546
+ return "retry";
547
+ }
548
+ }
549
+ const error = cpStatus === "resolved"
550
+ ? "approval consumed but the resume could not proceed — outcome unknown (claim rollback)"
551
+ : `approval checkpoint ${cpStatus} under the resume claim (claim rollback)`;
552
+ if (liveRow.status !== "parked")
553
+ return "lost";
554
+ const failed = structuredClone(liveRow);
555
+ failed.status = "failed";
556
+ failed.error = error;
557
+ failed.stoppedBy = "system";
558
+ failed.settledAt = now;
559
+ failed.updatedAt = now;
560
+ failed.completionId = rollbackCompletionIdCandidate;
561
+ delete failed.parkedCheckpointToken;
562
+ delete failed.parkClaimId;
563
+ delete failed.parkedAt;
564
+ try {
565
+ const won = await stores.agentStore.updateIf(ticket.handle, ticket.scope, failed, {
566
+ rev: liveRow.rev,
567
+ status: "parked",
568
+ parkClaimId: ticket.claimId,
569
+ });
570
+ if (won)
571
+ settleLocalParkedHandle(error);
572
+ return won ? "failed" : "lost";
573
+ }
574
+ catch {
575
+ return "retry";
576
+ }
577
+ }
578
+ export async function consumeParkedFlipLane(core, id, stores, ticket) {
579
+ const handle = core.handles.get(id);
580
+ if (!handle || handle.type !== "background_agent" || handle.status !== "parked")
581
+ return false;
582
+ const lane = handle.durable;
583
+ const doFlip = async () => {
584
+ let live;
585
+ try {
586
+ live = await stores.agentStore.get(ticket.handle, ticket.scope);
587
+ }
588
+ catch {
589
+ return undefined;
590
+ }
591
+ if (!live || live.status !== "parked" || live.parkClaimId !== ticket.claimId || live.parkedCheckpointToken !== ticket.token)
592
+ return undefined;
593
+ const flipped = structuredClone(live);
594
+ flipped.status = "running";
595
+ flipped.writerId = core.writerId;
596
+ flipped.writerEpoch = (live.writerEpoch ?? 0) + 1;
597
+ flipped.updatedAt = Date.now();
598
+ flipped.seq = (live.seq ?? 1) + 1;
599
+ delete flipped.parkedCheckpointToken;
600
+ delete flipped.parkedAt;
601
+ try {
602
+ const won = await stores.agentStore.updateIf(ticket.handle, ticket.scope, flipped, {
603
+ rev: live.rev,
604
+ status: "parked",
605
+ parkedCheckpointToken: ticket.token,
606
+ parkClaimId: ticket.claimId,
607
+ });
608
+ if (!won)
609
+ return undefined;
610
+ }
611
+ catch {
612
+ return undefined;
613
+ }
614
+ flipped.rev = live.rev + 1;
615
+ return flipped;
616
+ };
617
+ let flipped;
618
+ if (lane !== undefined && !lane.poisoned) {
619
+ const p = lane.chain.then(doFlip, doFlip);
620
+ lane.chain = p.then((f) => {
621
+ if (f !== undefined) {
622
+ lane.record = structuredClone(f);
623
+ lane.written = true;
624
+ }
625
+ });
626
+ flipped = await p;
627
+ }
628
+ else {
629
+ flipped = await doFlip();
630
+ }
631
+ if (flipped === undefined)
632
+ return false;
633
+ handle.status = "running";
634
+ handle.parkedCheckpointToken = undefined;
635
+ handle.resolveParkedStop = undefined;
636
+ handle.updatedAt = Date.now();
637
+ handle.completionId = undefined;
638
+ if (flipped.seq !== undefined)
639
+ handle.cycleSeq = flipped.seq;
640
+ return true;
641
+ }
642
+ export function finalizeParkedResumeLane(core, id) {
643
+ const handle = core.handles.get(id);
644
+ if (!handle || handle.type !== "background_agent")
645
+ return;
646
+ durableAgentWriteLane(handle, {}, ["parkClaimId"]);
647
+ }
648
+ export function settleBackgroundAgentLane(core, id, outcome) {
649
+ const handle = core.handles.get(id);
650
+ if (!handle || handle.type !== "background_agent")
651
+ return undefined;
652
+ if (handle.status !== "running") {
653
+ if (handle.status === "killed") {
654
+ mintCompletionId(handle);
655
+ let backfilled = false;
656
+ if (handle.result === undefined && outcome.result !== undefined && outcome.result !== "") {
657
+ handle.result = outcome.result;
658
+ handle.resultIsPartial = true;
659
+ backfilled = true;
660
+ if (outcome.resultFull !== undefined)
661
+ handle.resultFull = outcome.resultFull;
662
+ }
663
+ if (handle.error === undefined && outcome.error !== undefined && outcome.error !== BG_AGENT_REAP_STOP_ERROR) {
664
+ handle.error = outcome.error;
665
+ backfilled = true;
666
+ }
667
+ if (backfilled) {
668
+ handle.updatedAt = Date.now();
669
+ durableAgentWriteLane(handle, {
670
+ ...(handle.result !== undefined ? { finalOutput: handle.result } : {}),
671
+ ...(handle.resultFull !== undefined ? { finalOutputFull: handle.resultFull } : {}),
672
+ ...(handle.resultIsPartial ? { resultIsPartial: true } : {}),
673
+ ...(handle.error !== undefined ? { error: handle.error } : {}),
674
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
675
+ });
676
+ }
677
+ }
678
+ return handle.status === "completed" || handle.status === "failed" || handle.status === "killed" ? handle.status : undefined;
679
+ }
680
+ handle.status = outcome.status;
681
+ mintCompletionId(handle);
682
+ handle.notify = undefined;
683
+ for (const [, , qResolve] of handle.preAttachQueue ?? [])
684
+ qResolve?.({ ok: false, reason: "not_running" });
685
+ handle.preAttachQueue = undefined;
686
+ for (const r of [...(handle.inFlightDirect ?? [])])
687
+ r({ ok: false, reason: "not_running" });
688
+ handle.inFlightDirect = undefined;
689
+ if (outcome.status === "killed" && handle.stoppedBy === undefined) {
690
+ handle.stoppedBy = outcome.stoppedBy ?? handle.stopSource ?? "system";
691
+ }
692
+ if (outcome.result !== undefined)
693
+ handle.result = outcome.result;
694
+ if (outcome.resultFull !== undefined)
695
+ handle.resultFull = outcome.resultFull;
696
+ if (outcome.status === "killed" && outcome.result !== undefined && outcome.result !== "") {
697
+ handle.resultIsPartial = true;
698
+ }
699
+ if (outcome.error !== undefined && !(outcome.status === "killed" && outcome.error === BG_AGENT_REAP_STOP_ERROR)) {
700
+ handle.error = outcome.error;
701
+ }
702
+ handle.updatedAt = Date.now();
703
+ if (outcome.seq !== undefined)
704
+ handle.cycleSeq = outcome.seq;
705
+ core.pokeBgQuiescence(handle.owner);
706
+ durableAgentWriteLane(handle, {
707
+ status: outcome.status,
708
+ ...(outcome.seq !== undefined ? { seq: outcome.seq } : {}),
709
+ settledAt: Date.now(),
710
+ ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
711
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
712
+ ...(handle.result !== undefined ? { finalOutput: handle.result } : {}),
713
+ ...(handle.resultFull !== undefined ? { finalOutputFull: handle.resultFull } : {}),
714
+ ...(handle.resultIsPartial ? { resultIsPartial: true } : {}),
715
+ ...(handle.error !== undefined ? { error: handle.error } : {}),
716
+ }, ["parkedCheckpointToken", "parkClaimId", "parkedAt"]);
717
+ return outcome.status;
718
+ }
719
+ export function abortBackgroundAgentsForOwnerLane(core, access, opts) {
720
+ let aborted = 0;
721
+ for (const handle of core.handles.values()) {
722
+ if (handle.type !== "background_agent" || handle.status !== "running" || !canAccess(handle, access))
723
+ continue;
724
+ if (opts?.skipSessionScoped && handle.sessionScoped)
725
+ continue;
726
+ if (opts?.sessionScopedOnly && !handle.sessionScoped)
727
+ continue;
728
+ core.markStopSource(handle.id, "parent");
729
+ handle.abort.abort();
730
+ aborted++;
731
+ }
732
+ return aborted;
733
+ }
734
+ export function resolveBackgroundAgentByNameLane(core, name, access, opts) {
735
+ const key = normalizeAgentName(name);
736
+ if (key === "")
737
+ return { status: "not_found" };
738
+ const latestOf = (pool) => {
739
+ let best;
740
+ for (const h of pool)
741
+ if (best === undefined || h.createdAt >= best.createdAt)
742
+ best = h;
743
+ return best;
744
+ };
745
+ const named = [];
746
+ for (const h of core.handles.values()) {
747
+ if (h.type !== "background_agent" || h.name === undefined || !canAccess(h, access))
748
+ continue;
749
+ if (h.name === name || normalizeAgentName(h.name) === key)
750
+ named.push(h);
751
+ }
752
+ if (opts?.preferRunning === true) {
753
+ const namedRunning = named.filter((h) => h.status === "running");
754
+ if (namedRunning.length > 1) {
755
+ return {
756
+ status: "ambiguous",
757
+ message: `Multiple RUNNING background agents match "${name}": ${namedRunning.map((h) => `${h.id} (${h.name ?? h.description ?? "unnamed"})`).join(", ")}. Use the task ID.`,
758
+ };
759
+ }
760
+ if (namedRunning.length === 1)
761
+ return { status: "found", handle: namedRunning[0] };
762
+ }
763
+ const namedWinner = latestOf(named);
764
+ if (namedWinner !== undefined)
765
+ return { status: "found", handle: namedWinner };
766
+ const exact = [];
767
+ for (const h of core.handles.values()) {
768
+ if (h.type !== "background_agent" || !canAccess(h, access))
769
+ continue;
770
+ if (h.description === name)
771
+ exact.push(h);
772
+ }
773
+ const exactRunning = exact.filter((h) => h.status === "running");
774
+ const exactPool = exactRunning.length > 0 ? exactRunning : exact;
775
+ if (exactPool.length === 1)
776
+ return { status: "found", handle: exactPool[0] };
777
+ if (exactPool.length > 1) {
778
+ return {
779
+ status: "ambiguous",
780
+ message: `Multiple background agents match "${name}": ${exactPool.map((h) => (h.description ? `${h.id} (${h.description})` : h.id)).join(", ")}. Use the task ID.`,
781
+ };
782
+ }
783
+ const matches = [];
784
+ for (const h of core.handles.values()) {
785
+ if (h.type !== "background_agent" || !canAccess(h, access))
786
+ continue;
787
+ if (h.description !== undefined && normalizeAgentName(h.description) === key)
788
+ matches.push(h);
789
+ }
790
+ const running = matches.filter((h) => h.status === "running");
791
+ const pool = running.length > 0 ? running : matches;
792
+ if (pool.length === 1)
793
+ return { status: "found", handle: pool[0] };
794
+ if (pool.length > 1) {
795
+ return {
796
+ status: "ambiguous",
797
+ message: `Multiple background agents match "${name}": ${pool.map((h) => (h.description ? `${h.id} (${h.description})` : h.id)).join(", ")}. Use the task ID.`,
798
+ };
799
+ }
800
+ const byKey = new Map();
801
+ for (const h of core.handles.values()) {
802
+ if (h.type !== "background_agent" || h.status !== "running" || !canAccess(h, access))
803
+ continue;
804
+ for (const label of [h.name, h.description]) {
805
+ if (label === undefined)
806
+ continue;
807
+ const k = normalizeAgentName(label);
808
+ if (k !== "" && !byKey.has(k))
809
+ byKey.set(k, label);
810
+ }
811
+ }
812
+ const closest = closestName(key, byKey.keys());
813
+ return { status: "not_found", ...(closest !== undefined ? { suggestion: byKey.get(closest) } : {}) };
814
+ }
815
+ export function markRetainedContinuationLane(core, id) {
816
+ const handle = core.handles.get(id);
817
+ if (handle && handle.type === "background_agent")
818
+ handle.retainedContinuation = true;
819
+ }
820
+ export function reviveBackgroundAgentLane(core, id, access, abort) {
821
+ if (core.reapingHandles.has(id) || core.claimingHandles.has(id))
822
+ return { ok: false, reason: "not_found" };
823
+ const handle = core.handles.get(id);
824
+ if (!handle || handle.type !== "background_agent" || !canAccess(handle, access))
825
+ return { ok: false, reason: "not_found" };
826
+ if (handle.status === "running" || handle.status === "pending")
827
+ return { ok: false, reason: "still_running" };
828
+ if (handle.status === "parked")
829
+ return { ok: false, reason: "still_running" };
830
+ handle.status = "running";
831
+ handle.channelState = "attaching";
832
+ handle.notify = undefined;
833
+ if (abort !== undefined)
834
+ handle.abort = abort;
835
+ handle.result = undefined;
836
+ handle.resultFull = undefined;
837
+ handle.error = undefined;
838
+ handle.resultIsPartial = undefined;
839
+ handle.stopSource = undefined;
840
+ handle.completionId = undefined;
841
+ handle.reviveCycle = (handle.reviveCycle ?? 0) + 1;
842
+ handle.cycleSeq = (handle.cycleSeq ?? 1) + 1;
843
+ handle.updatedAt = Date.now();
844
+ durableAgentWriteLane(handle, { status: "running" }, ["settledAt", "stoppedBy", "finalOutput", "finalOutputFull", "error", "resultIsPartial", "completionId", "summary", "recentSteps", "editedFiles", "usage"]);
845
+ return { ok: true, cycle: handle.reviveCycle };
846
+ }
847
+ export function settleRevivedAgentLane(core, id, cycle, outcome) {
848
+ const handle = core.handles.get(id);
849
+ if (!handle || handle.type !== "background_agent")
850
+ return undefined;
851
+ if ((handle.reviveCycle ?? 0) !== cycle)
852
+ return undefined;
853
+ return settleBackgroundAgentLane(core, id, outcome);
854
+ }
855
+ export function unmarkRetainedContinuationLane(core, id) {
856
+ const handle = core.handles.get(id);
857
+ if (handle && handle.type === "background_agent")
858
+ handle.retainedContinuation = false;
859
+ }
860
+ export function attachAgentNotifyLane(core, id, notify, cycle) {
861
+ const handle = core.handles.get(id);
862
+ if (!handle || handle.type !== "background_agent" || handle.status !== "running")
863
+ return;
864
+ if (cycle !== undefined && (handle.reviveCycle ?? 0) !== cycle)
865
+ return;
866
+ handle.notify = notify;
867
+ handle.channelState = "attaching";
868
+ const live = handle.preAttachQueue;
869
+ if (live !== undefined && live.length > 0) {
870
+ void (async () => {
871
+ while (handle.preAttachQueue === live && live.length > 0) {
872
+ const [qPayload, qOpts, qResolve] = live[0];
873
+ let result;
874
+ try {
875
+ const d = await notify(qPayload, qOpts);
876
+ result =
877
+ d === "parked" && handle.retainedContinuation !== true
878
+ ? { ok: false, reason: "not_running" }
879
+ : d === "parked"
880
+ ? { ok: true, disposition: "parked" }
881
+ : { ok: true, disposition: "queued" };
882
+ }
883
+ catch {
884
+ result = { ok: false, reason: "not_running" };
885
+ }
886
+ if (handle.preAttachQueue !== live)
887
+ return;
888
+ live.shift();
889
+ qResolve?.(result);
890
+ }
891
+ if (handle.preAttachQueue === live) {
892
+ handle.preAttachQueue = undefined;
893
+ handle.channelState = "attached";
894
+ }
895
+ })();
896
+ }
897
+ else {
898
+ handle.preAttachQueue = undefined;
899
+ handle.channelState = "attached";
900
+ }
901
+ }
902
+ export async function deliverToRunningAgentLane(core, id, access, notification, opts) {
903
+ const handle = core.handles.get(id);
904
+ if (!handle || handle.type !== "background_agent" || !canAccess(handle, access))
905
+ return { ok: false, reason: "not_found" };
906
+ if (handle.status !== "running")
907
+ return { ok: false, reason: "not_running" };
908
+ const bounded = (p, fallback, onTimeout) => new Promise((resolve) => {
909
+ const t = setTimeout(() => {
910
+ try {
911
+ onTimeout?.();
912
+ }
913
+ catch {
914
+ }
915
+ resolve(fallback);
916
+ }, core.deliverConfirmTimeoutMs);
917
+ t.unref?.();
918
+ void p.then((v) => {
919
+ clearTimeout(t);
920
+ resolve(v);
921
+ }, () => {
922
+ clearTimeout(t);
923
+ resolve(fallback);
924
+ });
925
+ });
926
+ if (handle.notify === undefined || handle.channelState === "attaching") {
927
+ if (handle.channelState !== "attaching")
928
+ return { ok: false, reason: "no_channel" };
929
+ const q = (handle.preAttachQueue ??= []);
930
+ if (q.length >= 8)
931
+ return { ok: false, reason: "no_channel" };
932
+ return bounded(new Promise((resolve) => {
933
+ q.push([notification, opts, resolve]);
934
+ }), { ok: true, disposition: "pending" });
935
+ }
936
+ let detachOnTimeout;
937
+ return await bounded(new Promise((resolve) => {
938
+ const inflight = (handle.inFlightDirect ??= new Set());
939
+ const settleOnce = (r) => {
940
+ if (!inflight.has(settleOnce))
941
+ return;
942
+ inflight.delete(settleOnce);
943
+ resolve(r);
944
+ };
945
+ inflight.add(settleOnce);
946
+ detachOnTimeout = () => inflight.delete(settleOnce);
947
+ void (async () => {
948
+ let result;
949
+ try {
950
+ const d = await handle.notify?.(notification, opts);
951
+ if (d === "parked") {
952
+ result = handle.retainedContinuation === true ? { ok: true, disposition: "parked" } : { ok: false, reason: "not_running" };
953
+ }
954
+ else {
955
+ result = { ok: true, disposition: "queued" };
956
+ }
957
+ }
958
+ catch {
959
+ result = { ok: false, reason: "not_running" };
960
+ }
961
+ settleOnce(result);
962
+ })();
963
+ }), { ok: true, disposition: "pending" }, () => detachOnTimeout?.());
964
+ }
965
+ export function runningBackgroundAgentLabelsLane(core, access) {
966
+ const out = [];
967
+ for (const h of core.handles.values()) {
968
+ if (h.type !== "background_agent" || h.status !== "running" || !canAccess(h, access))
969
+ continue;
970
+ const label = h.name ?? h.description;
971
+ out.push(label ? `${h.id} (${label})` : h.id);
972
+ }
973
+ return out;
974
+ }
975
+ export function serveDurableAgentRowLane(row) {
976
+ if (row.status === "parked") {
977
+ return {
978
+ content: delimitUntrusted(`TaskOutput ${row.handle}`, `status: parked
979
+ The agent is durably suspended, waiting for an approval decision. It resumes when the pending approval is decided (durable approval inbox), or lands failed if the approval expires.`),
980
+ details: {
981
+ task_id: row.handle,
982
+ type: "background_agent",
983
+ status: "parked",
984
+ retrieval_status: "success",
985
+ ...(row.seq !== undefined ? { seq: row.seq } : {}),
986
+ },
987
+ };
988
+ }
989
+ const body = `status: ${row.status}
990
+ ${row.error ? `error: ${row.error}
991
+ ` : ""}${row.finalOutput ? `--- result${row.resultIsPartial ? " (partial — produced before the task was stopped)" : ""} ---
992
+ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}`;
993
+ return {
994
+ content: delimitUntrusted(`TaskOutput ${row.handle}`, body),
995
+ details: {
996
+ task_id: row.handle,
997
+ type: "background_agent",
998
+ status: row.status,
999
+ retrieval_status: "success",
1000
+ ...(row.status === "killed" && row.stoppedBy !== undefined ? { stoppedBy: row.stoppedBy } : {}),
1001
+ ...(row.seq !== undefined ? { seq: row.seq } : {}),
1002
+ ...(row.resultIsPartial ? { partial_result: true } : {}),
1003
+ ...(row.completionId !== undefined ? { completionId: row.completionId } : {}),
1004
+ },
1005
+ };
1006
+ }
1007
+ export async function pollBackgroundAgentLane(handle, deadline, signal) {
1008
+ while (handle.status === "running" && deadline !== undefined && Date.now() < deadline && !signal?.aborted) {
1009
+ await sleepPollStep(deadline, signal);
1010
+ }
1011
+ const running = handle.status === "running";
1012
+ const retrieval = running && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success";
1013
+ if (handle.status === "parked") {
1014
+ return {
1015
+ content: delimitUntrusted(`TaskOutput ${handle.id}`, `status: parked
1016
+ The agent is durably suspended, waiting for an approval decision. It resumes when the pending approval is decided (durable approval inbox), or lands failed if the approval expires.`),
1017
+ details: {
1018
+ task_id: handle.id,
1019
+ type: "background_agent",
1020
+ status: "parked",
1021
+ retrieval_status: "success",
1022
+ ...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
1023
+ },
1024
+ };
1025
+ }
1026
+ const body = running
1027
+ ? `status: running
1028
+ The agent is still working — you will be notified when it completes.`
1029
+ : `status: ${handle.status}
1030
+ ${handle.error ? `error: ${handle.error}
1031
+ ` : ""}${handle.result ? `--- result${handle.resultIsPartial ? " (partial — produced before the task was stopped)" : ""} ---
1032
+ ${clipTaskOutput(handle.resultFull ?? handle.result, handle.outputFile)}` : "(no result text)"}`;
1033
+ return {
1034
+ content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
1035
+ details: {
1036
+ task_id: handle.id,
1037
+ type: "background_agent",
1038
+ status: handle.status,
1039
+ retrieval_status: retrieval,
1040
+ ...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
1041
+ ...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
1042
+ ...(handle.resultIsPartial ? { partial_result: true } : {}),
1043
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1044
+ },
1045
+ };
1046
+ }
1047
+ export async function stopBackgroundAgentLane(core, handle) {
1048
+ const entryStatus = handle.status;
1049
+ if (handle.status === "parked") {
1050
+ const arbiter = handle.resolveParkedStop;
1051
+ if (arbiter === undefined) {
1052
+ return {
1053
+ content: `${handle.id} is parked on a pending approval — stop it by deciding (deny/expire) the pending approval in the durable approval inbox.`,
1054
+ details: { task_id: handle.id, type: "background_agent", status: "parked", retrieval_status: "not_ready", error: "parked_pending_approval" },
1055
+ };
1056
+ }
1057
+ let stopWon = false;
1058
+ let arbiterUnreachable = false;
1059
+ try {
1060
+ stopWon = await arbiter();
1061
+ }
1062
+ catch {
1063
+ stopWon = false;
1064
+ arbiterUnreachable = true;
1065
+ }
1066
+ if (stopWon && handle.status === "parked") {
1067
+ core.markStopSource(handle.id, "parent");
1068
+ handle.status = "killed";
1069
+ handle.stoppedBy = handle.stopSource ?? "parent";
1070
+ handle.updatedAt = Date.now();
1071
+ mintCompletionId(handle);
1072
+ handle.parkedCheckpointToken = undefined;
1073
+ handle.resolveParkedStop = undefined;
1074
+ core.pokeBgQuiescence(handle.owner);
1075
+ {
1076
+ const lane = handle.durable;
1077
+ if (lane !== undefined) {
1078
+ try {
1079
+ const live = await lane.store.get(handle.id, lane.record.scope);
1080
+ if (live && (live.status === "parked" || live.status === "running")) {
1081
+ const term = structuredClone(live);
1082
+ term.status = "killed";
1083
+ if (handle.stoppedBy !== undefined)
1084
+ term.stoppedBy = handle.stoppedBy;
1085
+ term.settledAt = Date.now();
1086
+ term.updatedAt = Date.now();
1087
+ delete term.parkedCheckpointToken;
1088
+ delete term.parkClaimId;
1089
+ delete term.parkedAt;
1090
+ await lane.store.update(handle.id, lane.record.scope, term, { rev: live.rev });
1091
+ }
1092
+ }
1093
+ catch {
1094
+ }
1095
+ }
1096
+ }
1097
+ durableAgentWriteLane(handle, {
1098
+ status: "killed",
1099
+ settledAt: Date.now(),
1100
+ ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
1101
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1102
+ }, ["parkedCheckpointToken", "parkClaimId"]);
1103
+ return {
1104
+ content: `Terminated ${handle.id} (pending approval expired).`,
1105
+ details: {
1106
+ task_id: handle.id,
1107
+ type: "background_agent",
1108
+ status: "killed",
1109
+ retrieval_status: "success",
1110
+ ...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
1111
+ ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
1112
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1113
+ },
1114
+ };
1115
+ }
1116
+ if (arbiterUnreachable) {
1117
+ return {
1118
+ content: `${handle.id} was not stopped: the approval arbitration store was unreachable — the row stays parked; retry the stop.`,
1119
+ details: { task_id: handle.id, type: "background_agent", status: handle.status, retrieval_status: "not_ready", error: "park_arbiter_unreachable" },
1120
+ isError: true,
1121
+ };
1122
+ }
1123
+ return {
1124
+ content: `${handle.id} was not stopped: its pending approval was already consumed (a resume won the arbitration).`,
1125
+ details: { task_id: handle.id, type: "background_agent", status: handle.status, retrieval_status: "not_ready", error: "park_resume_won" },
1126
+ isError: true,
1127
+ };
1128
+ }
1129
+ if (handle.status === "running") {
1130
+ core.markStopSource(handle.id, "parent");
1131
+ handle.abort.abort();
1132
+ handle.status = "killed";
1133
+ handle.stoppedBy = handle.stopSource ?? "parent";
1134
+ handle.updatedAt = Date.now();
1135
+ mintCompletionId(handle);
1136
+ handle.notify = undefined;
1137
+ for (const [, , qResolve] of handle.preAttachQueue ?? [])
1138
+ qResolve?.({ ok: false, reason: "not_running" });
1139
+ handle.preAttachQueue = undefined;
1140
+ for (const r of [...(handle.inFlightDirect ?? [])])
1141
+ r({ ok: false, reason: "not_running" });
1142
+ handle.inFlightDirect = undefined;
1143
+ core.pokeBgQuiescence(handle.owner);
1144
+ durableAgentWriteLane(handle, {
1145
+ status: "killed",
1146
+ settledAt: Date.now(),
1147
+ ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
1148
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1149
+ });
1150
+ }
1151
+ return {
1152
+ content: entryStatus === "running" ? `Terminated ${handle.id}.` : alreadyTerminalStopNote(handle.id, handle.status),
1153
+ details: {
1154
+ task_id: handle.id,
1155
+ type: "background_agent",
1156
+ status: handle.status,
1157
+ retrieval_status: "success",
1158
+ ...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
1159
+ ...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
1160
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1161
+ },
1162
+ };
1163
+ }