mixdog 0.9.87 → 0.9.89
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 +13 -7
- package/scripts/tool-failures.mjs +60 -24
- package/src/agents/heavy-worker/AGENT.md +3 -4
- package/src/agents/worker/AGENT.md +1 -2
- package/src/cli.mjs +8 -0
- package/src/defaults/cycle3-review-prompt.md +4 -4
- package/src/rules/agent/00-core.md +3 -0
- package/src/rules/agent/42-cycle3-agent.md +5 -6
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/01-tool.md +29 -35
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +57 -4
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
- package/src/runtime/agent/orchestrator/config.mjs +29 -11
- package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +23 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +160 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +18 -1
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +50 -8
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
- package/src/runtime/agent/orchestrator/session/manager.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
- package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
- package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
- package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
- package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
- package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +85 -5
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +86 -3
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +210 -38
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +87 -19
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -8
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
- package/src/runtime/media/lanes.mjs +2 -2
- package/src/runtime/media/renditions.mjs +101 -17
- package/src/runtime/media/renditions.test.mjs +54 -0
- package/src/runtime/media/store.mjs +111 -43
- package/src/runtime/media/store.test.mjs +53 -11
- package/src/runtime/memory/index.mjs +26 -50
- package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
- package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
- package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
- package/src/runtime/memory/lib/http-router.mjs +4 -0
- package/src/runtime/memory/lib/ko-morph.mjs +49 -6
- package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -18
- package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
- package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
- package/src/runtime/memory/lib/query-handlers.mjs +26 -28
- package/src/runtime/memory/tool-defs.mjs +3 -3
- package/src/runtime/shared/atomic-file.mjs +5 -1
- package/src/runtime/shared/child-guardian.mjs +73 -2
- package/src/runtime/shared/provider-api-key.mjs +22 -5
- package/src/runtime/shared/tool-execution-contract.mjs +53 -0
- package/src/session-runtime/config-helpers.mjs +6 -5
- package/src/session-runtime/lifecycle-api.mjs +16 -4
- package/src/session-runtime/media-api.mjs +31 -16
- package/src/session-runtime/prewarm.mjs +2 -24
- package/src/session-runtime/runtime-core.mjs +33 -15
- package/src/session-runtime/runtime-tunables.mjs +0 -3
- package/src/session-runtime/session-lifecycle.mjs +0 -6
- package/src/session-runtime/session-title.mjs +228 -0
- package/src/session-runtime/session-turn-api.mjs +11 -7
- package/src/session-runtime/workflow-agents-api.mjs +16 -0
- package/src/session-runtime/workflow.mjs +7 -5
- package/src/standalone/agent-tool/job-views.mjs +90 -36
- package/src/standalone/agent-tool/spawn-flow.mjs +25 -216
- package/src/standalone/agent-tool/worker-index.mjs +8 -0
- package/src/standalone/agent-tool/worker-rows.mjs +2 -1
- package/src/standalone/agent-tool.mjs +17 -17
- package/src/standalone/agent-watchdog-registry.mjs +19 -2
- package/src/standalone/channel-daemon.mjs +8 -0
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/standalone/memory-runtime-proxy.mjs +4 -7
- package/src/standalone/projects.mjs +4 -1
- package/src/standalone/usage-dashboard.mjs +25 -3
- package/src/tui/App.jsx +5 -3
- package/src/tui/app/resume-picker.mjs +7 -2
- package/src/tui/app/route-pickers.mjs +11 -117
- package/src/tui/app/slash-dispatch.mjs +13 -3
- package/src/tui/app/transcript-row-estimate.mjs +1 -1
- package/src/tui/app/transcript-window.mjs +17 -93
- package/src/tui/app/use-transcript-scroll.mjs +32 -2
- package/src/tui/app/use-transcript-window.mjs +64 -114
- package/src/tui/components/Markdown.jsx +19 -2
- package/src/tui/components/Message.jsx +8 -3
- package/src/tui/components/ToolExecution.jsx +7 -3
- package/src/tui/dist/index.mjs +243 -223
- package/src/tui/engine/live-share.mjs +15 -5
- package/src/tui/engine/render-timing.mjs +2 -2
- package/src/tui/engine/session-api-ext.mjs +74 -4
- package/src/tui/engine/session-flow.mjs +3 -1
- package/src/tui/engine.mjs +2 -2
- package/src/tui/hooks/useSharedTick.mjs +0 -2
- package/src/tui/index.jsx +5 -5
- package/src/ui/statusline-agents.mjs +44 -11
- package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
- package/src/workflows/default/WORKFLOW.md +3 -3
- package/src/workflows/solo/WORKFLOW.md +5 -3
- package/src/runtime/media/index.mjs +0 -18
- package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
- package/src/standalone/agent-shard/shard-child.mjs +0 -300
- package/src/standalone/agent-shard/shard-pool.mjs +0 -443
|
@@ -9,6 +9,45 @@ import { buildAgentTaskProgressFields } from '../agent-task-status.mjs';
|
|
|
9
9
|
import { clean, normalizeAgentName, positiveInt, presetKey, terminalPidForContext } from './helpers.mjs';
|
|
10
10
|
import { getBackgroundTask, listBackgroundTasks, sanitizeTaskMeta, taskIdFromArgs } from '../../runtime/shared/background-tasks.mjs';
|
|
11
11
|
import { resolveAgentSpawnPreset } from './spawn-preset.mjs';
|
|
12
|
+
import { isTerminalWorkerStatus } from './worker-rows.mjs';
|
|
13
|
+
|
|
14
|
+
// Job statuses that can never progress again (mirrors TERMINAL_STATUSES in
|
|
15
|
+
// runtime/shared/background-tasks.mjs; 'canceled' accepted defensively).
|
|
16
|
+
const TERMINAL_JOB_STATUSES = new Set(['completed', 'failed', 'cancelled', 'canceled']);
|
|
17
|
+
|
|
18
|
+
export function isTerminalJobStatus(status) {
|
|
19
|
+
return TERMINAL_JOB_STATUSES.has(String(status ?? '').trim().toLowerCase());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// A finished job is frozen history. Tags are reused, so its meta.sessionId can
|
|
23
|
+
// point at a session that is NOW running a *different* job; reading live
|
|
24
|
+
// progress for it made every old completed row inherit the current worker's
|
|
25
|
+
// stage ("20 completed rows all reporting streaming/model active"). Terminal
|
|
26
|
+
// rows therefore report only their own recorded status: no live worker
|
|
27
|
+
// snapshot, no silent_for/watchdog/queued_followups.
|
|
28
|
+
export function terminalJobFrozenFields(status, now = Date.now()) {
|
|
29
|
+
const stage = String(status ?? '').trim().toLowerCase() || 'unknown';
|
|
30
|
+
return {
|
|
31
|
+
workerStatus: stage,
|
|
32
|
+
stage,
|
|
33
|
+
clientHostPid: null,
|
|
34
|
+
lastStreamDeltaAt: null,
|
|
35
|
+
...buildAgentTaskProgressFields({ now, runtimeStage: stage, taskStatus: stage }),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Default /agents worker section = workers that can still do work. Idle,
|
|
40
|
+
// closed, errored and reaped/unknown rows are history and stay reachable
|
|
41
|
+
// through the task section (status/read keep their terminal result).
|
|
42
|
+
export function isActiveWorkerRow(row = {}) {
|
|
43
|
+
const stage = String(row.stage ?? '').trim().toLowerCase();
|
|
44
|
+
const status = String(row.status ?? '').trim().toLowerCase();
|
|
45
|
+
if (ACTIVE_STAGES.has(stage) || ACTIVE_STAGES.has(status)) return true;
|
|
46
|
+
const effective = stage || status;
|
|
47
|
+
if (!effective || effective === 'unknown') return false;
|
|
48
|
+
return !isTerminalWorkerStatus(effective);
|
|
49
|
+
}
|
|
50
|
+
|
|
12
51
|
export function createJobViews({
|
|
13
52
|
mgr,
|
|
14
53
|
getLiveSession,
|
|
@@ -32,7 +71,7 @@ export function createJobViews({
|
|
|
32
71
|
// chain-gate defaults to the spawn-prep cap (see provider-init.mjs comments).
|
|
33
72
|
const { ensureProvider } = createProviderInit(reg, DEFAULT_SPAWN_PREP_TIMEOUT_MS);
|
|
34
73
|
|
|
35
|
-
function list({ scanSessions = false, context = {} } = {}) {
|
|
74
|
+
function list({ scanSessions = false, context = {}, includeTerminal = false } = {}) {
|
|
36
75
|
refreshTagsFromSessions({ scanSessions, context });
|
|
37
76
|
const now = Date.now();
|
|
38
77
|
const rows = [];
|
|
@@ -70,7 +109,7 @@ export function createJobViews({
|
|
|
70
109
|
tools: Array.isArray(session.tools) ? session.tools.length : Math.max(0, Number(session.toolCount || 0)),
|
|
71
110
|
});
|
|
72
111
|
}
|
|
73
|
-
return rows;
|
|
112
|
+
return includeTerminal ? rows : rows.filter((row) => isActiveWorkerRow(row));
|
|
74
113
|
}
|
|
75
114
|
|
|
76
115
|
function sessionProgressExtras(sessionId, role, now = Date.now(), taskStatus = null) {
|
|
@@ -115,25 +154,32 @@ export function createJobViews({
|
|
|
115
154
|
|
|
116
155
|
function listJobs(context = {}) {
|
|
117
156
|
const wantedPid = terminalPidForContext(context);
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
const rows = listBackgroundTasks({ surface: 'agent', context }).map((task) => {
|
|
159
|
+
const head = {
|
|
160
|
+
task_id: task.task_id,
|
|
161
|
+
type: task.operation,
|
|
162
|
+
status: task.status,
|
|
163
|
+
tag: task.tag || null,
|
|
164
|
+
sessionId: task.sessionId || null,
|
|
165
|
+
agent: task.agent || null,
|
|
166
|
+
preset: task.preset || null,
|
|
167
|
+
provider: task.provider || null,
|
|
168
|
+
model: task.model || null,
|
|
169
|
+
effort: task.effort || null,
|
|
170
|
+
fast: task.fast === true || task.fast === false ? task.fast : null,
|
|
171
|
+
maxLoopIterations: task.maxLoopIterations || null,
|
|
172
|
+
startedAt: task.startedAt,
|
|
173
|
+
finishedAt: task.finishedAt || null,
|
|
174
|
+
error: task.error || null,
|
|
175
|
+
};
|
|
176
|
+
if (isTerminalJobStatus(task.status)) return { ...head, ...terminalJobFrozenFields(task.status, now) };
|
|
177
|
+
return {
|
|
178
|
+
...head,
|
|
179
|
+
...jobWorkerSnapshot(task.sessionId),
|
|
180
|
+
...sessionProgressExtras(task.sessionId, task.agent || null, now, task.status),
|
|
181
|
+
};
|
|
182
|
+
});
|
|
137
183
|
return wantedPid
|
|
138
184
|
? rows.filter((row) => {
|
|
139
185
|
const pid = positiveInt(row.clientHostPid);
|
|
@@ -221,20 +267,7 @@ export function createJobViews({
|
|
|
221
267
|
|
|
222
268
|
function renderJob(job, includeResult = false) {
|
|
223
269
|
const meta = job.meta || {};
|
|
224
|
-
|
|
225
|
-
// Spawn is deferred: before the worker session exists, sessionProgressExtras
|
|
226
|
-
// returns {} and the status card would show only "status: running" with no
|
|
227
|
-
// stage/progress. Fill a minimal stage so the caller can tell the job is
|
|
228
|
-
// still spinning up rather than silently stalled.
|
|
229
|
-
if (!meta.sessionId && (!progress || Object.keys(progress).length === 0)) {
|
|
230
|
-
const spawning = job.status === 'running';
|
|
231
|
-
progress = {
|
|
232
|
-
worker_stage: spawning ? 'spawning' : (job.status || 'unknown'),
|
|
233
|
-
last_progress: spawning ? 'spawning worker session' : (job.status || 'unknown'),
|
|
234
|
-
diagnostic: spawning ? 'worker session not started yet' : (job.status || 'unknown'),
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
return {
|
|
270
|
+
const head = {
|
|
238
271
|
task_id: job.taskId,
|
|
239
272
|
type: job.operation,
|
|
240
273
|
status: job.status,
|
|
@@ -251,9 +284,30 @@ export function createJobViews({
|
|
|
251
284
|
startedAt: job.startedAt,
|
|
252
285
|
finishedAt: job.finishedAt || null,
|
|
253
286
|
error: job.error || null,
|
|
287
|
+
};
|
|
288
|
+
const tail = includeResult && job.result !== undefined ? { result: job.result } : {};
|
|
289
|
+
// Frozen terminal view: never re-read the (possibly reused) live session.
|
|
290
|
+
if (isTerminalJobStatus(job.status)) {
|
|
291
|
+
return { ...head, ...terminalJobFrozenFields(job.status), ...tail };
|
|
292
|
+
}
|
|
293
|
+
let progress = sessionProgressExtras(meta.sessionId, meta.agent || null, Date.now(), job.status);
|
|
294
|
+
// Spawn is deferred: before the worker session exists, sessionProgressExtras
|
|
295
|
+
// returns {} and the status card would show only "status: running" with no
|
|
296
|
+
// stage/progress. Fill a minimal stage so the caller can tell the job is
|
|
297
|
+
// still spinning up rather than silently stalled.
|
|
298
|
+
if (!meta.sessionId && (!progress || Object.keys(progress).length === 0)) {
|
|
299
|
+
const spawning = job.status === 'running';
|
|
300
|
+
progress = {
|
|
301
|
+
worker_stage: spawning ? 'spawning' : (job.status || 'unknown'),
|
|
302
|
+
last_progress: spawning ? 'spawning worker session' : (job.status || 'unknown'),
|
|
303
|
+
diagnostic: spawning ? 'worker session not started yet' : (job.status || 'unknown'),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
...head,
|
|
254
308
|
...jobWorkerSnapshot(meta.sessionId),
|
|
255
309
|
...progress,
|
|
256
|
-
...
|
|
310
|
+
...tail,
|
|
257
311
|
};
|
|
258
312
|
}
|
|
259
313
|
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
// Spawn/job lifecycle, extracted from agent-tool.mjs: job start (foreground
|
|
2
2
|
// deferral, admission control), the progress-idle watchdogs, spawn prep
|
|
3
|
-
// (
|
|
3
|
+
// (provider/session preparation), and the full runSpawn
|
|
4
4
|
// execution with turn-review collection and terminal accounting.
|
|
5
|
-
import { clean, clearAgentStatuslineRoute, nonNegativeInt, normalizeAgentName, positiveInt, presetKey, readAgentFrontmatterPermission, resolvePrompt,
|
|
5
|
+
import { clean, clearAgentStatuslineRoute, nonNegativeInt, normalizeAgentName, positiveInt, presetKey, readAgentFrontmatterPermission, resolvePrompt, terminalPidForContext, withCwdHeader, writeAgentStatuslineRoute } from './helpers.mjs';
|
|
6
6
|
import { createNotify } from './notify.mjs';
|
|
7
7
|
import { reconcileBackgroundTask, sanitizeTaskMeta, startBackgroundTask } from '../../runtime/shared/background-tasks.mjs';
|
|
8
8
|
import { abnormalEmptyFinishError, renderResult } from './render.mjs';
|
|
9
|
-
import { agentShardEnabled, shardRequest } from '../agent-shard/shard-pool.mjs';
|
|
10
9
|
import { resourceAdmission } from '../../runtime/shared/resource-admission.mjs';
|
|
11
10
|
import { createProgressWatchdogRegistry } from '../agent-watchdog-registry.mjs';
|
|
12
|
-
import { isTerminalWorkerStatus } from './worker-rows.mjs';
|
|
13
11
|
import { resolve } from 'node:path';
|
|
14
12
|
import { normalizeAgentPermission } from '../../runtime/shared/markdown-frontmatter.mjs';
|
|
15
13
|
import { resolveAgentSpawnPreset } from './spawn-preset.mjs';
|
|
@@ -25,7 +23,6 @@ export function createSpawnFlow({
|
|
|
25
23
|
preparedSpawnMeta,
|
|
26
24
|
upsertWorkerSessionDeferred,
|
|
27
25
|
refreshTagsFromSessions,
|
|
28
|
-
readWorkerRows,
|
|
29
26
|
defaultCwd,
|
|
30
27
|
nextTag,
|
|
31
28
|
cancelReap,
|
|
@@ -74,17 +71,6 @@ export function createSpawnFlow({
|
|
|
74
71
|
if (currentMeta?.sessionId) {
|
|
75
72
|
try { mgr.closeSession(currentMeta.sessionId, 'agent-task-cancel'); } catch {}
|
|
76
73
|
}
|
|
77
|
-
// Sharded spawn: the live session exists only in the shard child —
|
|
78
|
-
// forward the cancel by sessionId (known after the `spawned` progress
|
|
79
|
-
// event) or tag.
|
|
80
|
-
if (agentShardEnabled() && (currentMeta?.tag || currentMeta?.sessionId)) {
|
|
81
|
-
try {
|
|
82
|
-
void shardRequest('cancel', {
|
|
83
|
-
tag: currentMeta?.tag || null,
|
|
84
|
-
sessionId: currentMeta?.sessionId || null,
|
|
85
|
-
}).catch(() => {});
|
|
86
|
-
} catch { /* best-effort */ }
|
|
87
|
-
}
|
|
88
74
|
},
|
|
89
75
|
run: async () => {
|
|
90
76
|
const lease = await resourceAdmission.acquire('agent', {
|
|
@@ -109,13 +95,6 @@ export function createSpawnFlow({
|
|
|
109
95
|
function startDeferredSpawnJob(args, callerCwd, context, notifyContext, extras = {}) {
|
|
110
96
|
return startJob('spawn', pendingSpawnMeta(args, extras), async (job) => {
|
|
111
97
|
if (job?.status === 'cancelled') return null;
|
|
112
|
-
// P1 agent sharding (MIXDOG_AGENT_SHARD=1, default off): run the worker
|
|
113
|
-
// session in a shard child process instead of this event loop. Prompt
|
|
114
|
-
// resolution stays here (cheap); prep + the full agent loop run in the
|
|
115
|
-
// shard. Tag/index/watchdog parity lands in P2 (docs/agent-shard-design.md).
|
|
116
|
-
if (agentShardEnabled()) {
|
|
117
|
-
return await runShardSpawn(args, callerCwd, context, job);
|
|
118
|
-
}
|
|
119
98
|
// prepareSpawn (ensureProvider/prepareAgentSession) runs before runSpawn
|
|
120
99
|
// installs its progress watchdog, so guard prep with an internal env-
|
|
121
100
|
// backed cap rather than exposing per-call timeout knobs on the agent
|
|
@@ -158,6 +137,7 @@ export function createSpawnFlow({
|
|
|
158
137
|
stage: 'running',
|
|
159
138
|
task_id: job.taskId,
|
|
160
139
|
startedAt: job.startedAt,
|
|
140
|
+
turnStartedAt: new Date().toISOString(),
|
|
161
141
|
});
|
|
162
142
|
if (job?.status === 'cancelled') {
|
|
163
143
|
closePreparedSpawn(prepared);
|
|
@@ -170,202 +150,26 @@ export function createSpawnFlow({
|
|
|
170
150
|
// Idle-progress watchdogs (shared sweep timer) live in
|
|
171
151
|
// agent-watchdog-registry.mjs.
|
|
172
152
|
const progressWatchdogs = createProgressWatchdogRegistry({ mgr });
|
|
173
|
-
const startProgressIdleWatchdog = (sessionId, watchdogPolicy, agent = null) =>
|
|
174
|
-
progressWatchdogs.start(sessionId, watchdogPolicy, agent);
|
|
153
|
+
const startProgressIdleWatchdog = (sessionId, watchdogPolicy, agent = null, options = {}) =>
|
|
154
|
+
progressWatchdogs.start(sessionId, watchdogPolicy, agent, options);
|
|
155
|
+
|
|
156
|
+
// Queued `agent type=send` prompts run as follow-up turns INSIDE one
|
|
157
|
+
// askSession call, so the dispatch stamp alone would keep the panel elapsed
|
|
158
|
+
// growing across them. The watchdog sweep reports each turn's start; re-stamp
|
|
159
|
+
// the worker-index row so elapsed resets at the queued-turn boundary.
|
|
160
|
+
const turnStartStamper = (session, tag) => (turnStartedAtMs) => {
|
|
161
|
+
upsertWorkerSessionDeferred(session, tag, {
|
|
162
|
+
status: 'running',
|
|
163
|
+
stage: 'running',
|
|
164
|
+
turnStartedAt: new Date(turnStartedAtMs).toISOString(),
|
|
165
|
+
});
|
|
166
|
+
};
|
|
175
167
|
|
|
176
168
|
async function prepareSpawn(args, callerCwd = null, context = {}, prepState = null) {
|
|
177
|
-
// (sharded spawns never reach here — see startDeferredSpawnJob)
|
|
178
169
|
refreshTagsFromSessions({ context });
|
|
179
170
|
return prepareSpawnInProcess(args, callerCwd, context, prepState);
|
|
180
171
|
}
|
|
181
172
|
|
|
182
|
-
// Tag-reuse routing for sharded workers (P2): a same-tag spawn/send while
|
|
183
|
-
// the shard runs the session mirrors the in-process busy-session enqueue.
|
|
184
|
-
// Discriminator: a non-terminal worker-index row whose session has no live
|
|
185
|
-
// runtime in THIS process. (mgr.getSession must not be used here — it
|
|
186
|
-
// falls back to the on-disk session file the shard child persists, which
|
|
187
|
-
// makes a shard-owned session look lead-live.) Returns the rendered queued
|
|
188
|
-
// result, or null to fall through to the normal path (unknown tag,
|
|
189
|
-
// terminal row, lead-owned runtime, or the shard no longer owns the
|
|
190
|
-
// session — e.g. after a shard crash, where respawn is the correct
|
|
191
|
-
// recovery).
|
|
192
|
-
async function tryShardEnqueue(target, args, context, extras = {}) {
|
|
193
|
-
if (!agentShardEnabled()) return null;
|
|
194
|
-
const value = clean(target);
|
|
195
|
-
if (!value) return null;
|
|
196
|
-
const row = readWorkerRows(context).find((r) => (
|
|
197
|
-
(clean(r.tag) === value || clean(r.sessionId) === value)
|
|
198
|
-
&& rowMatchesContext(r, context)
|
|
199
|
-
&& !isTerminalWorkerStatus(r.status || r.stage)
|
|
200
|
-
));
|
|
201
|
-
if (!row) return null;
|
|
202
|
-
const rowSessionId = clean(row.sessionId);
|
|
203
|
-
const runtime = rowSessionId ? mgr.getSessionRuntime?.(rowSessionId) : null;
|
|
204
|
-
if (runtime?.controller?.signal && !runtime.controller.signal.aborted) return null;
|
|
205
|
-
const prompt = await resolvePrompt(args, clean(row.cwd) || defaultCwd || process.cwd());
|
|
206
|
-
if (!clean(prompt)) return null;
|
|
207
|
-
let queued = null;
|
|
208
|
-
try {
|
|
209
|
-
queued = await shardRequest('enqueue', {
|
|
210
|
-
tag: clean(row.tag) || null,
|
|
211
|
-
sessionId: rowSessionId || null,
|
|
212
|
-
prompt,
|
|
213
|
-
});
|
|
214
|
-
} catch { return null; }
|
|
215
|
-
if (queued?.queued !== true) return null;
|
|
216
|
-
return renderResult({
|
|
217
|
-
queued: true,
|
|
218
|
-
...extras,
|
|
219
|
-
tag: clean(row.tag) || null,
|
|
220
|
-
sessionId: rowSessionId || null,
|
|
221
|
-
agent: clean(row.agent) || null,
|
|
222
|
-
queueDepth: queued.queueDepth || 1,
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async function runShardSpawn(args, callerCwd, context, job) {
|
|
227
|
-
const agent = normalizeAgentName(args.agent);
|
|
228
|
-
if (!agent) throw new Error('agent spawn: agent is required');
|
|
229
|
-
const tag = clean(args.tag) || nextTag(agent, context);
|
|
230
|
-
const baseCwd = resolve(callerCwd || defaultCwd || process.cwd());
|
|
231
|
-
const workerCwd = clean(args.cwd) ? resolve(baseCwd, args.cwd) : baseCwd;
|
|
232
|
-
const prompt = withCwdHeader(await resolvePrompt(args, workerCwd), workerCwd);
|
|
233
|
-
const clientHostPid = terminalPidForContext(context) || null;
|
|
234
|
-
// P2 progress parity: the shard reports the bound session the moment it
|
|
235
|
-
// exists (`spawned`), so the lead mirrors its in-process spawn
|
|
236
|
-
// bookkeeping — tag maps, worker-index row, statusline route, job meta,
|
|
237
|
-
// SubagentStart — while the worker loop runs in the child.
|
|
238
|
-
let spawned = null;
|
|
239
|
-
const onSpawned = (payload = {}) => {
|
|
240
|
-
if (spawned || !payload.sessionId) return;
|
|
241
|
-
spawned = payload;
|
|
242
|
-
const sessionLike = {
|
|
243
|
-
id: payload.sessionId,
|
|
244
|
-
agent,
|
|
245
|
-
cwd: payload.cwd || workerCwd,
|
|
246
|
-
provider: payload.provider || null,
|
|
247
|
-
model: payload.model || null,
|
|
248
|
-
presetName: payload.presetName || null,
|
|
249
|
-
effort: payload.effort || null,
|
|
250
|
-
fast: payload.fast === true,
|
|
251
|
-
clientHostPid,
|
|
252
|
-
status: 'running',
|
|
253
|
-
};
|
|
254
|
-
cancelReap(payload.sessionId);
|
|
255
|
-
writeAgentStatuslineRoute(payload.sessionId, {
|
|
256
|
-
provider: payload.provider || null,
|
|
257
|
-
model: payload.model || null,
|
|
258
|
-
effort: payload.effort || null,
|
|
259
|
-
fast: payload.fast === true,
|
|
260
|
-
});
|
|
261
|
-
bindTag(tag, sessionLike, {
|
|
262
|
-
agent,
|
|
263
|
-
preset: payload.presetName || null,
|
|
264
|
-
provider: payload.provider || null,
|
|
265
|
-
model: payload.model || null,
|
|
266
|
-
effort: payload.effort || null,
|
|
267
|
-
fast: payload.fast === true,
|
|
268
|
-
status: 'running',
|
|
269
|
-
stage: 'running',
|
|
270
|
-
});
|
|
271
|
-
if (job) {
|
|
272
|
-
mergeJobMeta(job, {
|
|
273
|
-
tag,
|
|
274
|
-
sessionId: payload.sessionId,
|
|
275
|
-
agent,
|
|
276
|
-
preset: payload.presetName || null,
|
|
277
|
-
provider: payload.provider || null,
|
|
278
|
-
model: payload.model || null,
|
|
279
|
-
effort: payload.effort || null,
|
|
280
|
-
fast: payload.fast === true,
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
emitSubagentEvent('start', agent, { session_id: payload.sessionId, tag });
|
|
284
|
-
// Cancel raced the shard prep: the task is already terminal on the lead
|
|
285
|
-
// but the child just started its loop — abort it now that the sessionId
|
|
286
|
-
// is known, instead of letting it burn tokens to completion.
|
|
287
|
-
if (job?.status === 'cancelled') {
|
|
288
|
-
try { void shardRequest('cancel', { tag, sessionId: payload.sessionId }).catch(() => {}); } catch { /* best-effort */ }
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
let finalStatus = 'error';
|
|
292
|
-
try {
|
|
293
|
-
const response = await shardRequest('spawn', {
|
|
294
|
-
args: {
|
|
295
|
-
agent,
|
|
296
|
-
provider: args.provider,
|
|
297
|
-
model: args.model,
|
|
298
|
-
effort: args.effort,
|
|
299
|
-
fast: args.fast,
|
|
300
|
-
preset: args.preset,
|
|
301
|
-
},
|
|
302
|
-
tag,
|
|
303
|
-
cwd: workerCwd,
|
|
304
|
-
prompt,
|
|
305
|
-
context: args.context || null,
|
|
306
|
-
parentSessionId: clean(context?.callerSessionId || context?.sessionId) || null,
|
|
307
|
-
ownerSessionId: clean(context?.callerSessionId || context?.sessionId) || null,
|
|
308
|
-
clientHostPid,
|
|
309
|
-
}, {
|
|
310
|
-
onProgress: (event, payload) => { if (event === 'spawned') onSpawned(payload); },
|
|
311
|
-
});
|
|
312
|
-
// Same abnormal-empty promotion as the in-process completionValue; a
|
|
313
|
-
// watchdog partial handoff (stallAbort) is a valid completion and is
|
|
314
|
-
// exempt, matching runSpawn's catch path.
|
|
315
|
-
const abnormalError = response?.stallAbort === true
|
|
316
|
-
? null
|
|
317
|
-
: abnormalEmptyFinishError(response, agent);
|
|
318
|
-
const value = {
|
|
319
|
-
tag,
|
|
320
|
-
sessionId: response.sessionId,
|
|
321
|
-
agent,
|
|
322
|
-
preset: response.presetName || null,
|
|
323
|
-
provider: response.provider || null,
|
|
324
|
-
model: response.model || null,
|
|
325
|
-
effort: response.effort || null,
|
|
326
|
-
fast: response.fast === true,
|
|
327
|
-
content: response.content || '',
|
|
328
|
-
...(response.stallAbort === true ? { stallAbort: true } : {}),
|
|
329
|
-
...(abnormalError ? { error: abnormalError } : {}),
|
|
330
|
-
};
|
|
331
|
-
if (job) job._terminalResultValue = value;
|
|
332
|
-
if (value.error) throw new Error(value.error);
|
|
333
|
-
finalStatus = 'idle';
|
|
334
|
-
return value;
|
|
335
|
-
} finally {
|
|
336
|
-
// Terminal bookkeeping parity with runSpawn's finally: worker-index row
|
|
337
|
-
// transitions to idle/error, the terminal-reap timer starts (tombstone +
|
|
338
|
-
// statusline cleanup), and SubagentStop fires. Only after `spawned` —
|
|
339
|
-
// a prep failure inside the shard bound nothing on the lead.
|
|
340
|
-
if (spawned?.sessionId) {
|
|
341
|
-
upsertWorkerSessionDeferred({
|
|
342
|
-
id: spawned.sessionId,
|
|
343
|
-
agent,
|
|
344
|
-
cwd: spawned.cwd || workerCwd,
|
|
345
|
-
provider: spawned.provider || null,
|
|
346
|
-
model: spawned.model || null,
|
|
347
|
-
presetName: spawned.presetName || null,
|
|
348
|
-
effort: spawned.effort || null,
|
|
349
|
-
fast: spawned.fast === true,
|
|
350
|
-
clientHostPid,
|
|
351
|
-
status: finalStatus,
|
|
352
|
-
}, tag, {
|
|
353
|
-
agent,
|
|
354
|
-
preset: spawned.presetName || null,
|
|
355
|
-
provider: spawned.provider || null,
|
|
356
|
-
model: spawned.model || null,
|
|
357
|
-
effort: spawned.effort || null,
|
|
358
|
-
fast: spawned.fast === true,
|
|
359
|
-
status: finalStatus,
|
|
360
|
-
stage: finalStatus,
|
|
361
|
-
finishedAt: new Date().toISOString(),
|
|
362
|
-
});
|
|
363
|
-
scheduleReap(spawned.sessionId, spawned.provider || null);
|
|
364
|
-
emitSubagentEvent('stop', agent, { session_id: spawned.sessionId, tag, status: finalStatus });
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
173
|
async function prepareSpawnInProcess(args, callerCwd = null, context = {}, prepState = null) {
|
|
370
174
|
const config = cfgMod.loadConfig();
|
|
371
175
|
const agent = normalizeAgentName(args.agent);
|
|
@@ -442,7 +246,9 @@ export function createSpawnFlow({
|
|
|
442
246
|
|
|
443
247
|
async function runSpawn(prepared, notifyContext = null, job = null) {
|
|
444
248
|
const { args, tag, session, agent, preset, presetName, workerCwd, prompt, watchdogPolicy } = prepared;
|
|
445
|
-
const watchdog = startProgressIdleWatchdog(session.id, watchdogPolicy, agent
|
|
249
|
+
const watchdog = startProgressIdleWatchdog(session.id, watchdogPolicy, agent, {
|
|
250
|
+
onTurnStart: turnStartStamper(session, tag),
|
|
251
|
+
});
|
|
446
252
|
const turnReview = createTurnReviewCollector(session, tag, agent, notifyContext || {});
|
|
447
253
|
let finalStatus = 'idle';
|
|
448
254
|
// SubagentStart: a worker session is about to run its first turn.
|
|
@@ -456,6 +262,7 @@ export function createSpawnFlow({
|
|
|
456
262
|
fast: preset.fast === true,
|
|
457
263
|
status: 'running',
|
|
458
264
|
stage: 'running',
|
|
265
|
+
turnStartedAt: new Date().toISOString(),
|
|
459
266
|
});
|
|
460
267
|
let handoffMsgStart = 0;
|
|
461
268
|
try {
|
|
@@ -609,6 +416,9 @@ export function createSpawnFlow({
|
|
|
609
416
|
scheduleReap(session.id);
|
|
610
417
|
// SubagentStop: worker finished (terminal), regardless of outcome.
|
|
611
418
|
emitSubagentEvent('stop', agent, { session_id: session.id, tag, status: finalStatus });
|
|
419
|
+
// Keep the durable session/tag for same-tag follow-ups, but release every
|
|
420
|
+
// process-local provider, shell, watchdog and tool cache immediately.
|
|
421
|
+
try { mgr.unloadSessionRuntime?.(session.id, 'agent-turn-complete'); } catch {}
|
|
612
422
|
}
|
|
613
423
|
}
|
|
614
424
|
|
|
@@ -620,9 +430,8 @@ export function createSpawnFlow({
|
|
|
620
430
|
startDeferredSpawnJob,
|
|
621
431
|
progressWatchdogs,
|
|
622
432
|
startProgressIdleWatchdog,
|
|
433
|
+
turnStartStamper,
|
|
623
434
|
prepareSpawn,
|
|
624
|
-
tryShardEnqueue,
|
|
625
|
-
runShardSpawn,
|
|
626
435
|
prepareSpawnInProcess,
|
|
627
436
|
runSpawn,
|
|
628
437
|
};
|
|
@@ -54,6 +54,7 @@ export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCw
|
|
|
54
54
|
.map((row) => ({
|
|
55
55
|
tag: clean(row.tag),
|
|
56
56
|
sessionId: clean(row.sessionId),
|
|
57
|
+
ownerSessionId: clean(row.ownerSessionId || row.parentSessionId) || null,
|
|
57
58
|
agent: clean(row.agent) || null,
|
|
58
59
|
provider: clean(row.provider) || null,
|
|
59
60
|
model: clean(row.model) || null,
|
|
@@ -66,6 +67,7 @@ export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCw
|
|
|
66
67
|
updatedAt: clean(row.updatedAt) || null,
|
|
67
68
|
lastUsedAt: clean(row.lastUsedAt) || null,
|
|
68
69
|
finishedAt: clean(row.finishedAt) || null,
|
|
70
|
+
turnStartedAt: clean(row.turnStartedAt) || null,
|
|
69
71
|
clientHostPid: positiveInt(row.clientHostPid),
|
|
70
72
|
cwd: clean(row.cwd) || null,
|
|
71
73
|
task_id: clean(row.task_id || row.taskId) || null,
|
|
@@ -199,6 +201,9 @@ export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCw
|
|
|
199
201
|
return {
|
|
200
202
|
tag,
|
|
201
203
|
sessionId,
|
|
204
|
+
ownerSessionId: clean(extra.ownerSessionId || extra.parentSessionId)
|
|
205
|
+
|| clean(session?.ownerSessionId || session?.parentSessionId)
|
|
206
|
+
|| null,
|
|
202
207
|
agent: clean(extra.agent) || clean(session?.agent) || null,
|
|
203
208
|
provider: clean(extra.provider) || clean(session?.provider) || null,
|
|
204
209
|
model: clean(extra.model) || clean(session?.model) || null,
|
|
@@ -211,6 +216,9 @@ export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCw
|
|
|
211
216
|
updatedAt: clean(extra.updatedAt) || nowIso,
|
|
212
217
|
lastUsedAt: clean(session?.lastUsedAt) || null,
|
|
213
218
|
finishedAt: clean(extra.finishedAt) || null,
|
|
219
|
+
// Turn dispatch stamps this; terminal upserts leave it null and the
|
|
220
|
+
// merge in applyWorkerRowUpsert preserves the running turn's value.
|
|
221
|
+
turnStartedAt: clean(extra.turnStartedAt) || null,
|
|
214
222
|
clientHostPid: positiveInt(extra.clientHostPid) || positiveInt(session?.clientHostPid),
|
|
215
223
|
cwd: clean(session?.cwd) || clean(extra.cwd) || null,
|
|
216
224
|
task_id: clean(extra.task_id || extra.taskId) || null,
|
|
@@ -56,7 +56,7 @@ export function applyWorkerRowUpsert(byKey, normalized) {
|
|
|
56
56
|
if (!key) return;
|
|
57
57
|
const prev = byKey.get(key) || {};
|
|
58
58
|
const merged = { ...prev, ...normalized };
|
|
59
|
-
for (const field of ['agent', 'provider', 'model', 'preset', 'effort', 'fast', 'clientHostPid', 'cwd', 'task_id', 'permission', 'toolPermission']) {
|
|
59
|
+
for (const field of ['ownerSessionId', 'agent', 'provider', 'model', 'preset', 'effort', 'fast', 'clientHostPid', 'cwd', 'task_id', 'permission', 'toolPermission', 'turnStartedAt']) {
|
|
60
60
|
if ((merged[field] === null || merged[field] === '') && prev[field] != null && prev[field] !== '') {
|
|
61
61
|
merged[field] = prev[field];
|
|
62
62
|
}
|
|
@@ -71,6 +71,7 @@ export function applyWorkerRowUpsert(byKey, normalized) {
|
|
|
71
71
|
export function workerRowToSession(row = {}) {
|
|
72
72
|
return {
|
|
73
73
|
id: row.sessionId,
|
|
74
|
+
ownerSessionId: row.ownerSessionId || null,
|
|
74
75
|
agentTag: row.tag,
|
|
75
76
|
agent: row.agent || null,
|
|
76
77
|
provider: row.provider || null,
|
|
@@ -62,7 +62,6 @@ import {
|
|
|
62
62
|
} from './agent-tool/worker-rows.mjs';
|
|
63
63
|
import { resolveAgentTerminalReapMs } from '../session-runtime/config-helpers.mjs';
|
|
64
64
|
import { createWorkerIndex } from './agent-tool/worker-index.mjs';
|
|
65
|
-
import { agentShardEnabled, shardRequest } from './agent-shard/shard-pool.mjs';
|
|
66
65
|
import {
|
|
67
66
|
beginAgentTurnReview,
|
|
68
67
|
completeAgentTurnReview,
|
|
@@ -192,9 +191,8 @@ export function createStandaloneAgent({
|
|
|
192
191
|
startDeferredSpawnJob,
|
|
193
192
|
progressWatchdogs,
|
|
194
193
|
startProgressIdleWatchdog,
|
|
194
|
+
turnStartStamper,
|
|
195
195
|
prepareSpawn,
|
|
196
|
-
tryShardEnqueue,
|
|
197
|
-
runShardSpawn,
|
|
198
196
|
prepareSpawnInProcess,
|
|
199
197
|
runSpawn,
|
|
200
198
|
} = createSpawnFlow({
|
|
@@ -242,11 +240,19 @@ export function createStandaloneAgent({
|
|
|
242
240
|
async function runSend(prepared, notifyContext = null, job = null) {
|
|
243
241
|
const { args, session, sessionId, prompt } = prepared;
|
|
244
242
|
const sendAgent = session.agent || normalizeAgentName(args.agent);
|
|
245
|
-
const watchdog = startProgressIdleWatchdog(sessionId, resolveAgentWatchdogPolicy(sendAgent), sendAgent);
|
|
246
243
|
const tag = tagForSession(sessionId);
|
|
244
|
+
// Queued sends run as follow-up turns inside this same askSession call;
|
|
245
|
+
// the watchdog sweep re-stamps turnStartedAt at each turn boundary.
|
|
246
|
+
const watchdog = startProgressIdleWatchdog(sessionId, resolveAgentWatchdogPolicy(sendAgent), sendAgent, {
|
|
247
|
+
onTurnStart: turnStartStamper(session, tag),
|
|
248
|
+
});
|
|
247
249
|
const turnReview = createTurnReviewCollector(session, tag, sendAgent, notifyContext || {});
|
|
248
250
|
let finalStatus = 'idle';
|
|
249
|
-
upsertWorkerSessionDeferred(session, tag, {
|
|
251
|
+
upsertWorkerSessionDeferred(session, tag, {
|
|
252
|
+
status: 'running',
|
|
253
|
+
stage: 'running',
|
|
254
|
+
turnStartedAt: new Date().toISOString(),
|
|
255
|
+
});
|
|
250
256
|
let handoffMsgStart = 0;
|
|
251
257
|
try {
|
|
252
258
|
const completionValue = (result) => {
|
|
@@ -366,6 +372,9 @@ export function createStandaloneAgent({
|
|
|
366
372
|
} catch {}
|
|
367
373
|
}
|
|
368
374
|
scheduleReap(sessionId);
|
|
375
|
+
// Same lifecycle as a fresh spawn: the transcript/tag stays resumable,
|
|
376
|
+
// while heavy process-local runtime state is reclaimed immediately.
|
|
377
|
+
try { mgr.unloadSessionRuntime?.(sessionId, 'agent-turn-complete'); } catch {}
|
|
369
378
|
}
|
|
370
379
|
}
|
|
371
380
|
|
|
@@ -458,7 +467,9 @@ export function createStandaloneAgent({
|
|
|
458
467
|
tasksRemoved: cleaned.removed,
|
|
459
468
|
tagsRemoved: beforeTags - tags.size,
|
|
460
469
|
tasks: listJobs(scopedContext).length,
|
|
461
|
-
|
|
470
|
+
// Cleanup reports how many worker rows remain known (idle/terminal
|
|
471
|
+
// included); only the /agents worker section hides terminal rows.
|
|
472
|
+
workers: list({ scanSessions: wantsSessionScan(args), context: scopedContext, includeTerminal: true }).length,
|
|
462
473
|
};
|
|
463
474
|
}
|
|
464
475
|
|
|
@@ -537,11 +548,6 @@ export function createStandaloneAgent({
|
|
|
537
548
|
if (type === 'cancel') return renderResult(close(args, scopedContext));
|
|
538
549
|
if (type === 'close') return renderResult(close(args, scopedContext));
|
|
539
550
|
if (type === 'send') {
|
|
540
|
-
// Sharded busy worker: queue the follow-up into the shard-owned live
|
|
541
|
-
// session (in-process busy sessions are handled below via
|
|
542
|
-
// dispatchToExistingSession; their lead session resolves live).
|
|
543
|
-
const shardQueued = await tryShardEnqueue(clean(args.tag || args.sessionId), args, scopedContext);
|
|
544
|
-
if (shardQueued) return shardQueued;
|
|
545
551
|
try {
|
|
546
552
|
const prepared = await prepareSend(args, scopedContext);
|
|
547
553
|
return dispatchToExistingSession(prepared, notifyContext);
|
|
@@ -614,12 +620,6 @@ export function createStandaloneAgent({
|
|
|
614
620
|
let respawned = false;
|
|
615
621
|
let spawnArgs = args;
|
|
616
622
|
if (explicitTag) {
|
|
617
|
-
// Sharded busy worker under this tag: queue into the shard-owned
|
|
618
|
-
// live session BEFORE the live-reuse resolve — the lead's disk-read
|
|
619
|
-
// session view would otherwise misroute this into an in-process
|
|
620
|
-
// send against a shard-owned session file (dual writer).
|
|
621
|
-
const shardQueued = await tryShardEnqueue(explicitTag, args, scopedContext, { reused: true });
|
|
622
|
-
if (shardQueued) return shardQueued;
|
|
623
623
|
// Resolve a LIVE same-tag session in this terminal (busy or idle).
|
|
624
624
|
let liveSessionId = null;
|
|
625
625
|
try {
|
|
@@ -32,6 +32,15 @@ export function createProgressWatchdogRegistry({ mgr }) {
|
|
|
32
32
|
const snapshot = typeof mgr.getSessionProgressSnapshot === 'function'
|
|
33
33
|
? mgr.getSessionProgressSnapshot(sessionId)
|
|
34
34
|
: null;
|
|
35
|
+
// Turn boundary: askStartedAt is re-stamped by markSessionAskStart for
|
|
36
|
+
// EVERY turn of one askSession call, including the follow-up turns drained
|
|
37
|
+
// from the pending-message queue. The sweep already reads this snapshot, so
|
|
38
|
+
// observing the change is the cheapest per-turn signal available here.
|
|
39
|
+
const askStartedAt = Number(snapshot?.askStartedAt || 0);
|
|
40
|
+
if (askStartedAt > 0 && askStartedAt !== state.lastAskStartedAt) {
|
|
41
|
+
state.lastAskStartedAt = askStartedAt;
|
|
42
|
+
try { state.onTurnStart?.(askStartedAt); } catch { /* bookkeeping only */ }
|
|
43
|
+
}
|
|
35
44
|
const abortErr = snapshot ? evaluateAgentWatchdogAbort(snapshot, now, watchdogPolicy) : null;
|
|
36
45
|
const sess = typeof mgr.getSession === 'function' ? mgr.getSession(sessionId) : null;
|
|
37
46
|
const iteration = typeof sess?.lastIterationIndex === 'number' ? sess.lastIterationIndex : null;
|
|
@@ -79,7 +88,7 @@ export function createProgressWatchdogRegistry({ mgr }) {
|
|
|
79
88
|
return {
|
|
80
89
|
/** Watch a session until the returned handle is stopped; null when the
|
|
81
90
|
* policy is off or the manager cannot link an abort signal. */
|
|
82
|
-
start(sessionId, watchdogPolicy, agent = null) {
|
|
91
|
+
start(sessionId, watchdogPolicy, agent = null, { onTurnStart = null } = {}) {
|
|
83
92
|
if (!sessionId || !agentWatchdogPolicyActive(watchdogPolicy)) return null;
|
|
84
93
|
if (typeof mgr.getSessionProgressSnapshot !== 'function'
|
|
85
94
|
&& typeof mgr.getSessionLastProgressAt !== 'function') return null;
|
|
@@ -87,7 +96,15 @@ export function createProgressWatchdogRegistry({ mgr }) {
|
|
|
87
96
|
const controller = new AbortController();
|
|
88
97
|
const anchorTs = Date.now();
|
|
89
98
|
try { mgr.linkParentSignalToSession(sessionId, controller.signal); } catch { return null; }
|
|
90
|
-
const state = {
|
|
99
|
+
const state = {
|
|
100
|
+
sessionId,
|
|
101
|
+
watchdogPolicy,
|
|
102
|
+
agent,
|
|
103
|
+
controller,
|
|
104
|
+
anchorTs,
|
|
105
|
+
onTurnStart: typeof onTurnStart === 'function' ? onTurnStart : null,
|
|
106
|
+
lastAskStartedAt: 0,
|
|
107
|
+
};
|
|
91
108
|
watched.set(sessionId, state);
|
|
92
109
|
ensureTimer();
|
|
93
110
|
return {
|