mixdog 0.9.86 → 0.9.87
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.
- package/package.json +2 -2
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +4 -2
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +125 -16
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -3
- package/src/runtime/agent/orchestrator/tools/token-manifest.json +23 -2
- package/src/runtime/memory/lib/memory-action-handlers.mjs +4 -1
- package/src/runtime/memory/tool-defs.mjs +19 -3
- package/src/session-runtime/lifecycle-api.mjs +15 -0
- package/src/session-runtime/provider-auth-api.mjs +8 -0
- package/src/session-runtime/provider-usage.mjs +30 -5
- package/src/session-runtime/runtime-core.mjs +267 -481
- package/src/session-runtime/session-lifecycle.mjs +378 -0
- package/src/standalone/agent-shard/shard-pool.mjs +6 -5
- package/src/standalone/agent-tool/job-views.mjs +329 -0
- package/src/standalone/agent-tool/spawn-flow.mjs +629 -0
- package/src/standalone/agent-tool/tag-registry.mjs +340 -0
- package/src/standalone/agent-tool.mjs +100 -1151
- package/src/standalone/usage-dashboard.mjs +23 -1
- package/src/tui/App.jsx +362 -2576
- package/src/tui/app/app-view.jsx +504 -0
- package/src/tui/app/create-app-pickers.mjs +313 -0
- package/src/tui/app/prompt-submit.mjs +501 -0
- package/src/tui/app/shell-layout.mjs +563 -0
- package/src/tui/app/usage-context-panels.mjs +288 -0
- package/src/tui/app/use-copy-selection.mjs +56 -0
- package/src/tui/app/use-global-key-input.mjs +151 -0
- package/src/tui/app/use-pasted-buffers.mjs +115 -0
- package/src/tui/app/use-prompt-draft-flow.mjs +167 -0
- package/src/tui/app/use-prompt-hint.mjs +58 -0
- package/src/tui/app/use-prompt-queue-history.mjs +100 -0
- package/src/tui/app/use-terminal-chrome.mjs +72 -0
- package/src/tui/app/use-transcript-activity.mjs +131 -0
- package/src/tui/app/use-welcome-prompt-hint.mjs +96 -0
- package/src/tui/dist/index.mjs +9802 -9024
- package/src/tui/engine/session-api-ext.mjs +23 -0
|
@@ -49,6 +49,9 @@ import {
|
|
|
49
49
|
import { abnormalEmptyFinishError, renderResult } from './agent-tool/render.mjs';
|
|
50
50
|
import { createProviderInit } from './agent-tool/provider-init.mjs';
|
|
51
51
|
import { createNotify } from './agent-tool/notify.mjs';
|
|
52
|
+
import { createTagRegistry } from './agent-tool/tag-registry.mjs';
|
|
53
|
+
import { createJobViews } from './agent-tool/job-views.mjs';
|
|
54
|
+
import { createSpawnFlow } from './agent-tool/spawn-flow.mjs';
|
|
52
55
|
import { resolveAgentSpawnPreset } from './agent-tool/spawn-preset.mjs';
|
|
53
56
|
import {
|
|
54
57
|
TAG_TOMBSTONE_TTL_MS,
|
|
@@ -116,1162 +119,108 @@ export function createStandaloneAgent({
|
|
|
116
119
|
},
|
|
117
120
|
};
|
|
118
121
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
.filter((entry) => entry.tag === value);
|
|
156
|
-
if (matches.length === 1) return matches[0].session.id;
|
|
157
|
-
if (matches.length > 1) {
|
|
158
|
-
throw new Error(`agent: tag "${value}" is ambiguous across terminals; use sessionId`);
|
|
159
|
-
}
|
|
160
|
-
const sessionId = tags.get(value) || null;
|
|
161
|
-
const session = getLiveSession(sessionId);
|
|
162
|
-
return session && sessionMatchesContext(session, context) ? sessionId : null;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function getLiveSession(sessionId) {
|
|
166
|
-
if (!sessionId) return null;
|
|
167
|
-
const session = mgr.getSession(sessionId);
|
|
168
|
-
return session && session.closed !== true ? session : null;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function tagForSession(sessionId) {
|
|
172
|
-
const session = getLiveSession(sessionId);
|
|
173
|
-
const persistedTag = agentTagOf(session);
|
|
174
|
-
if (persistedTag) return persistedTag;
|
|
175
|
-
for (const [tag, sid] of tags.entries()) {
|
|
176
|
-
if (sid === sessionId) return tag;
|
|
177
|
-
}
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function agentSessionEntries({ scanSessions = false, context = {}, excludeTerminalTraces = false } = {}) {
|
|
182
|
-
const rows = [];
|
|
183
|
-
const seen = new Set();
|
|
184
|
-
const add = (session, fallbackTag = '') => {
|
|
185
|
-
const tag = agentTagOf(session) || clean(fallbackTag);
|
|
186
|
-
if (!tag || !session?.id || session.closed === true) return;
|
|
187
|
-
if (!sessionMatchesContext(session, context)) return;
|
|
188
|
-
if (seen.has(session.id)) return;
|
|
189
|
-
seen.add(session.id);
|
|
190
|
-
rows.push({ tag, session });
|
|
191
|
-
};
|
|
192
|
-
const addIndexRow = (row) => {
|
|
193
|
-
const tag = clean(row?.tag);
|
|
194
|
-
const sessionId = clean(row?.sessionId);
|
|
195
|
-
if (!tag || !sessionId || !rowMatchesContext(row, context)) return;
|
|
196
|
-
if (seen.has(sessionId)) return;
|
|
197
|
-
// Collision/resolution enumeration only: a row that is in a terminal
|
|
198
|
-
// (or idle-but-finished) state AND has no live session behind it is a
|
|
199
|
-
// lingering trace kept for the reap grace window. excludeTerminalTraces drops those
|
|
200
|
-
// rows so live-session reuse/spawn resolution can proceed; list/status
|
|
201
|
-
// keep excludeTerminalTraces=false so finished workers still appear.
|
|
202
|
-
if (excludeTerminalTraces
|
|
203
|
-
&& isTerminalWorkerStatus(row.status || row.stage)
|
|
204
|
-
&& !getLiveSession(sessionId)) {
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
seen.add(sessionId);
|
|
208
|
-
rows.push({ tag, session: workerRowToSession(row), indexRow: row });
|
|
209
|
-
};
|
|
210
|
-
for (const row of readWorkerRows(context)) addIndexRow(row);
|
|
211
|
-
if (scanSessions) {
|
|
212
|
-
for (const session of mgr.listSessions({ includeClosed: false }) || []) {
|
|
213
|
-
const tag = agentTagOf(session);
|
|
214
|
-
add(session, tag);
|
|
215
|
-
if (tag) upsertWorkerSessionDeferred(session, tag);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
for (const [tag, sessionId] of tags.entries()) {
|
|
219
|
-
add(getLiveSession(sessionId), tag);
|
|
220
|
-
}
|
|
221
|
-
return rows;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function nextTag(agent, context = {}) {
|
|
225
|
-
refreshTagsFromSessions({ context });
|
|
226
|
-
// Auto tags are agent + a per-agent local index with NO hyphen
|
|
227
|
-
// ("worker3", "heavy-worker7", or "agent1" when the agent is unset). The
|
|
228
|
-
// index is the max existing `^agent(\d+)$` + 1, escaping the agent so a
|
|
229
|
-
// hyphenated agent ("heavy-worker") is matched literally. Keep incrementing
|
|
230
|
-
// on any live collision.
|
|
231
|
-
const base = clean(agent) || 'agent';
|
|
232
|
-
const escaped = base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
233
|
-
const re = new RegExp(`^${escaped}(\\d+)$`);
|
|
234
|
-
let maxN = 0;
|
|
235
|
-
for (const existing of tags.keys()) {
|
|
236
|
-
const match = re.exec(existing);
|
|
237
|
-
if (!match) continue;
|
|
238
|
-
const n = Number(match[1]);
|
|
239
|
-
if (Number.isFinite(n) && n > maxN) maxN = n;
|
|
240
|
-
}
|
|
241
|
-
let n = maxN + 1;
|
|
242
|
-
let tag = `${base}${n}`;
|
|
243
|
-
while (resolveTag(tag, context)) tag = `${base}${++n}`;
|
|
244
|
-
return tag;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function refreshTagsFromSessions({ scanSessions = false, context = {} } = {}) {
|
|
248
|
-
transitionStaleNonterminalRows(context);
|
|
249
|
-
const indexedRows = refreshTagsFromIndex(context);
|
|
250
|
-
const indexedKeys = new Set(indexedRows.map((row) => `${row.tag}\0${row.sessionId}`));
|
|
251
|
-
for (const [tag, sessionId] of [...tags.entries()]) {
|
|
252
|
-
if (indexedKeys.has(`${tag}\0${sessionId}`)) continue;
|
|
253
|
-
const session = getLiveSession(sessionId);
|
|
254
|
-
if (!session || session.closed) {
|
|
255
|
-
tags.delete(tag);
|
|
256
|
-
tagAgents.delete(tag);
|
|
257
|
-
tagCwds.delete(tag);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
if (!scanSessions) return;
|
|
261
|
-
for (const session of mgr.listSessions({ includeClosed: false }) || []) {
|
|
262
|
-
const tag = agentTagOf(session);
|
|
263
|
-
if (!tag || tags.has(tag)) continue;
|
|
264
|
-
if (!sessionMatchesContext(session, context)) continue;
|
|
265
|
-
tags.set(tag, session.id);
|
|
266
|
-
if (session.agent) tagAgents.set(tag, session.agent);
|
|
267
|
-
if (session.cwd) tagCwds.set(tag, session.cwd);
|
|
268
|
-
upsertWorkerSessionDeferred(session, tag);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function bindTag(tag, session, extra = {}) {
|
|
273
|
-
if (!tag || !session?.id) return;
|
|
274
|
-
tags.set(tag, session.id);
|
|
275
|
-
if (session.agent) tagAgents.set(tag, session.agent);
|
|
276
|
-
if (session.cwd) tagCwds.set(tag, session.cwd);
|
|
277
|
-
upsertWorkerSessionDeferred(session, tag, extra);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function forgetTag(tag) {
|
|
281
|
-
if (!tag) return;
|
|
282
|
-
const sessionId = tags.get(tag) || '';
|
|
283
|
-
tags.delete(tag);
|
|
284
|
-
tagAgents.delete(tag);
|
|
285
|
-
tagCwds.delete(tag);
|
|
286
|
-
removeWorkerRow({ tag, sessionId });
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function forgetTerminalSession(tag, sessionId) {
|
|
290
|
-
const value = clean(tag);
|
|
291
|
-
const id = clean(sessionId);
|
|
292
|
-
if (value && id && tags.get(value) === id) {
|
|
293
|
-
tags.delete(value);
|
|
294
|
-
tagAgents.delete(value);
|
|
295
|
-
tagCwds.delete(value);
|
|
296
|
-
}
|
|
297
|
-
if (id) removeWorkerRow({ sessionId: id });
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function tombstoneTerminalSession(tag, sessionId, session = null) {
|
|
301
|
-
const value = clean(tag);
|
|
302
|
-
const id = clean(sessionId);
|
|
303
|
-
if (!value || !id) {
|
|
304
|
-
forgetTerminalSession(value, id);
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
if (tags.get(value) === id) {
|
|
308
|
-
tags.delete(value);
|
|
309
|
-
tagAgents.delete(value);
|
|
310
|
-
tagCwds.delete(value);
|
|
311
|
-
}
|
|
312
|
-
const tombstone = {
|
|
313
|
-
tag: value,
|
|
314
|
-
agent: clean(session?.agent) || null,
|
|
315
|
-
cwd: clean(session?.cwd) || null,
|
|
316
|
-
clientHostPid: positiveInt(session?.clientHostPid),
|
|
317
|
-
reapedAt: new Date().toISOString(),
|
|
318
|
-
};
|
|
319
|
-
flushWorkerIndexMutations();
|
|
320
|
-
writeWorkerRows((byKey, tombstonesByKey, priorityTombstoneKeys) => {
|
|
321
|
-
for (const [key, row] of [...byKey.entries()]) {
|
|
322
|
-
if (clean(row.sessionId) === id) byKey.delete(key);
|
|
323
|
-
}
|
|
324
|
-
const tombstoneKey = tagTombstoneKey(tombstone);
|
|
325
|
-
tombstonesByKey.set(tombstoneKey, tombstone);
|
|
326
|
-
priorityTombstoneKeys.add(tombstoneKey);
|
|
122
|
+
// Tag maps + resolve/bind/reap lifecycle: agent-tool/tag-registry.mjs.
|
|
123
|
+
const {
|
|
124
|
+
tags,
|
|
125
|
+
tagAgents,
|
|
126
|
+
tagCwds,
|
|
127
|
+
reapTimers,
|
|
128
|
+
wantsSessionScan,
|
|
129
|
+
resolveTag,
|
|
130
|
+
getLiveSession,
|
|
131
|
+
tagForSession,
|
|
132
|
+
agentSessionEntries,
|
|
133
|
+
nextTag,
|
|
134
|
+
refreshTagsFromSessions,
|
|
135
|
+
bindTag,
|
|
136
|
+
forgetTag,
|
|
137
|
+
forgetTerminalSession,
|
|
138
|
+
tombstoneTerminalSession,
|
|
139
|
+
tagTombstoneForTag,
|
|
140
|
+
consumeTagTombstone,
|
|
141
|
+
cancelReap,
|
|
142
|
+
scheduleReap,
|
|
143
|
+
transitionStaleNonterminalRows,
|
|
144
|
+
readAllTagTombstones,
|
|
145
|
+
readTagTombstones,
|
|
146
|
+
readWorkerRows,
|
|
147
|
+
writeWorkerRows,
|
|
148
|
+
flushWorkerIndexMutations,
|
|
149
|
+
upsertWorkerSession,
|
|
150
|
+
upsertWorkerSessionDeferred,
|
|
151
|
+
removeWorkerRow,
|
|
152
|
+
refreshTagsFromIndex,
|
|
153
|
+
} = createTagRegistry({
|
|
154
|
+
dataDir,
|
|
155
|
+
cfgMod,
|
|
156
|
+
mgr,
|
|
157
|
+
emitSubagentEvent,
|
|
327
158
|
});
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function tagTombstoneForTag(tag, context = {}) {
|
|
331
|
-
const value = clean(tag);
|
|
332
|
-
if (!value || value.startsWith('sess_')) return null;
|
|
333
|
-
return readTagTombstones(context).find((row) => row.tag === value) || null;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function consumeTagTombstone(tombstone) {
|
|
337
|
-
if (!tombstone?.tag) return false;
|
|
338
|
-
const key = tagTombstoneKey(tombstone);
|
|
339
|
-
flushWorkerIndexMutations();
|
|
340
|
-
writeWorkerRows((_byKey, tombstonesByKey) => tombstonesByKey.delete(key));
|
|
341
|
-
return true;
|
|
342
|
-
}
|
|
343
159
|
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
reapTimers.set(sessionId, handle);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function transitionStaleNonterminalRows(context = {}) {
|
|
372
|
-
const staleRows = readWorkerRows(context).filter((row) => {
|
|
373
|
-
if (isTerminalWorkerStatus(row.status || row.stage)) return false;
|
|
374
|
-
if (getLiveSession(clean(row.sessionId))) return false;
|
|
375
|
-
const rowTime = workerRowTime(row);
|
|
376
|
-
const reapMs = resolveAgentTerminalReapMs(cfgMod.loadConfig(), row.provider);
|
|
377
|
-
// A row with no timestamp has no usable heartbeat at all. Explicitly
|
|
378
|
-
// disabled terminal reaping still gets the tombstone TTL as a finite
|
|
379
|
-
// stale-heartbeat bound, so malformed/running index rows cannot block a
|
|
380
|
-
// tag forever.
|
|
381
|
-
return rowTime <= 0 || Date.now() - rowTime >= (reapMs ?? TAG_TOMBSTONE_TTL_MS);
|
|
160
|
+
// Job/session views (list/getJob/render/spawn-meta): agent-tool/job-views.mjs.
|
|
161
|
+
const {
|
|
162
|
+
isSessionBusy,
|
|
163
|
+
ensureProvider,
|
|
164
|
+
list,
|
|
165
|
+
sessionProgressExtras,
|
|
166
|
+
jobWorkerSnapshot,
|
|
167
|
+
listJobs,
|
|
168
|
+
getJob,
|
|
169
|
+
workerFallbackJob,
|
|
170
|
+
getJobOrWorker,
|
|
171
|
+
renderJob,
|
|
172
|
+
preparedSpawnMeta,
|
|
173
|
+
pendingSpawnMeta,
|
|
174
|
+
mergeJobMeta,
|
|
175
|
+
} = createJobViews({
|
|
176
|
+
mgr,
|
|
177
|
+
getLiveSession,
|
|
178
|
+
reg,
|
|
179
|
+
DEFAULT_SPAWN_PREP_TIMEOUT_MS,
|
|
180
|
+
refreshTagsFromSessions,
|
|
181
|
+
agentSessionEntries,
|
|
182
|
+
tags,
|
|
183
|
+
cfgMod,
|
|
382
184
|
});
|
|
383
|
-
if (staleRows.length === 0) return false;
|
|
384
|
-
flushWorkerIndexMutations();
|
|
385
|
-
const nowIso = new Date().toISOString();
|
|
386
|
-
writeWorkerRows((byKey, tombstonesByKey, priorityTombstoneKeys) => {
|
|
387
|
-
for (const row of staleRows) {
|
|
388
|
-
const sessionId = clean(row.sessionId);
|
|
389
|
-
for (const [key, candidate] of [...byKey.entries()]) {
|
|
390
|
-
if (clean(candidate.sessionId) === sessionId) byKey.delete(key);
|
|
391
|
-
}
|
|
392
|
-
const tombstone = {
|
|
393
|
-
tag: clean(row.tag),
|
|
394
|
-
agent: clean(row.agent) || null,
|
|
395
|
-
cwd: clean(row.cwd) || null,
|
|
396
|
-
clientHostPid: positiveInt(row.clientHostPid),
|
|
397
|
-
reapedAt: nowIso,
|
|
398
|
-
};
|
|
399
|
-
const tombstoneKey = tagTombstoneKey(tombstone);
|
|
400
|
-
tombstonesByKey.set(tombstoneKey, tombstone);
|
|
401
|
-
priorityTombstoneKeys.add(tombstoneKey);
|
|
402
|
-
if (tags.get(tombstone.tag) === sessionId) {
|
|
403
|
-
tags.delete(tombstone.tag);
|
|
404
|
-
tagAgents.delete(tombstone.tag);
|
|
405
|
-
tagCwds.delete(tombstone.tag);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
});
|
|
409
|
-
return true;
|
|
410
|
-
}
|
|
411
185
|
|
|
412
|
-
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
createdAt: session.createdAt || null,
|
|
450
|
-
updatedAt: session.updatedAt || null,
|
|
451
|
-
lastUsedAt: session.lastUsedAt || null,
|
|
452
|
-
clientHostPid: session.clientHostPid || null,
|
|
453
|
-
lastStreamDeltaAt: runtime?.lastStreamDeltaAt ? new Date(runtime.lastStreamDeltaAt).toISOString() : null,
|
|
454
|
-
staleSeconds: runtime?.lastStreamDeltaAt ? Math.floor((now - runtime.lastStreamDeltaAt) / 1000) : null,
|
|
455
|
-
windowTokens: Number(session.lastContextTokens ?? session.lastInputTokens) || 0,
|
|
456
|
-
windowCap: Number(session.contextWindow) || null,
|
|
457
|
-
permission: session.permission || null,
|
|
458
|
-
toolPermission: session.toolPermission || null,
|
|
459
|
-
messages: Array.isArray(session.messages) ? session.messages.length : Math.max(0, Number(session.messageCount || 0)),
|
|
460
|
-
tools: Array.isArray(session.tools) ? session.tools.length : Math.max(0, Number(session.toolCount || 0)),
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
return rows;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function sessionProgressExtras(sessionId, role, now = Date.now(), taskStatus = null) {
|
|
467
|
-
if (!sessionId) return {};
|
|
468
|
-
const session = mgr.getSession(sessionId);
|
|
469
|
-
const runtime = mgr.getSessionRuntime?.(sessionId) || null;
|
|
470
|
-
const snapshot = typeof mgr.getSessionProgressSnapshot === 'function'
|
|
471
|
-
? mgr.getSessionProgressSnapshot(sessionId)
|
|
472
|
-
: null;
|
|
473
|
-
const policy = role ? resolveAgentWatchdogPolicy(role) : null;
|
|
474
|
-
const queuedFollowups = typeof mgr.getSessionPendingMessageDepth === 'function'
|
|
475
|
-
? mgr.getSessionPendingMessageDepth(sessionId)
|
|
476
|
-
: null;
|
|
477
|
-
return buildAgentTaskProgressFields({
|
|
478
|
-
now,
|
|
479
|
-
sessionStatus: session?.status || null,
|
|
480
|
-
runtimeStage: runtime?.stage || snapshot?.stage || session?.status || null,
|
|
481
|
-
snapshot,
|
|
482
|
-
runtime,
|
|
483
|
-
policy,
|
|
484
|
-
queuedFollowups,
|
|
485
|
-
taskStatus,
|
|
486
|
-
lastToolCall: runtime?.lastToolCall || null,
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function jobWorkerSnapshot(sessionId) {
|
|
491
|
-
if (!sessionId) return null;
|
|
492
|
-
const session = mgr.getSession(sessionId);
|
|
493
|
-
if (!session) return null;
|
|
494
|
-
const runtime = mgr.getSessionRuntime?.(sessionId);
|
|
495
|
-
const status = session.closed === true ? 'closed' : (session.status || 'idle');
|
|
496
|
-
const progress = sessionProgressExtras(sessionId, session.agent || null);
|
|
497
|
-
return {
|
|
498
|
-
workerStatus: status,
|
|
499
|
-
stage: progress.worker_stage || runtime?.stage || status,
|
|
500
|
-
clientHostPid: session.clientHostPid || null,
|
|
501
|
-
lastStreamDeltaAt: runtime?.lastStreamDeltaAt ? new Date(runtime.lastStreamDeltaAt).toISOString() : null,
|
|
502
|
-
...progress,
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function listJobs(context = {}) {
|
|
507
|
-
const wantedPid = terminalPidForContext(context);
|
|
508
|
-
const rows = listBackgroundTasks({ surface: 'agent', context }).map((task) => ({
|
|
509
|
-
task_id: task.task_id,
|
|
510
|
-
type: task.operation,
|
|
511
|
-
status: task.status,
|
|
512
|
-
tag: task.tag || null,
|
|
513
|
-
sessionId: task.sessionId || null,
|
|
514
|
-
agent: task.agent || null,
|
|
515
|
-
preset: task.preset || null,
|
|
516
|
-
provider: task.provider || null,
|
|
517
|
-
model: task.model || null,
|
|
518
|
-
effort: task.effort || null,
|
|
519
|
-
fast: task.fast === true || task.fast === false ? task.fast : null,
|
|
520
|
-
maxLoopIterations: task.maxLoopIterations || null,
|
|
521
|
-
startedAt: task.startedAt,
|
|
522
|
-
finishedAt: task.finishedAt || null,
|
|
523
|
-
error: task.error || null,
|
|
524
|
-
...jobWorkerSnapshot(task.sessionId),
|
|
525
|
-
...sessionProgressExtras(task.sessionId, task.agent || null, Date.now(), task.status),
|
|
526
|
-
}));
|
|
527
|
-
return wantedPid
|
|
528
|
-
? rows.filter((row) => {
|
|
529
|
-
const pid = positiveInt(row.clientHostPid);
|
|
530
|
-
// Spawn-prep rows have no worker session (and therefore no pid) yet;
|
|
531
|
-
// they already passed taskMatchesScope, so the queued spawn stays
|
|
532
|
-
// visible instead of vanishing until a pool slot frees up (user bug:
|
|
533
|
-
// "spawned 5, only 4 listed").
|
|
534
|
-
return pid ? pid === wantedPid : true;
|
|
535
|
-
})
|
|
536
|
-
: rows;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
function getJob(args, context = {}) {
|
|
540
|
-
const taskId = taskIdFromArgs(args);
|
|
541
|
-
if (taskId) {
|
|
542
|
-
const task = getBackgroundTask(taskId, { surface: 'agent', context });
|
|
543
|
-
if (!task) throw new Error(`agent read/status: task "${taskId}" not found`);
|
|
544
|
-
return task;
|
|
545
|
-
}
|
|
546
|
-
// Fall back to tag/sessionId resolution, same precedence as close()
|
|
547
|
-
// (agent-tool.mjs close()): clean(args.tag || args.sessionId || ...).
|
|
548
|
-
const target = clean(args.tag || args.sessionId);
|
|
549
|
-
if (!target) throw new Error('agent read/status: task_id, tag, or sessionId is required');
|
|
550
|
-
const candidates = listBackgroundTasks({ surface: 'agent', context })
|
|
551
|
-
.filter(Boolean)
|
|
552
|
-
.filter((row) => row.tag === target || row.sessionId === target);
|
|
553
|
-
if (!candidates.length) throw new Error(`agent read/status: no task found for tag/sessionId "${target}"`);
|
|
554
|
-
// Prefer most recent when multiple tasks match the same tag.
|
|
555
|
-
candidates.sort((a, b) => new Date(b.startedAt || 0) - new Date(a.startedAt || 0));
|
|
556
|
-
const task = getBackgroundTask(candidates[0].task_id, { surface: 'agent', context });
|
|
557
|
-
if (!task) throw new Error(`agent read/status: task "${candidates[0].task_id}" not found`);
|
|
558
|
-
return task;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// Background tasks are process-memory only; a worker SESSION can outlive
|
|
562
|
-
// its spawn task (task pruned, engine restarted). read/status by tag then
|
|
563
|
-
// used to throw "no task found" even though the worker exists. Fall back to
|
|
564
|
-
// a synthetic job view built from the worker session, with the last
|
|
565
|
-
// assistant message as the readable result.
|
|
566
|
-
function workerFallbackJob(target, context = {}) {
|
|
567
|
-
refreshTagsFromSessions({ scanSessions: true, context });
|
|
568
|
-
const sessionId = tags.get(target) || (mgr.getSession(target) ? target : null);
|
|
569
|
-
if (!sessionId) return null;
|
|
570
|
-
const session = mgr.getSession(sessionId);
|
|
571
|
-
if (!session) return null;
|
|
572
|
-
const status = session.closed === true ? 'closed' : (session.status || 'idle');
|
|
573
|
-
const msgs = Array.isArray(session.messages) ? session.messages : [];
|
|
574
|
-
const lastAssistant = [...msgs].reverse().find((m) => m?.role === 'assistant'
|
|
575
|
-
&& (typeof m.content === 'string' ? m.content.trim() : m.content));
|
|
576
|
-
const resultText = lastAssistant
|
|
577
|
-
? (typeof lastAssistant.content === 'string' ? lastAssistant.content : JSON.stringify(lastAssistant.content))
|
|
578
|
-
: '(worker session has no assistant output yet)';
|
|
579
|
-
return {
|
|
580
|
-
taskId: null,
|
|
581
|
-
operation: 'worker',
|
|
582
|
-
status,
|
|
583
|
-
startedAt: session.createdAt || null,
|
|
584
|
-
finishedAt: null,
|
|
585
|
-
error: null,
|
|
586
|
-
result: resultText,
|
|
587
|
-
meta: {
|
|
588
|
-
tag: tags.has(target) ? target : (session.tag || null),
|
|
589
|
-
sessionId,
|
|
590
|
-
agent: session.agent || null,
|
|
591
|
-
preset: session.presetName || null,
|
|
592
|
-
provider: session.provider || null,
|
|
593
|
-
model: session.model || null,
|
|
594
|
-
effort: session.effort || null,
|
|
595
|
-
fast: session.fast === true,
|
|
596
|
-
maxLoopIterations: null,
|
|
597
|
-
},
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function getJobOrWorker(args, context = {}) {
|
|
602
|
-
try {
|
|
603
|
-
return getJob(args, context);
|
|
604
|
-
} catch (error) {
|
|
605
|
-
const target = clean(args.tag || args.sessionId);
|
|
606
|
-
const fallback = target ? workerFallbackJob(target, context) : null;
|
|
607
|
-
if (fallback) return fallback;
|
|
608
|
-
throw error;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
function renderJob(job, includeResult = false) {
|
|
613
|
-
const meta = job.meta || {};
|
|
614
|
-
let progress = sessionProgressExtras(meta.sessionId, meta.agent || null, Date.now(), job.status);
|
|
615
|
-
// Spawn is deferred: before the worker session exists, sessionProgressExtras
|
|
616
|
-
// returns {} and the status card would show only "status: running" with no
|
|
617
|
-
// stage/progress. Fill a minimal stage so the caller can tell the job is
|
|
618
|
-
// still spinning up rather than silently stalled.
|
|
619
|
-
if (!meta.sessionId && (!progress || Object.keys(progress).length === 0)) {
|
|
620
|
-
const spawning = job.status === 'running';
|
|
621
|
-
progress = {
|
|
622
|
-
worker_stage: spawning ? 'spawning' : (job.status || 'unknown'),
|
|
623
|
-
last_progress: spawning ? 'spawning worker session' : (job.status || 'unknown'),
|
|
624
|
-
diagnostic: spawning ? 'worker session not started yet' : (job.status || 'unknown'),
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
return {
|
|
628
|
-
task_id: job.taskId,
|
|
629
|
-
type: job.operation,
|
|
630
|
-
status: job.status,
|
|
631
|
-
tag: meta.tag || null,
|
|
632
|
-
sessionId: meta.sessionId || null,
|
|
633
|
-
agent: meta.agent || null,
|
|
634
|
-
...(meta.respawned === true ? { respawned: true, note: 'previous session reaped — fresh session, no prior context; re-supply anchors if needed' } : {}),
|
|
635
|
-
preset: meta.preset || null,
|
|
636
|
-
provider: meta.provider || null,
|
|
637
|
-
model: meta.model || null,
|
|
638
|
-
effort: meta.effort || null,
|
|
639
|
-
fast: meta.fast === true || meta.fast === false ? meta.fast : null,
|
|
640
|
-
maxLoopIterations: meta.maxLoopIterations || null,
|
|
641
|
-
startedAt: job.startedAt,
|
|
642
|
-
finishedAt: job.finishedAt || null,
|
|
643
|
-
error: job.error || null,
|
|
644
|
-
...jobWorkerSnapshot(meta.sessionId),
|
|
645
|
-
...progress,
|
|
646
|
-
...(includeResult && job.result !== undefined ? { result: job.result } : {}),
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
function preparedSpawnMeta(prepared, extras = {}) {
|
|
651
|
-
return sanitizeTaskMeta({
|
|
652
|
-
...(extras || {}),
|
|
653
|
-
tag: prepared.tag,
|
|
654
|
-
sessionId: prepared.session.id,
|
|
655
|
-
agent: prepared.agent,
|
|
656
|
-
preset: presetKey(prepared.preset) || prepared.presetName,
|
|
657
|
-
provider: prepared.preset.provider,
|
|
658
|
-
model: prepared.preset.model,
|
|
659
|
-
effort: prepared.preset.effort || null,
|
|
660
|
-
fast: prepared.preset.fast === true,
|
|
661
|
-
maxLoopIterations: prepared.maxLoopIterations || null,
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
function pendingSpawnMeta(args = {}, extras = {}) {
|
|
666
|
-
const agent = normalizeAgentName(args.agent);
|
|
667
|
-
// Best-effort resolve the default preset so the pending "Spawn …" card can
|
|
668
|
-
// already show the model (e.g. "Spawn Heavy Worker (Opus 4.8)") even when
|
|
669
|
-
// the caller did not pass an explicit provider/model. Never throw: fall back
|
|
670
|
-
// to whatever raw args carry.
|
|
671
|
-
let resolved = null;
|
|
672
|
-
if (!clean(args.model) || !clean(args.provider)) {
|
|
673
|
-
try { resolved = resolveAgentSpawnPreset(cfgMod.loadConfig(), args)?.preset || null; }
|
|
674
|
-
catch { resolved = null; }
|
|
675
|
-
}
|
|
676
|
-
return sanitizeTaskMeta({
|
|
677
|
-
...(extras || {}),
|
|
678
|
-
tag: clean(args.tag) || null,
|
|
679
|
-
sessionId: null,
|
|
680
|
-
agent: agent || null,
|
|
681
|
-
preset: clean(args.preset) || presetKey(resolved) || null,
|
|
682
|
-
provider: clean(args.provider) || clean(resolved?.provider) || null,
|
|
683
|
-
model: clean(args.model) || clean(resolved?.model) || null,
|
|
684
|
-
effort: clean(args.effort) || clean(resolved?.effort) || null,
|
|
685
|
-
fast: args.fast === true ? true : (resolved?.fast === true ? true : null),
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
function mergeJobMeta(job, meta = {}) {
|
|
690
|
-
if (!job || !meta || typeof meta !== 'object') return;
|
|
691
|
-
const next = sanitizeTaskMeta({ ...(job.meta || {}), ...meta });
|
|
692
|
-
job.meta = next;
|
|
693
|
-
if (job.input && typeof job.input === 'object') {
|
|
694
|
-
job.input = {
|
|
695
|
-
...job.input,
|
|
696
|
-
tag: next.tag || job.input.tag || null,
|
|
697
|
-
sessionId: next.sessionId || job.input.sessionId || null,
|
|
698
|
-
agent: next.agent || job.input.agent || null,
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
job.label = next.tag || next.sessionId || job.label;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
function closePreparedSpawn(prepared, reason = 'agent-task-cancel') {
|
|
705
|
-
if (!prepared?.session?.id) return;
|
|
706
|
-
try { mgr.closeSession(prepared.session.id, reason); } catch {}
|
|
707
|
-
try { clearAgentStatuslineRoute(prepared.session.id); } catch {}
|
|
708
|
-
forgetTerminalSession(prepared.tag, prepared.session.id);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// Owner/worker completion notification lives in ./agent-tool/notify.mjs; the
|
|
712
|
-
// factory captures mgr and registers the canonical completion fallback.
|
|
713
|
-
const { workerNotifyFn, notifyOwnerAgentCompletionEarly } = createNotify(mgr);
|
|
714
|
-
|
|
715
|
-
function startJob(type, meta, run, notifyContext = null) {
|
|
716
|
-
const clientHostPid = terminalPidForContext(notifyContext);
|
|
717
|
-
const jobMeta = sanitizeTaskMeta({
|
|
718
|
-
...(meta || {}),
|
|
719
|
-
...(clientHostPid ? { clientHostPid } : {}),
|
|
186
|
+
// Spawn/job lifecycle (startJob/watchdogs/prepare/run): agent-tool/spawn-flow.mjs.
|
|
187
|
+
const {
|
|
188
|
+
closePreparedSpawn,
|
|
189
|
+
workerNotifyFn,
|
|
190
|
+
notifyOwnerAgentCompletionEarly,
|
|
191
|
+
startJob,
|
|
192
|
+
startDeferredSpawnJob,
|
|
193
|
+
progressWatchdogs,
|
|
194
|
+
startProgressIdleWatchdog,
|
|
195
|
+
prepareSpawn,
|
|
196
|
+
tryShardEnqueue,
|
|
197
|
+
runShardSpawn,
|
|
198
|
+
prepareSpawnInProcess,
|
|
199
|
+
runSpawn,
|
|
200
|
+
} = createSpawnFlow({
|
|
201
|
+
mgr,
|
|
202
|
+
forgetTerminalSession,
|
|
203
|
+
pendingSpawnMeta,
|
|
204
|
+
DEFAULT_SPAWN_PREP_TIMEOUT_MS,
|
|
205
|
+
mergeJobMeta,
|
|
206
|
+
preparedSpawnMeta,
|
|
207
|
+
upsertWorkerSessionDeferred,
|
|
208
|
+
refreshTagsFromSessions,
|
|
209
|
+
readWorkerRows,
|
|
210
|
+
defaultCwd,
|
|
211
|
+
nextTag,
|
|
212
|
+
cancelReap,
|
|
213
|
+
bindTag,
|
|
214
|
+
emitSubagentEvent,
|
|
215
|
+
scheduleReap,
|
|
216
|
+
cfgMod,
|
|
217
|
+
dataDir,
|
|
218
|
+
STANDALONE_SOURCE_ROOT,
|
|
219
|
+
ensureProvider,
|
|
220
|
+
resolveTag,
|
|
221
|
+
wantsSessionScan,
|
|
222
|
+
createTurnReviewCollector,
|
|
720
223
|
});
|
|
721
|
-
let task;
|
|
722
|
-
const admissionController = new AbortController();
|
|
723
|
-
task = startBackgroundTask({
|
|
724
|
-
surface: 'agent',
|
|
725
|
-
operation: type,
|
|
726
|
-
label: jobMeta?.tag || jobMeta?.sessionId || type,
|
|
727
|
-
input: { type, tag: jobMeta?.tag || null, sessionId: jobMeta?.sessionId || null, agent: jobMeta?.agent || null },
|
|
728
|
-
context: notifyContext,
|
|
729
|
-
meta: jobMeta,
|
|
730
|
-
resultType: 'agent_task_result',
|
|
731
|
-
renderResult: (result) => renderResult(result),
|
|
732
|
-
cancel: () => {
|
|
733
|
-
try { admissionController.abort(new Error('agent task cancelled before resource admission')); } catch {}
|
|
734
|
-
const currentMeta = task?.meta || jobMeta;
|
|
735
|
-
if (currentMeta?.sessionId) {
|
|
736
|
-
try { mgr.closeSession(currentMeta.sessionId, 'agent-task-cancel'); } catch {}
|
|
737
|
-
}
|
|
738
|
-
// Sharded spawn: the live session exists only in the shard child —
|
|
739
|
-
// forward the cancel by sessionId (known after the `spawned` progress
|
|
740
|
-
// event) or tag.
|
|
741
|
-
if (agentShardEnabled() && (currentMeta?.tag || currentMeta?.sessionId)) {
|
|
742
|
-
try {
|
|
743
|
-
void shardRequest('cancel', {
|
|
744
|
-
tag: currentMeta?.tag || null,
|
|
745
|
-
sessionId: currentMeta?.sessionId || null,
|
|
746
|
-
}).catch(() => {});
|
|
747
|
-
} catch { /* best-effort */ }
|
|
748
|
-
}
|
|
749
|
-
},
|
|
750
|
-
run: async () => {
|
|
751
|
-
const lease = await resourceAdmission.acquire('agent', {
|
|
752
|
-
signal: admissionController.signal,
|
|
753
|
-
label: jobMeta?.tag || type,
|
|
754
|
-
});
|
|
755
|
-
try {
|
|
756
|
-
// Yield one macrotask before doing agent work. startBackgroundTask uses
|
|
757
|
-
// a Promise microtask, which otherwise begins CPU-heavy spawn prep
|
|
758
|
-
// before the TUI receives/render the "running" result.
|
|
759
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
760
|
-
if (task?.status === 'cancelled') return null;
|
|
761
|
-
return await resourceAdmission.runWithLease(lease, () => run(task));
|
|
762
|
-
} finally {
|
|
763
|
-
await lease.release();
|
|
764
|
-
}
|
|
765
|
-
},
|
|
766
|
-
});
|
|
767
|
-
return task;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
function startDeferredSpawnJob(args, callerCwd, context, notifyContext, extras = {}) {
|
|
771
|
-
return startJob('spawn', pendingSpawnMeta(args, extras), async (job) => {
|
|
772
|
-
if (job?.status === 'cancelled') return null;
|
|
773
|
-
// P1 agent sharding (MIXDOG_AGENT_SHARD=1, default off): run the worker
|
|
774
|
-
// session in a shard child process instead of this event loop. Prompt
|
|
775
|
-
// resolution stays here (cheap); prep + the full agent loop run in the
|
|
776
|
-
// shard. Tag/index/watchdog parity lands in P2 (docs/agent-shard-design.md).
|
|
777
|
-
if (agentShardEnabled()) {
|
|
778
|
-
return await runShardSpawn(args, callerCwd, context, job);
|
|
779
|
-
}
|
|
780
|
-
// prepareSpawn (ensureProvider/prepareAgentSession) runs before runSpawn
|
|
781
|
-
// installs its progress watchdog, so guard prep with an internal env-
|
|
782
|
-
// backed cap rather than exposing per-call timeout knobs on the agent
|
|
783
|
-
// tool surface.
|
|
784
|
-
const prepDeadlineMs = nonNegativeInt(args.spawnPrepTimeoutMs ?? args.prepTimeoutMs)
|
|
785
|
-
?? DEFAULT_SPAWN_PREP_TIMEOUT_MS;
|
|
786
|
-
let prepared;
|
|
787
|
-
const prepState = { timedOut: false };
|
|
788
|
-
if (prepDeadlineMs > 0) {
|
|
789
|
-
let prepTimer = null;
|
|
790
|
-
let timedOut = false;
|
|
791
|
-
// If prep wins the race we use its result. If the timeout wins, the
|
|
792
|
-
// prepareSpawn promise may still resolve later with a fully-built
|
|
793
|
-
// session/tag/route — attach a cleanup so the late-arriving prepared is
|
|
794
|
-
// torn down, otherwise the orphaned tag would collide on re-spawn.
|
|
795
|
-
const prepPromise = prepareSpawn(args, callerCwd, context, prepState);
|
|
796
|
-
prepPromise.then((late) => {
|
|
797
|
-
if (timedOut) closePreparedSpawn(late, 'agent-spawn-prep-timeout');
|
|
798
|
-
}, () => {});
|
|
799
|
-
const timeout = new Promise((_resolve, reject) => {
|
|
800
|
-
prepTimer = setTimeout(() => {
|
|
801
|
-
timedOut = true;
|
|
802
|
-
prepState.timedOut = true;
|
|
803
|
-
reject(new Error(`agent spawn prep timed out (${prepDeadlineMs}ms) before model request`));
|
|
804
|
-
}, prepDeadlineMs);
|
|
805
|
-
prepTimer.unref?.();
|
|
806
|
-
});
|
|
807
|
-
try {
|
|
808
|
-
prepared = await Promise.race([prepPromise, timeout]);
|
|
809
|
-
} finally {
|
|
810
|
-
if (prepTimer) clearTimeout(prepTimer);
|
|
811
|
-
}
|
|
812
|
-
} else {
|
|
813
|
-
prepared = await prepareSpawn(args, callerCwd, context, prepState);
|
|
814
|
-
}
|
|
815
|
-
mergeJobMeta(job, preparedSpawnMeta(prepared, extras));
|
|
816
|
-
upsertWorkerSessionDeferred(prepared.session, prepared.tag, {
|
|
817
|
-
...preparedSpawnMeta(prepared, extras),
|
|
818
|
-
status: 'running',
|
|
819
|
-
stage: 'running',
|
|
820
|
-
task_id: job.taskId,
|
|
821
|
-
startedAt: job.startedAt,
|
|
822
|
-
});
|
|
823
|
-
if (job?.status === 'cancelled') {
|
|
824
|
-
closePreparedSpawn(prepared);
|
|
825
|
-
return null;
|
|
826
|
-
}
|
|
827
|
-
return await runSpawn(prepared, notifyContext, job);
|
|
828
|
-
}, notifyContext);
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
// Idle-progress watchdogs (shared sweep timer) live in
|
|
832
|
-
// agent-watchdog-registry.mjs.
|
|
833
|
-
const progressWatchdogs = createProgressWatchdogRegistry({ mgr });
|
|
834
|
-
const startProgressIdleWatchdog = (sessionId, watchdogPolicy, agent = null) =>
|
|
835
|
-
progressWatchdogs.start(sessionId, watchdogPolicy, agent);
|
|
836
|
-
|
|
837
|
-
async function prepareSpawn(args, callerCwd = null, context = {}, prepState = null) {
|
|
838
|
-
// (sharded spawns never reach here — see startDeferredSpawnJob)
|
|
839
|
-
refreshTagsFromSessions({ context });
|
|
840
|
-
return prepareSpawnInProcess(args, callerCwd, context, prepState);
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
// Tag-reuse routing for sharded workers (P2): a same-tag spawn/send while
|
|
844
|
-
// the shard runs the session mirrors the in-process busy-session enqueue.
|
|
845
|
-
// Discriminator: a non-terminal worker-index row whose session has no live
|
|
846
|
-
// runtime in THIS process. (mgr.getSession must not be used here — it
|
|
847
|
-
// falls back to the on-disk session file the shard child persists, which
|
|
848
|
-
// makes a shard-owned session look lead-live.) Returns the rendered queued
|
|
849
|
-
// result, or null to fall through to the normal path (unknown tag,
|
|
850
|
-
// terminal row, lead-owned runtime, or the shard no longer owns the
|
|
851
|
-
// session — e.g. after a shard crash, where respawn is the correct
|
|
852
|
-
// recovery).
|
|
853
|
-
async function tryShardEnqueue(target, args, context, extras = {}) {
|
|
854
|
-
if (!agentShardEnabled()) return null;
|
|
855
|
-
const value = clean(target);
|
|
856
|
-
if (!value) return null;
|
|
857
|
-
const row = readWorkerRows(context).find((r) => (
|
|
858
|
-
(clean(r.tag) === value || clean(r.sessionId) === value)
|
|
859
|
-
&& rowMatchesContext(r, context)
|
|
860
|
-
&& !isTerminalWorkerStatus(r.status || r.stage)
|
|
861
|
-
));
|
|
862
|
-
if (!row) return null;
|
|
863
|
-
const rowSessionId = clean(row.sessionId);
|
|
864
|
-
const runtime = rowSessionId ? mgr.getSessionRuntime?.(rowSessionId) : null;
|
|
865
|
-
if (runtime?.controller?.signal && !runtime.controller.signal.aborted) return null;
|
|
866
|
-
const prompt = await resolvePrompt(args, clean(row.cwd) || defaultCwd || process.cwd());
|
|
867
|
-
if (!clean(prompt)) return null;
|
|
868
|
-
let queued = null;
|
|
869
|
-
try {
|
|
870
|
-
queued = await shardRequest('enqueue', {
|
|
871
|
-
tag: clean(row.tag) || null,
|
|
872
|
-
sessionId: rowSessionId || null,
|
|
873
|
-
prompt,
|
|
874
|
-
});
|
|
875
|
-
} catch { return null; }
|
|
876
|
-
if (queued?.queued !== true) return null;
|
|
877
|
-
return renderResult({
|
|
878
|
-
queued: true,
|
|
879
|
-
...extras,
|
|
880
|
-
tag: clean(row.tag) || null,
|
|
881
|
-
sessionId: rowSessionId || null,
|
|
882
|
-
agent: clean(row.agent) || null,
|
|
883
|
-
queueDepth: queued.queueDepth || 1,
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
async function runShardSpawn(args, callerCwd, context, job) {
|
|
888
|
-
const agent = normalizeAgentName(args.agent);
|
|
889
|
-
if (!agent) throw new Error('agent spawn: agent is required');
|
|
890
|
-
const tag = clean(args.tag) || nextTag(agent, context);
|
|
891
|
-
const baseCwd = resolve(callerCwd || defaultCwd || process.cwd());
|
|
892
|
-
const workerCwd = clean(args.cwd) ? resolve(baseCwd, args.cwd) : baseCwd;
|
|
893
|
-
const prompt = withCwdHeader(await resolvePrompt(args, workerCwd), workerCwd);
|
|
894
|
-
const clientHostPid = terminalPidForContext(context) || null;
|
|
895
|
-
// P2 progress parity: the shard reports the bound session the moment it
|
|
896
|
-
// exists (`spawned`), so the lead mirrors its in-process spawn
|
|
897
|
-
// bookkeeping — tag maps, worker-index row, statusline route, job meta,
|
|
898
|
-
// SubagentStart — while the worker loop runs in the child.
|
|
899
|
-
let spawned = null;
|
|
900
|
-
const onSpawned = (payload = {}) => {
|
|
901
|
-
if (spawned || !payload.sessionId) return;
|
|
902
|
-
spawned = payload;
|
|
903
|
-
const sessionLike = {
|
|
904
|
-
id: payload.sessionId,
|
|
905
|
-
agent,
|
|
906
|
-
cwd: payload.cwd || workerCwd,
|
|
907
|
-
provider: payload.provider || null,
|
|
908
|
-
model: payload.model || null,
|
|
909
|
-
presetName: payload.presetName || null,
|
|
910
|
-
effort: payload.effort || null,
|
|
911
|
-
fast: payload.fast === true,
|
|
912
|
-
clientHostPid,
|
|
913
|
-
status: 'running',
|
|
914
|
-
};
|
|
915
|
-
cancelReap(payload.sessionId);
|
|
916
|
-
writeAgentStatuslineRoute(payload.sessionId, {
|
|
917
|
-
provider: payload.provider || null,
|
|
918
|
-
model: payload.model || null,
|
|
919
|
-
effort: payload.effort || null,
|
|
920
|
-
fast: payload.fast === true,
|
|
921
|
-
});
|
|
922
|
-
bindTag(tag, sessionLike, {
|
|
923
|
-
agent,
|
|
924
|
-
preset: payload.presetName || null,
|
|
925
|
-
provider: payload.provider || null,
|
|
926
|
-
model: payload.model || null,
|
|
927
|
-
effort: payload.effort || null,
|
|
928
|
-
fast: payload.fast === true,
|
|
929
|
-
status: 'running',
|
|
930
|
-
stage: 'running',
|
|
931
|
-
});
|
|
932
|
-
if (job) {
|
|
933
|
-
mergeJobMeta(job, {
|
|
934
|
-
tag,
|
|
935
|
-
sessionId: payload.sessionId,
|
|
936
|
-
agent,
|
|
937
|
-
preset: payload.presetName || null,
|
|
938
|
-
provider: payload.provider || null,
|
|
939
|
-
model: payload.model || null,
|
|
940
|
-
effort: payload.effort || null,
|
|
941
|
-
fast: payload.fast === true,
|
|
942
|
-
});
|
|
943
|
-
}
|
|
944
|
-
emitSubagentEvent('start', agent, { session_id: payload.sessionId, tag });
|
|
945
|
-
// Cancel raced the shard prep: the task is already terminal on the lead
|
|
946
|
-
// but the child just started its loop — abort it now that the sessionId
|
|
947
|
-
// is known, instead of letting it burn tokens to completion.
|
|
948
|
-
if (job?.status === 'cancelled') {
|
|
949
|
-
try { void shardRequest('cancel', { tag, sessionId: payload.sessionId }).catch(() => {}); } catch { /* best-effort */ }
|
|
950
|
-
}
|
|
951
|
-
};
|
|
952
|
-
let finalStatus = 'error';
|
|
953
|
-
try {
|
|
954
|
-
const response = await shardRequest('spawn', {
|
|
955
|
-
args: {
|
|
956
|
-
agent,
|
|
957
|
-
provider: args.provider,
|
|
958
|
-
model: args.model,
|
|
959
|
-
effort: args.effort,
|
|
960
|
-
fast: args.fast,
|
|
961
|
-
preset: args.preset,
|
|
962
|
-
},
|
|
963
|
-
tag,
|
|
964
|
-
cwd: workerCwd,
|
|
965
|
-
prompt,
|
|
966
|
-
context: args.context || null,
|
|
967
|
-
parentSessionId: clean(context?.callerSessionId || context?.sessionId) || null,
|
|
968
|
-
ownerSessionId: clean(context?.callerSessionId || context?.sessionId) || null,
|
|
969
|
-
clientHostPid,
|
|
970
|
-
}, {
|
|
971
|
-
onProgress: (event, payload) => { if (event === 'spawned') onSpawned(payload); },
|
|
972
|
-
});
|
|
973
|
-
// Same abnormal-empty promotion as the in-process completionValue; a
|
|
974
|
-
// watchdog partial handoff (stallAbort) is a valid completion and is
|
|
975
|
-
// exempt, matching runSpawn's catch path.
|
|
976
|
-
const abnormalError = response?.stallAbort === true
|
|
977
|
-
? null
|
|
978
|
-
: abnormalEmptyFinishError(response, agent);
|
|
979
|
-
const value = {
|
|
980
|
-
tag,
|
|
981
|
-
sessionId: response.sessionId,
|
|
982
|
-
agent,
|
|
983
|
-
preset: response.presetName || null,
|
|
984
|
-
provider: response.provider || null,
|
|
985
|
-
model: response.model || null,
|
|
986
|
-
effort: response.effort || null,
|
|
987
|
-
fast: response.fast === true,
|
|
988
|
-
content: response.content || '',
|
|
989
|
-
...(response.stallAbort === true ? { stallAbort: true } : {}),
|
|
990
|
-
...(abnormalError ? { error: abnormalError } : {}),
|
|
991
|
-
};
|
|
992
|
-
if (job) job._terminalResultValue = value;
|
|
993
|
-
if (value.error) throw new Error(value.error);
|
|
994
|
-
finalStatus = 'idle';
|
|
995
|
-
return value;
|
|
996
|
-
} finally {
|
|
997
|
-
// Terminal bookkeeping parity with runSpawn's finally: worker-index row
|
|
998
|
-
// transitions to idle/error, the terminal-reap timer starts (tombstone +
|
|
999
|
-
// statusline cleanup), and SubagentStop fires. Only after `spawned` —
|
|
1000
|
-
// a prep failure inside the shard bound nothing on the lead.
|
|
1001
|
-
if (spawned?.sessionId) {
|
|
1002
|
-
upsertWorkerSessionDeferred({
|
|
1003
|
-
id: spawned.sessionId,
|
|
1004
|
-
agent,
|
|
1005
|
-
cwd: spawned.cwd || workerCwd,
|
|
1006
|
-
provider: spawned.provider || null,
|
|
1007
|
-
model: spawned.model || null,
|
|
1008
|
-
presetName: spawned.presetName || null,
|
|
1009
|
-
effort: spawned.effort || null,
|
|
1010
|
-
fast: spawned.fast === true,
|
|
1011
|
-
clientHostPid,
|
|
1012
|
-
status: finalStatus,
|
|
1013
|
-
}, tag, {
|
|
1014
|
-
agent,
|
|
1015
|
-
preset: spawned.presetName || null,
|
|
1016
|
-
provider: spawned.provider || null,
|
|
1017
|
-
model: spawned.model || null,
|
|
1018
|
-
effort: spawned.effort || null,
|
|
1019
|
-
fast: spawned.fast === true,
|
|
1020
|
-
status: finalStatus,
|
|
1021
|
-
stage: finalStatus,
|
|
1022
|
-
finishedAt: new Date().toISOString(),
|
|
1023
|
-
});
|
|
1024
|
-
scheduleReap(spawned.sessionId, spawned.provider || null);
|
|
1025
|
-
emitSubagentEvent('stop', agent, { session_id: spawned.sessionId, tag, status: finalStatus });
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
async function prepareSpawnInProcess(args, callerCwd = null, context = {}, prepState = null) {
|
|
1031
|
-
const config = cfgMod.loadConfig();
|
|
1032
|
-
const agent = normalizeAgentName(args.agent);
|
|
1033
|
-
if (!agent) throw new Error('agent spawn: agent is required');
|
|
1034
|
-
const agentPermission = readAgentFrontmatterPermission(agent, dataDir, STANDALONE_SOURCE_ROOT);
|
|
1035
|
-
const agentPerm = normalizeAgentPermission(agentPermission) || null;
|
|
1036
|
-
const { presetName, preset } = resolveAgentSpawnPreset(config, args);
|
|
1037
|
-
await ensureProvider(config, preset.provider);
|
|
1038
|
-
if (prepState?.timedOut) {
|
|
1039
|
-
throw new Error('agent spawn prep timed out before session bind');
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
const tag = clean(args.tag) || nextTag(agent, context);
|
|
1043
|
-
// Any resolved same-tag binding in this terminal (live or lingering trace)
|
|
1044
|
-
// blocks a fresh spawn. execute() routes live reuse before prepareSpawn.
|
|
1045
|
-
if (resolveTag(tag, context, { scanSessions: wantsSessionScan(args) })) {
|
|
1046
|
-
throw new Error(`agent spawn: tag "${tag}" already exists`);
|
|
1047
|
-
}
|
|
1048
|
-
const baseCwd = resolve(callerCwd || defaultCwd || process.cwd());
|
|
1049
|
-
const workerCwd = clean(args.cwd) ? resolve(baseCwd, args.cwd) : baseCwd;
|
|
1050
|
-
const prompt = withCwdHeader(await resolvePrompt(args, workerCwd), workerCwd);
|
|
1051
|
-
if (prepState?.timedOut) {
|
|
1052
|
-
throw new Error('agent spawn prep timed out before session bind');
|
|
1053
|
-
}
|
|
1054
|
-
const runtimeSpec = cfgMod.resolveRuntimeSpec(preset, { lane: 'agent', agentId: tag });
|
|
1055
|
-
const maxLoopIterations = positiveInt(args.maxLoopIterations) || null;
|
|
1056
|
-
const watchdogPolicy = resolveAgentWatchdogPolicy(agent);
|
|
1057
|
-
const { session, effectiveCwd } = prepareAgentSession({
|
|
1058
|
-
agent,
|
|
1059
|
-
presetName,
|
|
1060
|
-
preset,
|
|
1061
|
-
runtimeSpec,
|
|
1062
|
-
owner: AGENT_OWNER,
|
|
1063
|
-
cwd: workerCwd,
|
|
1064
|
-
sourceType: 'cli',
|
|
1065
|
-
sourceName: agent,
|
|
1066
|
-
parentSessionId: clean(context?.callerSessionId || context?.sessionId) || null,
|
|
1067
|
-
ownerSessionId: clean(context?.callerSessionId || context?.sessionId) || null,
|
|
1068
|
-
clientHostPid: terminalPidForContext(context) || null,
|
|
1069
|
-
agentTag: tag,
|
|
1070
|
-
taskType: clean(args.taskType) || clean(args.typeHint) || undefined,
|
|
1071
|
-
maxLoopIterations: maxLoopIterations || undefined,
|
|
1072
|
-
permission: agentPerm || undefined,
|
|
1073
|
-
cacheKeyOverride: args.cacheKey || undefined,
|
|
1074
|
-
});
|
|
1075
|
-
// Lead sessions write a gateway-session route when created; agent sessions
|
|
1076
|
-
// are built through prepareAgentSession(), so mirror that registration here
|
|
1077
|
-
// or the vendored L1/L2 statusline cannot resolve the agent route/model.
|
|
1078
|
-
writeAgentStatuslineRoute(session.id, preset);
|
|
1079
|
-
bindTag(tag, session, {
|
|
1080
|
-
agent,
|
|
1081
|
-
preset: presetKey(preset) || presetName,
|
|
1082
|
-
provider: preset.provider,
|
|
1083
|
-
model: preset.model,
|
|
1084
|
-
effort: preset.effort || null,
|
|
1085
|
-
fast: preset.fast === true,
|
|
1086
|
-
status: 'idle',
|
|
1087
|
-
stage: 'idle',
|
|
1088
|
-
});
|
|
1089
|
-
cancelReap(session.id);
|
|
1090
|
-
return {
|
|
1091
|
-
args,
|
|
1092
|
-
tag,
|
|
1093
|
-
session,
|
|
1094
|
-
agent,
|
|
1095
|
-
preset,
|
|
1096
|
-
presetName,
|
|
1097
|
-
workerCwd: effectiveCwd || workerCwd,
|
|
1098
|
-
prompt,
|
|
1099
|
-
maxLoopIterations,
|
|
1100
|
-
watchdogPolicy,
|
|
1101
|
-
};
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
async function runSpawn(prepared, notifyContext = null, job = null) {
|
|
1105
|
-
const { args, tag, session, agent, preset, presetName, workerCwd, prompt, watchdogPolicy } = prepared;
|
|
1106
|
-
const watchdog = startProgressIdleWatchdog(session.id, watchdogPolicy, agent);
|
|
1107
|
-
const turnReview = createTurnReviewCollector(session, tag, agent, notifyContext || {});
|
|
1108
|
-
let finalStatus = 'idle';
|
|
1109
|
-
// SubagentStart: a worker session is about to run its first turn.
|
|
1110
|
-
emitSubagentEvent('start', agent, { session_id: session.id, tag });
|
|
1111
|
-
upsertWorkerSessionDeferred(session, tag, {
|
|
1112
|
-
agent,
|
|
1113
|
-
preset: presetKey(preset) || presetName,
|
|
1114
|
-
provider: preset.provider,
|
|
1115
|
-
model: preset.model,
|
|
1116
|
-
effort: preset.effort || null,
|
|
1117
|
-
fast: preset.fast === true,
|
|
1118
|
-
status: 'running',
|
|
1119
|
-
stage: 'running',
|
|
1120
|
-
});
|
|
1121
|
-
let handoffMsgStart = 0;
|
|
1122
|
-
try {
|
|
1123
|
-
const completionValue = (result) => {
|
|
1124
|
-
// Promote an abnormal finish (iteration cap, truncation, or a public
|
|
1125
|
-
// agent's empty terminal turn) to an explicit error, so the Lead
|
|
1126
|
-
// receives it as a failure with an accurate reason instead of a silent
|
|
1127
|
-
// `completed` empty result. Keyed off loop.mjs terminationReason;
|
|
1128
|
-
// hidden agents finishing normally-empty are left untagged (benign).
|
|
1129
|
-
const abnormalError = abnormalEmptyFinishError(result, agent);
|
|
1130
|
-
return {
|
|
1131
|
-
tag,
|
|
1132
|
-
sessionId: session.id,
|
|
1133
|
-
agent,
|
|
1134
|
-
preset: presetKey(preset) || presetName,
|
|
1135
|
-
provider: preset.provider,
|
|
1136
|
-
model: preset.model,
|
|
1137
|
-
effort: preset.effort || null,
|
|
1138
|
-
fast: preset.fast === true,
|
|
1139
|
-
content: result?.content || '',
|
|
1140
|
-
...(abnormalError ? { error: abnormalError } : {}),
|
|
1141
|
-
};
|
|
1142
|
-
};
|
|
1143
|
-
handoffMsgStart = resolveHandoffMessageStartIndex(mgr.getSession(session.id));
|
|
1144
|
-
const result = await mgr.askSession(session.id, prompt, args.context || null, null, workerCwd, null, {
|
|
1145
|
-
notifyFn: workerNotifyFn(session.id, notifyContext || {}),
|
|
1146
|
-
onToolResult: (message) => turnReview.onToolResult(message),
|
|
1147
|
-
...(job ? {
|
|
1148
|
-
onTerminalResult: (terminalResult) => {
|
|
1149
|
-
turnReview.complete();
|
|
1150
|
-
const value = completionValue(terminalResult);
|
|
1151
|
-
if (job) job._terminalResultValue = value;
|
|
1152
|
-
notifyOwnerAgentCompletionEarly(job, value, notifyContext || {});
|
|
1153
|
-
// Mark the task terminal the moment the worker produces its final
|
|
1154
|
-
// result, so a hung/slow post-result session save cannot strand the
|
|
1155
|
-
// task (and the status card) in `running`. Idempotent; the finally
|
|
1156
|
-
// reconcile remains a backup for the error/no-terminal-result path.
|
|
1157
|
-
if (job?.taskId) {
|
|
1158
|
-
try {
|
|
1159
|
-
// An empty/abnormal finish is a failure, not a completion:
|
|
1160
|
-
// reconcile as `failed` with the accurate error so the Lead
|
|
1161
|
-
// card renders `error: …` instead of a header-only empty card.
|
|
1162
|
-
reconcileBackgroundTask(job.taskId, value.error
|
|
1163
|
-
? {
|
|
1164
|
-
status: 'failed',
|
|
1165
|
-
result: value,
|
|
1166
|
-
error: value.error,
|
|
1167
|
-
terminalReason: 'agent-empty-final',
|
|
1168
|
-
}
|
|
1169
|
-
: {
|
|
1170
|
-
status: 'completed',
|
|
1171
|
-
result: value,
|
|
1172
|
-
terminalReason: 'agent-terminal-result',
|
|
1173
|
-
});
|
|
1174
|
-
} catch {}
|
|
1175
|
-
}
|
|
1176
|
-
},
|
|
1177
|
-
} : {}),
|
|
1178
|
-
});
|
|
1179
|
-
// The early preview no longer promises body suppression, so the canonical
|
|
1180
|
-
// notifyTaskCompletion is left to fire exactly once with output via the
|
|
1181
|
-
// resolve/reconcile/finally path.
|
|
1182
|
-
const finalValue = completionValue(result);
|
|
1183
|
-
// Non-job return path (or job path where the terminal-result reconcile
|
|
1184
|
-
// already ran): if the finish was abnormal-empty, surface it as a thrown
|
|
1185
|
-
// error so finalStatus becomes 'error' and the caller's error path (and
|
|
1186
|
-
// the finally reconcile below, as `failed`) render the accurate reason.
|
|
1187
|
-
if (finalValue.error) {
|
|
1188
|
-
finalStatus = 'error';
|
|
1189
|
-
if (job) job._terminalResultValue = finalValue;
|
|
1190
|
-
throw new Error(finalValue.error);
|
|
1191
|
-
}
|
|
1192
|
-
return finalValue;
|
|
1193
|
-
} catch (error) {
|
|
1194
|
-
const partial = watchdogPartialHandoffFromError(error, mgr.getSession(session.id), handoffMsgStart);
|
|
1195
|
-
if (partial) {
|
|
1196
|
-
finalStatus = 'idle';
|
|
1197
|
-
const value = {
|
|
1198
|
-
tag,
|
|
1199
|
-
sessionId: session.id,
|
|
1200
|
-
agent,
|
|
1201
|
-
preset: presetKey(preset) || presetName,
|
|
1202
|
-
provider: preset.provider,
|
|
1203
|
-
model: preset.model,
|
|
1204
|
-
effort: preset.effort || null,
|
|
1205
|
-
fast: preset.fast === true,
|
|
1206
|
-
content: partial,
|
|
1207
|
-
stallAbort: true,
|
|
1208
|
-
};
|
|
1209
|
-
if (job) job._terminalResultValue = value;
|
|
1210
|
-
if (job?.taskId) {
|
|
1211
|
-
try {
|
|
1212
|
-
reconcileBackgroundTask(job.taskId, {
|
|
1213
|
-
status: 'completed',
|
|
1214
|
-
result: value,
|
|
1215
|
-
terminalReason: 'agent-watchdog-partial',
|
|
1216
|
-
});
|
|
1217
|
-
} catch {}
|
|
1218
|
-
}
|
|
1219
|
-
return value;
|
|
1220
|
-
}
|
|
1221
|
-
finalStatus = 'error';
|
|
1222
|
-
// Part C: a mid-stream stall (StreamStalledError / ESTREAMSTALL) throws
|
|
1223
|
-
// here WITHOUT a terminal result, so the finally reconcile below (gated on
|
|
1224
|
-
// _terminalResultValue) would be skipped and only the outer task-reject
|
|
1225
|
-
// path would notify. Belt-and-suspenders: reconcile this job to `failed`
|
|
1226
|
-
// now so the owner (Lead) always gets a failure notification instead of a
|
|
1227
|
-
// task stranded in `running`. Idempotent — completeBackgroundTask no-ops
|
|
1228
|
-
// once terminal, so the outer reject path can't double-notify.
|
|
1229
|
-
if (job?.taskId && job._terminalResultValue === undefined) {
|
|
1230
|
-
try {
|
|
1231
|
-
reconcileBackgroundTask(job.taskId, {
|
|
1232
|
-
status: 'failed',
|
|
1233
|
-
error,
|
|
1234
|
-
terminalReason: 'agent-stream-stalled',
|
|
1235
|
-
});
|
|
1236
|
-
} catch {}
|
|
1237
|
-
}
|
|
1238
|
-
throw error;
|
|
1239
|
-
} finally {
|
|
1240
|
-
turnReview.complete();
|
|
1241
|
-
watchdog?.stop?.();
|
|
1242
|
-
upsertWorkerSessionDeferred(session, tag, {
|
|
1243
|
-
agent,
|
|
1244
|
-
preset: presetKey(preset) || presetName,
|
|
1245
|
-
provider: preset.provider,
|
|
1246
|
-
model: preset.model,
|
|
1247
|
-
effort: preset.effort || null,
|
|
1248
|
-
fast: preset.fast === true,
|
|
1249
|
-
status: finalStatus,
|
|
1250
|
-
stage: finalStatus,
|
|
1251
|
-
finishedAt: new Date().toISOString(),
|
|
1252
|
-
});
|
|
1253
|
-
// Safety net: if a post-result step (session save) hung or threw after the
|
|
1254
|
-
// worker already produced a terminal result, the task could otherwise be
|
|
1255
|
-
// stranded in `running`. Reconcile it to a terminal state using the
|
|
1256
|
-
// captured result so the owner gets a completion notification + the
|
|
1257
|
-
// statusline clears. Idempotent once the task is already terminal.
|
|
1258
|
-
if (job && job._terminalResultValue !== undefined) {
|
|
1259
|
-
try {
|
|
1260
|
-
reconcileBackgroundTask(job.taskId, {
|
|
1261
|
-
status: finalStatus === 'error' ? 'failed' : 'completed',
|
|
1262
|
-
result: job._terminalResultValue,
|
|
1263
|
-
...(finalStatus === 'error' && job._terminalResultValue?.error
|
|
1264
|
-
? { error: job._terminalResultValue.error }
|
|
1265
|
-
: {}),
|
|
1266
|
-
terminalReason: 'agent-finally-reconcile',
|
|
1267
|
-
});
|
|
1268
|
-
} catch {}
|
|
1269
|
-
}
|
|
1270
|
-
scheduleReap(session.id);
|
|
1271
|
-
// SubagentStop: worker finished (terminal), regardless of outcome.
|
|
1272
|
-
emitSubagentEvent('stop', agent, { session_id: session.id, tag, status: finalStatus });
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
224
|
|
|
1276
225
|
async function spawn(args) {
|
|
1277
226
|
return await runSpawn(await prepareSpawn(args));
|