mixdog 0.9.147 → 0.9.148
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 +23 -3
- package/scripts/build-voice-runtime-unix.sh +101 -0
- package/scripts/embedding-quality-probe.mjs +96 -0
- package/scripts/generate-voice-runtime-manifest.mjs +300 -0
- package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
- package/scripts/native-binary-arch.mjs +30 -13
- package/scripts/native-binary-arch.test.mjs +40 -4
- package/scripts/plan-voice-runtime-release.mjs +73 -0
- package/scripts/prepare-native-assets.mjs +50 -9
- package/scripts/prune-embedding-runtime.mjs +3 -3
- package/scripts/sync-voice-runtime-manifest.mjs +46 -0
- package/scripts/verify-release-assets.mjs +106 -6
- package/scripts/verify-voice-runtime-release.mjs +89 -0
- package/scripts/voice-runtime-config.json +52 -0
- package/src/app.mjs +42 -5
- package/src/headless-exec.mjs +58 -32
- package/src/headless-exec.test.mjs +74 -13
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/10-tool-workflow.md +7 -4
- package/src/rules/shared/30-exploration.md +2 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
- package/src/runtime/agent/orchestrator/config.mjs +53 -18
- package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +17 -11
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +20 -7
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +132 -42
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
- package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +58 -3
- package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
- package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
- package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +45 -8
- package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
- package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +178 -29
- package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
- package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
- package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
- package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
- package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/attachments/store.mjs +11 -3
- package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
- package/src/runtime/channels/lib/event-queue.mjs +51 -41
- package/src/runtime/channels/lib/scheduler.mjs +23 -28
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
- package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
- package/src/runtime/channels/lib/webhook.mjs +39 -20
- package/src/runtime/channels/lib/whisper-server.mjs +46 -1
- package/src/runtime/channels/lib/worker-main.mjs +3 -1
- package/src/runtime/media/adapters/gemini-video.mjs +20 -4
- package/src/runtime/media/jobs.mjs +13 -0
- package/src/runtime/media/renditions.mjs +7 -1
- package/src/runtime/media/store.mjs +42 -22
- package/src/runtime/memory/index.mjs +4 -9
- package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
- package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
- package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
- package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
- package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
- package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
- package/src/runtime/memory/lib/ko-morph.mjs +46 -15
- package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
- package/src/runtime/memory/lib/memory.mjs +50 -12
- package/src/runtime/memory/lib/pg/process.mjs +1 -10
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
- package/src/runtime/memory/lib/query-handlers.mjs +2 -4
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
- package/src/runtime/shared/background-tasks.mjs +15 -0
- package/src/runtime/shared/background-tasks.test.mjs +78 -0
- package/src/runtime/shared/child-guardian.mjs +2 -10
- package/src/runtime/shared/pid-liveness.mjs +27 -0
- package/src/runtime/shared/runtime-root.mjs +1 -11
- package/src/runtime/shared/service-discovery.mjs +1 -17
- package/src/runtime/shared/singleton-owner.mjs +1 -11
- package/src/runtime/shared/staged-update.mjs +2 -12
- package/src/runtime/shared/tool-status.mjs +5 -1
- package/src/runtime/shared/tool-surface.mjs +15 -7
- package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
- package/src/runtime/shared/turn-snapshot.mjs +137 -13
- package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
- package/src/runtime/shared/webhook-session-run.mjs +16 -2
- package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
- package/src/runtime/web-search/lib/state.mjs +15 -15
- package/src/runtime/web-search/lib/web-tools.mjs +46 -5
- package/src/session-runtime/context-status.mjs +4 -12
- package/src/session-runtime/runtime-core.mjs +20 -1
- package/src/standalone/agent-dispatch-broker.mjs +33 -2
- package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
- package/src/standalone/agent-tool/notify.mjs +3 -1
- package/src/standalone/agent-tool/spawn-flow.mjs +48 -66
- package/src/standalone/agent-tool/tag-registry.mjs +116 -33
- package/src/standalone/agent-tool/tool-def.mjs +2 -2
- package/src/standalone/agent-tool/worker-index.mjs +8 -11
- package/src/standalone/agent-tool/worker-rows.mjs +4 -1
- package/src/standalone/agent-tool.mjs +20 -55
- package/src/standalone/agent-watchdog-registry.mjs +28 -4
- package/src/standalone/channel-client.mjs +47 -15
- package/src/standalone/channel-transport.mjs +44 -25
- package/src/standalone/daemon.mjs +31 -2
- package/src/standalone/hook-bus/config.mjs +25 -9
- package/src/standalone/hook-bus/handlers.mjs +16 -52
- package/src/standalone/hook-bus.mjs +29 -23
- package/src/standalone/memory-runtime-proxy.mjs +1 -16
- package/src/standalone/session-client.mjs +86 -23
- package/src/standalone/session-protocol.mjs +0 -2
- package/src/standalone/session-runtime-host-health.test.mjs +5 -4
- package/src/standalone/session-runtime-host.mjs +101 -6
- package/src/standalone/session-runtime-record.mjs +1 -5
- package/src/standalone/session-runtime-worker.mjs +52 -57
- package/src/standalone/session-service.mjs +4 -1
- package/src/standalone/session-transport.mjs +56 -25
- package/src/tui/App.jsx +40 -49
- package/src/tui/app/app-view.jsx +14 -21
- package/src/tui/app/core-memory-picker.mjs +29 -16
- package/src/tui/app/create-app-pickers.mjs +15 -36
- package/src/tui/app/extension-pickers.mjs +60 -49
- package/src/tui/app/maintenance-pickers.mjs +100 -57
- package/src/tui/app/message-selector.mjs +6 -5
- package/src/tui/app/model-picker.mjs +30 -13
- package/src/tui/app/onboarding-steps.mjs +21 -12
- package/src/tui/app/panel-epoch.mjs +56 -0
- package/src/tui/app/panel-epoch.test.mjs +1009 -0
- package/src/tui/app/panel-surface.mjs +98 -0
- package/src/tui/app/project-picker.mjs +30 -29
- package/src/tui/app/prompt-submit.mjs +192 -97
- package/src/tui/app/prompt-submit.test.mjs +206 -0
- package/src/tui/app/provider-setup-picker.mjs +74 -43
- package/src/tui/app/resume-picker.mjs +11 -8
- package/src/tui/app/route-pickers.mjs +45 -28
- package/src/tui/app/settings-picker.mjs +69 -18
- package/src/tui/app/shell-layout.mjs +1 -3
- package/src/tui/app/slash-commands.mjs +0 -2
- package/src/tui/app/slash-dispatch.mjs +4 -2
- package/src/tui/app/text-entry-policy.mjs +25 -0
- package/src/tui/app/theme-effort-pickers.mjs +11 -16
- package/src/tui/app/usage-context-panels.mjs +21 -25
- package/src/tui/app/use-global-key-input.mjs +3 -2
- package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
- package/src/tui/app/use-prompt-handlers.mjs +3 -2
- package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
- package/src/tui/components/Picker.jsx +1 -1
- package/src/tui/components/TextEntryPanel.jsx +7 -2
- package/src/tui/dist/index.mjs +652 -561
- package/src/tui/lib/voice-setup.mjs +1 -2
- package/src/tui/session/agent-job-feed.mjs +73 -11
- package/src/tui/session/session-api-ext.mjs +5 -6
- package/src/tui/session/session-api.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +15 -5
- package/src/tui/session/tui-steering-persist.mjs +33 -19
- package/src/tui/session/tui-steering-persist.test.mjs +104 -0
- package/src/tui/session-local.mjs +0 -22
- package/scripts/.tmp-coverage-report.mjs +0 -96
- package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
- package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.test.mjs +0 -57
|
@@ -37,11 +37,14 @@ function webhookRoute(modelRef) {
|
|
|
37
37
|
throw new Error('webhook run has no model: set one on the endpoint or configure maintenance.webhook');
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export async function runWebhookSession({ name, model = null, prompt, cwd = null, workflow = null, attachments = null, delivery = null }) {
|
|
40
|
+
export async function runWebhookSession({ name, model = null, prompt, cwd = null, workflow = null, attachments = null, delivery = null, signal = null }) {
|
|
41
41
|
const endpoint = String(name || '').trim();
|
|
42
42
|
const body = String(prompt || '').trim();
|
|
43
43
|
if (!endpoint) throw new Error('runWebhookSession: endpoint name required');
|
|
44
44
|
if (!body) throw new Error(`webhook "${endpoint}" has no prompt body`);
|
|
45
|
+
// The dispatcher's cancellation (delivery timeout) has to reach the run
|
|
46
|
+
// itself: an already-cancelled dispatch must not even create a session.
|
|
47
|
+
if (signal?.aborted) throw signal.reason ?? new Error('webhook dispatch aborted');
|
|
45
48
|
const route = webhookRoute(model);
|
|
46
49
|
const projectCwd = cwd ? String(cwd) : null;
|
|
47
50
|
const session = createSession({
|
|
@@ -65,6 +68,17 @@ export async function runWebhookSession({ name, model = null, prompt, cwd = null
|
|
|
65
68
|
// read as the user-authored intent rather than payload noise.
|
|
66
69
|
// Stored attachments ride along as composer-style content parts.
|
|
67
70
|
const content = automationPromptContent(body, attachments);
|
|
68
|
-
|
|
71
|
+
// askOpts.signal is the parent-abort link: it is cascaded onto the turn
|
|
72
|
+
// controller, so aborting it stops the in-flight provider/tool work instead
|
|
73
|
+
// of leaving a timed-out delivery's session running with live side effects.
|
|
74
|
+
const result = await askSession(
|
|
75
|
+
session.id,
|
|
76
|
+
content,
|
|
77
|
+
null,
|
|
78
|
+
null,
|
|
79
|
+
projectCwd || undefined,
|
|
80
|
+
undefined,
|
|
81
|
+
signal ? { signal } : {},
|
|
82
|
+
);
|
|
69
83
|
return { sessionId: session.id, result: String(result?.content || '') };
|
|
70
84
|
}
|
|
@@ -303,16 +303,35 @@ function headersToCdpPairs(headers) {
|
|
|
303
303
|
* Pinned fetch for a paused Chromium request: validate each hop, follow redirects,
|
|
304
304
|
* return bytes for Fetch.fulfillRequest. Chromium never performs its own DNS/connect.
|
|
305
305
|
*/
|
|
306
|
+
const ENTITY_HEADERS = new Set([
|
|
307
|
+
'content-type',
|
|
308
|
+
'content-length',
|
|
309
|
+
'content-encoding',
|
|
310
|
+
'content-language',
|
|
311
|
+
'content-location',
|
|
312
|
+
])
|
|
313
|
+
|
|
314
|
+
function withoutEntityHeaders(headers) {
|
|
315
|
+
const out = {}
|
|
316
|
+
for (const [name, value] of Object.entries(headers || {})) {
|
|
317
|
+
if (ENTITY_HEADERS.has(name.toLowerCase())) continue
|
|
318
|
+
out[name] = value
|
|
319
|
+
}
|
|
320
|
+
return out
|
|
321
|
+
}
|
|
322
|
+
|
|
306
323
|
export async function fetchPinnedForPausedRequest(url, { signal, method = 'GET', headers = {}, body } = {}) {
|
|
307
|
-
|
|
324
|
+
let currentMethod = (method || 'GET').toUpperCase()
|
|
325
|
+
let currentHeaders = { ...headers }
|
|
326
|
+
let currentBody = body
|
|
308
327
|
let currentUrl = url
|
|
309
328
|
for (let hops = 0; ; hops++) {
|
|
310
329
|
assertPublicUrl(currentUrl)
|
|
311
330
|
const response = await pinnedFetch(currentUrl, {
|
|
312
331
|
signal,
|
|
313
|
-
method:
|
|
314
|
-
headers,
|
|
315
|
-
body:
|
|
332
|
+
method: currentMethod,
|
|
333
|
+
headers: currentHeaders,
|
|
334
|
+
body: currentBody,
|
|
316
335
|
redirect: 'manual',
|
|
317
336
|
})
|
|
318
337
|
if (REDIRECT_STATUSES.has(response.status)) {
|
|
@@ -325,6 +344,17 @@ export async function fetchPinnedForPausedRequest(url, { signal, method = 'GET',
|
|
|
325
344
|
throw new Error(`Redirect ${response.status} without Location header`)
|
|
326
345
|
}
|
|
327
346
|
currentUrl = new URL(location, currentUrl).toString()
|
|
347
|
+
// Redirect method/body rewrite (fetch spec): 303 always, and 301/302 for
|
|
348
|
+
// anything other than GET/HEAD, become a bodyless GET. 307/308 preserve
|
|
349
|
+
// BOTH — so the body has to be replayed instead of dropped, which is what
|
|
350
|
+
// the previous `hops === 0` body gate did while keeping POST.
|
|
351
|
+
if (response.status === 303
|
|
352
|
+
|| ((response.status === 301 || response.status === 302)
|
|
353
|
+
&& currentMethod !== 'GET' && currentMethod !== 'HEAD')) {
|
|
354
|
+
currentMethod = 'GET'
|
|
355
|
+
currentBody = undefined
|
|
356
|
+
currentHeaders = withoutEntityHeaders(currentHeaders)
|
|
357
|
+
}
|
|
328
358
|
continue
|
|
329
359
|
}
|
|
330
360
|
const respBody = await readBodyBytesWithCap(response, MAX_BODY_BYTES)
|
|
@@ -14,10 +14,6 @@ function now() {
|
|
|
14
14
|
function defaultState() {
|
|
15
15
|
return {
|
|
16
16
|
providers: {},
|
|
17
|
-
routingCache: {
|
|
18
|
-
rawBySite: {},
|
|
19
|
-
scrapeByHost: {},
|
|
20
|
-
},
|
|
21
17
|
}
|
|
22
18
|
}
|
|
23
19
|
|
|
@@ -74,15 +70,15 @@ export function loadUsageState() {
|
|
|
74
70
|
if (_instance) return _instance
|
|
75
71
|
const raw = readJson(USAGE_PATH, null)
|
|
76
72
|
const def = defaultState()
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
73
|
+
let state = def
|
|
74
|
+
if (raw && typeof raw === 'object') {
|
|
75
|
+
// Drop the legacy `routingCache` (rawBySite/scrapeByHost) instead of
|
|
76
|
+
// carrying it forward: it has no consumers, and a plain `...raw` kept
|
|
77
|
+
// rewriting it back to disk on every flush.
|
|
78
|
+
const { routingCache: legacyRoutingCache, ...rest } = raw
|
|
79
|
+
state = { ...def, ...rest, providers: { ...(raw.providers || {}) } }
|
|
80
|
+
if (legacyRoutingCache !== undefined) scheduleUsageFlush(state)
|
|
81
|
+
}
|
|
86
82
|
_instance = state
|
|
87
83
|
activeUsageState = state
|
|
88
84
|
return state
|
|
@@ -157,13 +153,17 @@ const PROVIDER_DISABLE_TTL_MS = {
|
|
|
157
153
|
unknown: 0,
|
|
158
154
|
}
|
|
159
155
|
|
|
160
|
-
|
|
156
|
+
// `siteScoped` marks a failure that the FETCHED SITE produced (its 400/401/403/
|
|
157
|
+
// 429), not a fault of the provider/extractor itself. Those statuses say
|
|
158
|
+
// nothing about provider health, so they must never install the provider-wide
|
|
159
|
+
// cooldown that would disable the extractor for every other host for 24h.
|
|
160
|
+
export function noteProviderFailure(state, provider, errorMessage, errorKind, { siteScoped = false } = {}) {
|
|
161
161
|
const payload = {
|
|
162
162
|
error: errorMessage,
|
|
163
163
|
lastUsedAt: now(),
|
|
164
164
|
lastFailureAt: now(),
|
|
165
165
|
}
|
|
166
|
-
const ttl = PROVIDER_DISABLE_TTL_MS[errorKind] ?? 0
|
|
166
|
+
const ttl = siteScoped ? 0 : (PROVIDER_DISABLE_TTL_MS[errorKind] ?? 0)
|
|
167
167
|
if (ttl > 0) {
|
|
168
168
|
payload.cooldownUntil = new Date(Date.now() + ttl).toISOString()
|
|
169
169
|
}
|
|
@@ -292,9 +292,33 @@ function _notifyPoolWaiter() {
|
|
|
292
292
|
if (next) next()
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
async function _acquirePoolSlot() {
|
|
295
|
+
async function _acquirePoolSlot(signal) {
|
|
296
|
+
const abortError = () => signal?.reason || new Error('aborted')
|
|
297
|
+
// Waiting for a pool slot used to be uncancellable: an aborted tool call kept
|
|
298
|
+
// queueing until a slot freed, then launched a page nobody was waiting for.
|
|
296
299
|
while (_poolActive >= PUPPETEER_POOL_MAX_PAGES) {
|
|
297
|
-
|
|
300
|
+
if (signal?.aborted) throw abortError()
|
|
301
|
+
await new Promise((resolve, reject) => {
|
|
302
|
+
let onAbort = null
|
|
303
|
+
const waiter = () => {
|
|
304
|
+
if (onAbort) signal.removeEventListener('abort', onAbort)
|
|
305
|
+
resolve()
|
|
306
|
+
}
|
|
307
|
+
if (signal) {
|
|
308
|
+
onAbort = () => {
|
|
309
|
+
const index = _poolWaiters.indexOf(waiter)
|
|
310
|
+
if (index >= 0) _poolWaiters.splice(index, 1)
|
|
311
|
+
reject(abortError())
|
|
312
|
+
}
|
|
313
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
314
|
+
}
|
|
315
|
+
_poolWaiters.push(waiter)
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
if (signal?.aborted) {
|
|
319
|
+
// Woken but cancelled: pass the wake-up on so a live waiter isn't stranded.
|
|
320
|
+
_notifyPoolWaiter()
|
|
321
|
+
throw abortError()
|
|
298
322
|
}
|
|
299
323
|
_poolActive++
|
|
300
324
|
_poolLastActivity = Date.now()
|
|
@@ -421,7 +445,7 @@ async function closeBrowserBounded(browser, timeoutMs = 5000) {
|
|
|
421
445
|
}
|
|
422
446
|
|
|
423
447
|
async function withPuppeteerPage(signal, fn) {
|
|
424
|
-
await _acquirePoolSlot()
|
|
448
|
+
await _acquirePoolSlot(signal)
|
|
425
449
|
let browser
|
|
426
450
|
let context
|
|
427
451
|
let page
|
|
@@ -435,13 +459,26 @@ async function withPuppeteerPage(signal, fn) {
|
|
|
435
459
|
}
|
|
436
460
|
if (signal?.aborted) throw signal.reason || new Error('aborted')
|
|
437
461
|
if (signal) {
|
|
438
|
-
|
|
462
|
+
// Tear down THIS call's page/context only. The browser is pooled and
|
|
463
|
+
// shared with every concurrent scrape, so closing it here killed
|
|
464
|
+
// unrelated in-flight pages.
|
|
465
|
+
onExternalAbort = () => {
|
|
466
|
+
try { void page?.close()?.catch?.(() => {}) } catch {}
|
|
467
|
+
try { void context?.close()?.catch?.(() => {}) } catch {}
|
|
468
|
+
}
|
|
439
469
|
signal.addEventListener('abort', onExternalAbort, { once: true })
|
|
440
470
|
}
|
|
471
|
+
// Creation is multi-step and awaits between each step: without a checkpoint
|
|
472
|
+
// per step an abort landing mid-setup was lost and the page ran anyway.
|
|
473
|
+
// (The finally below closes whatever was created by then.)
|
|
441
474
|
context = await browser.createBrowserContext()
|
|
475
|
+
if (signal?.aborted) throw signal.reason || new Error('aborted')
|
|
442
476
|
page = await context.newPage()
|
|
477
|
+
if (signal?.aborted) throw signal.reason || new Error('aborted')
|
|
443
478
|
cdp = await page.createCDPSession()
|
|
479
|
+
if (signal?.aborted) throw signal.reason || new Error('aborted')
|
|
444
480
|
await installPuppeteerSsrfGate(page, cdp, signal)
|
|
481
|
+
if (signal?.aborted) throw signal.reason || new Error('aborted')
|
|
445
482
|
return await fn(page)
|
|
446
483
|
} finally {
|
|
447
484
|
if (onExternalAbort && signal) signal.removeEventListener('abort', onExternalAbort)
|
|
@@ -610,7 +647,11 @@ async function scrapeUrl(url, timeoutMs, usageState, signal) {
|
|
|
610
647
|
throw err
|
|
611
648
|
}
|
|
612
649
|
const errorKind = classifyProviderError(error)
|
|
613
|
-
|
|
650
|
+
// A status carried by the error came from the target page, so it is a
|
|
651
|
+
// property of that site — not of `readability`/`puppeteer`, which hold no
|
|
652
|
+
// credentials at all.
|
|
653
|
+
const siteScoped = Number.isFinite(Number(error?.status)) || /\bHTTP\s+\d{3}\b/.test(message)
|
|
654
|
+
noteProviderFailure(usageState, extractor, message, errorKind, { siteScoped })
|
|
614
655
|
}
|
|
615
656
|
}
|
|
616
657
|
|
|
@@ -12,7 +12,6 @@ import { SUMMARY_PREFIX } from '../runtime/agent/orchestrator/session/compact.mj
|
|
|
12
12
|
import { hasUserConversationMessage } from '../runtime/agent/orchestrator/session/manager/prompt-utils.mjs';
|
|
13
13
|
import {
|
|
14
14
|
resolveGaugeContextTokens,
|
|
15
|
-
resolveCompactionPressureTokens,
|
|
16
15
|
resolveWorkerCompactPolicy,
|
|
17
16
|
} from '../runtime/agent/orchestrator/session/loop/compact-policy.mjs';
|
|
18
17
|
import {
|
|
@@ -269,15 +268,8 @@ export function createContextStatus({
|
|
|
269
268
|
...resolveSessionCompactPolicy(session || {}, compactBoundaryTokens),
|
|
270
269
|
tokenCalibration: providerTokenCalibration(session?.provider || route.provider),
|
|
271
270
|
};
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
const compactionPressureTokens = resolveCompactionPressureTokens(
|
|
275
|
-
messageSummary.estimatedTokens,
|
|
276
|
-
compactPolicy,
|
|
277
|
-
{ messages, sessionRef: session },
|
|
278
|
-
);
|
|
279
|
-
// Same numerator the auto-compact trigger decides on, so the gauge reaches
|
|
280
|
-
// 100% exactly when compaction fires instead of still showing headroom.
|
|
271
|
+
// One canonical numerator drives the gauge, proactive compaction, and
|
|
272
|
+
// telemetry: provider usage plus only the growth after its aligned prefix.
|
|
281
273
|
const usedTokens = resolveGaugeContextTokens(
|
|
282
274
|
messageSummary.estimatedTokens,
|
|
283
275
|
compactPolicy,
|
|
@@ -313,8 +305,8 @@ export function createContextStatus({
|
|
|
313
305
|
triggerTokens: compactTriggerTokens || null,
|
|
314
306
|
bufferTokens: Number.isFinite(compactBufferTokens) ? compactBufferTokens : null,
|
|
315
307
|
bufferRatio: compactBufferRatio,
|
|
316
|
-
currentEstimatedTokens:
|
|
317
|
-
pressureTokens:
|
|
308
|
+
currentEstimatedTokens: usedTokens,
|
|
309
|
+
pressureTokens: usedTokens,
|
|
318
310
|
reserveTokens: Math.max(0, Number(compactPolicy.configuredReserveTokens) || 0),
|
|
319
311
|
lastApiRequestTokens: lastContextTokens || 0,
|
|
320
312
|
lastApiRequestStale: lastUsageStale,
|
|
@@ -645,7 +645,26 @@ export async function createMixdogSessionRuntime({
|
|
|
645
645
|
state: mcpState,
|
|
646
646
|
});
|
|
647
647
|
const hooksStartedAt = performance.now();
|
|
648
|
-
const hooks = createStandaloneHookBus({
|
|
648
|
+
const hooks = createStandaloneHookBus({
|
|
649
|
+
dataDir: cfgMod.getPluginData(),
|
|
650
|
+
// `mcp_tool` hooks run against the SAME connected MCP servers this session
|
|
651
|
+
// uses. Without this runner every configured mcp_tool hook reported
|
|
652
|
+
// "handler type mcp_tool not configured", so the handler's timeout +
|
|
653
|
+
// cancellation path never ran in production. The hook's abort signal is
|
|
654
|
+
// forwarded so a timed-out hook cancels its tool call instead of leaving it
|
|
655
|
+
// holding an admission slot.
|
|
656
|
+
mcpToolRunner: async ({ name, args, signal }) => {
|
|
657
|
+
if (typeof mcpClient?.executeMcpTool !== 'function') {
|
|
658
|
+
throw new Error('MCP runtime is unavailable');
|
|
659
|
+
}
|
|
660
|
+
const result = await mcpClient.executeMcpTool(name, args ?? {}, {
|
|
661
|
+
scopeId: rt.mcpScopeId,
|
|
662
|
+
signal: signal || null,
|
|
663
|
+
ownerKey: `hook:${name}`,
|
|
664
|
+
});
|
|
665
|
+
return typeof result === 'string' ? result : String(result?.result ?? '');
|
|
666
|
+
},
|
|
667
|
+
});
|
|
649
668
|
hooks.emit('runtime:start', { cwd: rt.currentCwd, provider: rt.route.provider, model: rt.route.model, toolMode: rt.mode });
|
|
650
669
|
bootProfile('hooks:ready', { ms: (performance.now() - hooksStartedAt).toFixed(1) });
|
|
651
670
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createFairCallScheduler } from './fair-call-scheduler.mjs';
|
|
2
|
+
import { hashStructuredValue } from '../runtime/shared/json-metrics.mjs';
|
|
2
3
|
|
|
3
4
|
const MEMORY_AGENTS = Object.freeze([
|
|
4
5
|
'cycle1-agent',
|
|
@@ -13,6 +14,20 @@ function abortError(signal) {
|
|
|
13
14
|
: new Error(String(signal?.reason || 'agent dispatch canceled'));
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
/** Identity of a dispatch PAYLOAD. A retry repeats it exactly; a caller that
|
|
18
|
+
* reuses a callId for different work is issuing a different call. */
|
|
19
|
+
function dispatchSignature(agent, params) {
|
|
20
|
+
try {
|
|
21
|
+
return hashStructuredValue({
|
|
22
|
+
agent,
|
|
23
|
+
prompt: String(params?.prompt ?? ''),
|
|
24
|
+
preset: params?.preset ?? null,
|
|
25
|
+
cwd: typeof params?.cwd === 'string' ? params.cwd : null,
|
|
26
|
+
timeout: Number.isFinite(Number(params?.timeout)) ? Number(params.timeout) : null,
|
|
27
|
+
});
|
|
28
|
+
} catch { return null; }
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
function awaitSharedPreparation(promise, signal) {
|
|
17
32
|
if (!signal) return promise;
|
|
18
33
|
if (signal.aborted) return Promise.reject(abortError(signal));
|
|
@@ -113,8 +128,24 @@ export function createAgentDispatchBroker({
|
|
|
113
128
|
if (!prompt) return Promise.reject(new Error(`agent dispatch prompt required for "${agent}"`));
|
|
114
129
|
|
|
115
130
|
const id = String(callId || `broker-${process.pid}-${++nextCallId}`);
|
|
131
|
+
const signature = dispatchSignature(agent, params);
|
|
116
132
|
const existing = inFlight.get(id);
|
|
117
|
-
if (existing)
|
|
133
|
+
if (existing) {
|
|
134
|
+
// Scope note: a broker callId dedupes CONCURRENT duplicates only. The
|
|
135
|
+
// memory client mints a fresh id per dispatch and cancels instead of
|
|
136
|
+
// replaying (runtime/memory/lib/agent-ipc.mjs callAgentDispatch), so a
|
|
137
|
+
// settled id carries no promise and needs no retained result.
|
|
138
|
+
// Within that window a callId is still an idempotency key, not an
|
|
139
|
+
// overwrite slot: answering a different payload from an unrelated
|
|
140
|
+
// in-flight run is silent data loss.
|
|
141
|
+
if (signature && existing.signature && existing.signature !== signature) {
|
|
142
|
+
return Promise.reject(Object.assign(
|
|
143
|
+
new Error(`agent dispatch callId '${id}' was reused with a different payload`),
|
|
144
|
+
{ code: 'ECALLIDCONFLICT' },
|
|
145
|
+
));
|
|
146
|
+
}
|
|
147
|
+
return existing.promise;
|
|
148
|
+
}
|
|
118
149
|
|
|
119
150
|
const controller = new AbortController();
|
|
120
151
|
const abortFromCaller = () => {
|
|
@@ -146,7 +177,7 @@ export function createAgentDispatchBroker({
|
|
|
146
177
|
if (inFlight.get(id) === record) inFlight.delete(id);
|
|
147
178
|
try { onActivityChanged?.(snapshot()); } catch {}
|
|
148
179
|
});
|
|
149
|
-
record = { controller, promise: tracked };
|
|
180
|
+
record = { controller, promise: tracked, signature };
|
|
150
181
|
inFlight.set(id, record);
|
|
151
182
|
try { onActivityChanged?.(snapshot()); } catch {}
|
|
152
183
|
return tracked;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Background-task reconcile for a worker turn (spawn AND send paths).
|
|
2
|
+
//
|
|
3
|
+
// A worker turn has to mark its background task terminal from three separate
|
|
4
|
+
// windows, and both paths must do it identically — they used to carry two
|
|
5
|
+
// byte-identical copies of every block:
|
|
6
|
+
//
|
|
7
|
+
// 1. TERMINAL RESULT — the worker produced its final result. Reconciling
|
|
8
|
+
// here (rather than after the session save) keeps a hung/slow post-result
|
|
9
|
+
// step from stranding the task, and the status card, in `running`. An
|
|
10
|
+
// empty/abnormal finish is a FAILURE, not a completion, so the Lead card
|
|
11
|
+
// renders `error: …` instead of a header-only empty card.
|
|
12
|
+
// 2. STREAM STALLED — a mid-stream stall (StreamStalledError / ESTREAMSTALL)
|
|
13
|
+
// throws WITHOUT a terminal result, so the finally net below (gated on
|
|
14
|
+
// _terminalResultValue) is skipped and only the outer task-reject path
|
|
15
|
+
// would notify. Reconciling to `failed` here guarantees the owner (Lead)
|
|
16
|
+
// gets a failure notification instead of a task stuck in `running`.
|
|
17
|
+
// 3. FINALLY — safety net for a post-result step (session save) that hung or
|
|
18
|
+
// threw after a terminal result already existed.
|
|
19
|
+
//
|
|
20
|
+
// Every call is idempotent (the first terminal state wins, so the outer reject
|
|
21
|
+
// path cannot double-notify) and absorbs its own errors, so reporting on a
|
|
22
|
+
// turn can never break the turn.
|
|
23
|
+
import { reconcileBackgroundTask } from '../../runtime/shared/background-tasks.mjs';
|
|
24
|
+
|
|
25
|
+
export function reconcileJobTerminalResult(job, value) {
|
|
26
|
+
if (!job?.taskId) return;
|
|
27
|
+
try {
|
|
28
|
+
reconcileBackgroundTask(job.taskId, value.error
|
|
29
|
+
? {
|
|
30
|
+
status: 'failed',
|
|
31
|
+
result: value,
|
|
32
|
+
error: value.error,
|
|
33
|
+
terminalReason: 'agent-empty-final',
|
|
34
|
+
}
|
|
35
|
+
: {
|
|
36
|
+
status: 'completed',
|
|
37
|
+
result: value,
|
|
38
|
+
terminalReason: 'agent-terminal-result',
|
|
39
|
+
});
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Watchdog stall that still recovered a partial handoff: a completion. */
|
|
44
|
+
export function reconcileJobWatchdogPartial(job, value) {
|
|
45
|
+
if (!job?.taskId) return;
|
|
46
|
+
try {
|
|
47
|
+
reconcileBackgroundTask(job.taskId, {
|
|
48
|
+
status: 'completed',
|
|
49
|
+
result: value,
|
|
50
|
+
terminalReason: 'agent-watchdog-partial',
|
|
51
|
+
});
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function reconcileJobStreamStalled(job, error) {
|
|
56
|
+
if (!job?.taskId || job._terminalResultValue !== undefined) return;
|
|
57
|
+
try {
|
|
58
|
+
reconcileBackgroundTask(job.taskId, {
|
|
59
|
+
status: 'failed',
|
|
60
|
+
error,
|
|
61
|
+
terminalReason: 'agent-stream-stalled',
|
|
62
|
+
});
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function reconcileJobFinally(job, finalStatus) {
|
|
67
|
+
if (!job || job._terminalResultValue === undefined) return;
|
|
68
|
+
try {
|
|
69
|
+
reconcileBackgroundTask(job.taskId, {
|
|
70
|
+
status: finalStatus === 'error' ? 'failed' : 'completed',
|
|
71
|
+
result: job._terminalResultValue,
|
|
72
|
+
...(finalStatus === 'error' && job._terminalResultValue?.error
|
|
73
|
+
? { error: job._terminalResultValue.error }
|
|
74
|
+
: {}),
|
|
75
|
+
terminalReason: 'agent-finally-reconcile',
|
|
76
|
+
});
|
|
77
|
+
} catch {}
|
|
78
|
+
}
|
|
@@ -28,7 +28,9 @@ export function createNotify(mgr, { notifySessionCompletion } = {}) {
|
|
|
28
28
|
}
|
|
29
29
|
// Mark this as a deferred completion/task notification so a later session
|
|
30
30
|
// resume drops it rather than replaying it out-of-order (owner decision).
|
|
31
|
-
return Boolean(mgr.enqueuePendingMessage(target, markCompletionEntry(visible
|
|
31
|
+
return Boolean(mgr.enqueuePendingMessage(target, markCompletionEntry(visible, {
|
|
32
|
+
executionId: meta?.execution_id,
|
|
33
|
+
})) > 0);
|
|
32
34
|
} catch {
|
|
33
35
|
return false;
|
|
34
36
|
}
|
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
// execution with turn-review collection and terminal accounting.
|
|
5
5
|
import { agentDefinitionExists, clean, clearAgentStatuslineRoute, nonNegativeInt, normalizeAgentName, positiveInt, presetKey, readAgentFrontmatterPermission, resolvePrompt, terminalPidForContext, writeAgentStatuslineRoute } from './helpers.mjs';
|
|
6
6
|
import { createNotify } from './notify.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import { sanitizeTaskMeta, startBackgroundTask } from '../../runtime/shared/background-tasks.mjs';
|
|
8
|
+
import {
|
|
9
|
+
reconcileJobFinally,
|
|
10
|
+
reconcileJobStreamStalled,
|
|
11
|
+
reconcileJobTerminalResult,
|
|
12
|
+
reconcileJobWatchdogPartial,
|
|
13
|
+
} from './job-task-reconcile.mjs';
|
|
8
14
|
import { isAgentDisabled } from '../../runtime/shared/agent-route-config.mjs';
|
|
9
15
|
import { abnormalEmptyFinishError, renderResult } from './render.mjs';
|
|
10
16
|
import { resourceAdmission } from '../../runtime/shared/resource-admission.mjs';
|
|
@@ -14,8 +20,14 @@ import { normalizeAgentPermission } from '../../runtime/shared/markdown-frontmat
|
|
|
14
20
|
import { resolveAgentSpawnPreset } from './spawn-preset.mjs';
|
|
15
21
|
import { resolveAgentWatchdogPolicy, resolveHandoffMessageStartIndex, watchdogPartialHandoffFromError } from '../../runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs';
|
|
16
22
|
import { prepareAgentSession } from '../../runtime/agent/orchestrator/agent-runtime/session-builder.mjs';
|
|
23
|
+
import { saveSessionAsync } from '../../runtime/agent/orchestrator/session/store.mjs';
|
|
17
24
|
import { AGENT_OWNER } from '../../runtime/agent/orchestrator/agent-owner.mjs';
|
|
18
25
|
import { getProvider } from '../../runtime/agent/orchestrator/providers/registry.mjs';
|
|
26
|
+
// Mid-turn worker persistence cadence. The watchdog sweep runs every second;
|
|
27
|
+
// a transcript save is far more expensive than a bookkeeping stamp, so the
|
|
28
|
+
// in-progress snapshot lands on this slower clock.
|
|
29
|
+
const AGENT_PROGRESS_SAVE_MS = 2_000;
|
|
30
|
+
|
|
19
31
|
export function createSpawnFlow({
|
|
20
32
|
mgr,
|
|
21
33
|
forgetTerminalSession,
|
|
@@ -189,6 +201,35 @@ export function createSpawnFlow({
|
|
|
189
201
|
});
|
|
190
202
|
};
|
|
191
203
|
|
|
204
|
+
// In-turn progress stamp. turnStartStamper only fires at a TURN boundary, so
|
|
205
|
+
// one long first turn (a review agent easily runs 20+ tool iterations) left
|
|
206
|
+
// the worker row untouched for minutes and the panel looked stalled. The
|
|
207
|
+
// watchdog sweep reports each observed change; the row keeps its own live
|
|
208
|
+
// message count and updatedAt from it.
|
|
209
|
+
const progressStamper = (session, tag) => {
|
|
210
|
+
let lastSavedAt = 0;
|
|
211
|
+
return (progress = {}) => {
|
|
212
|
+
const messages = Number(progress?.messages) || 0;
|
|
213
|
+
upsertWorkerSessionDeferred(session, tag, {
|
|
214
|
+
status: 'running',
|
|
215
|
+
stage: 'running',
|
|
216
|
+
...(messages > 0 ? { messages } : {}),
|
|
217
|
+
});
|
|
218
|
+
// Mid-turn durability. A worker transcript otherwise reaches disk only
|
|
219
|
+
// when the turn ENDS, so a pane opened on a running worker shows the
|
|
220
|
+
// prompt and nothing after it — the work is real but invisible to every
|
|
221
|
+
// viewer. Persisting on the sweep makes the in-progress transcript
|
|
222
|
+
// readable like any other session. Throttled: a transcript save is not
|
|
223
|
+
// free, and the sweep runs every second.
|
|
224
|
+
const now = Date.now();
|
|
225
|
+
if (now - lastSavedAt < AGENT_PROGRESS_SAVE_MS) return;
|
|
226
|
+
lastSavedAt = now;
|
|
227
|
+
try {
|
|
228
|
+
Promise.resolve(saveSessionAsync(session)).catch(() => {});
|
|
229
|
+
} catch { /* progress persistence is best-effort */ }
|
|
230
|
+
};
|
|
231
|
+
};
|
|
232
|
+
|
|
192
233
|
async function prepareSpawn(args, callerCwd = null, context = {}, prepState = null) {
|
|
193
234
|
refreshTagsFromSessions({ context });
|
|
194
235
|
return prepareSpawnInProcess(args, callerCwd, context, prepState);
|
|
@@ -325,6 +366,7 @@ export function createSpawnFlow({
|
|
|
325
366
|
const { args, tag, session, agent, preset, presetName, workerCwd, prompt, watchdogPolicy } = prepared;
|
|
326
367
|
const watchdog = startProgressIdleWatchdog(session.id, watchdogPolicy, agent, {
|
|
327
368
|
onTurnStart: turnStartStamper(session, tag),
|
|
369
|
+
onProgress: progressStamper(session, tag),
|
|
328
370
|
});
|
|
329
371
|
const turnReview = createTurnReviewCollector(session, tag, agent, notifyContext || {});
|
|
330
372
|
let finalStatus = 'idle';
|
|
@@ -374,29 +416,7 @@ export function createSpawnFlow({
|
|
|
374
416
|
const value = completionValue(terminalResult);
|
|
375
417
|
if (job) job._terminalResultValue = value;
|
|
376
418
|
notifyOwnerAgentCompletionEarly(job, value, notifyContext || {});
|
|
377
|
-
|
|
378
|
-
// result, so a hung/slow post-result session save cannot strand the
|
|
379
|
-
// task (and the status card) in `running`. Idempotent; the finally
|
|
380
|
-
// reconcile remains a backup for the error/no-terminal-result path.
|
|
381
|
-
if (job?.taskId) {
|
|
382
|
-
try {
|
|
383
|
-
// An empty/abnormal finish is a failure, not a completion:
|
|
384
|
-
// reconcile as `failed` with the accurate error so the Lead
|
|
385
|
-
// card renders `error: …` instead of a header-only empty card.
|
|
386
|
-
reconcileBackgroundTask(job.taskId, value.error
|
|
387
|
-
? {
|
|
388
|
-
status: 'failed',
|
|
389
|
-
result: value,
|
|
390
|
-
error: value.error,
|
|
391
|
-
terminalReason: 'agent-empty-final',
|
|
392
|
-
}
|
|
393
|
-
: {
|
|
394
|
-
status: 'completed',
|
|
395
|
-
result: value,
|
|
396
|
-
terminalReason: 'agent-terminal-result',
|
|
397
|
-
});
|
|
398
|
-
} catch {}
|
|
399
|
-
}
|
|
419
|
+
reconcileJobTerminalResult(job, value);
|
|
400
420
|
},
|
|
401
421
|
} : {}),
|
|
402
422
|
});
|
|
@@ -431,34 +451,11 @@ export function createSpawnFlow({
|
|
|
431
451
|
stallAbort: true,
|
|
432
452
|
};
|
|
433
453
|
if (job) job._terminalResultValue = value;
|
|
434
|
-
|
|
435
|
-
try {
|
|
436
|
-
reconcileBackgroundTask(job.taskId, {
|
|
437
|
-
status: 'completed',
|
|
438
|
-
result: value,
|
|
439
|
-
terminalReason: 'agent-watchdog-partial',
|
|
440
|
-
});
|
|
441
|
-
} catch {}
|
|
442
|
-
}
|
|
454
|
+
reconcileJobWatchdogPartial(job, value);
|
|
443
455
|
return value;
|
|
444
456
|
}
|
|
445
457
|
finalStatus = 'error';
|
|
446
|
-
|
|
447
|
-
// here WITHOUT a terminal result, so the finally reconcile below (gated on
|
|
448
|
-
// _terminalResultValue) would be skipped and only the outer task-reject
|
|
449
|
-
// path would notify. Belt-and-suspenders: reconcile this job to `failed`
|
|
450
|
-
// now so the owner (Lead) always gets a failure notification instead of a
|
|
451
|
-
// task stranded in `running`. Idempotent — completeBackgroundTask no-ops
|
|
452
|
-
// once terminal, so the outer reject path can't double-notify.
|
|
453
|
-
if (job?.taskId && job._terminalResultValue === undefined) {
|
|
454
|
-
try {
|
|
455
|
-
reconcileBackgroundTask(job.taskId, {
|
|
456
|
-
status: 'failed',
|
|
457
|
-
error,
|
|
458
|
-
terminalReason: 'agent-stream-stalled',
|
|
459
|
-
});
|
|
460
|
-
} catch {}
|
|
461
|
-
}
|
|
458
|
+
reconcileJobStreamStalled(job, error);
|
|
462
459
|
throw error;
|
|
463
460
|
} finally {
|
|
464
461
|
turnReview.complete();
|
|
@@ -475,23 +472,7 @@ export function createSpawnFlow({
|
|
|
475
472
|
finishedAt: new Date().toISOString(),
|
|
476
473
|
});
|
|
477
474
|
setImmediate(notifyStatusChange);
|
|
478
|
-
|
|
479
|
-
// worker already produced a terminal result, the task could otherwise be
|
|
480
|
-
// stranded in `running`. Reconcile it to a terminal state using the
|
|
481
|
-
// captured result so the owner gets a completion notification + the
|
|
482
|
-
// statusline clears. Idempotent once the task is already terminal.
|
|
483
|
-
if (job && job._terminalResultValue !== undefined) {
|
|
484
|
-
try {
|
|
485
|
-
reconcileBackgroundTask(job.taskId, {
|
|
486
|
-
status: finalStatus === 'error' ? 'failed' : 'completed',
|
|
487
|
-
result: job._terminalResultValue,
|
|
488
|
-
...(finalStatus === 'error' && job._terminalResultValue?.error
|
|
489
|
-
? { error: job._terminalResultValue.error }
|
|
490
|
-
: {}),
|
|
491
|
-
terminalReason: 'agent-finally-reconcile',
|
|
492
|
-
});
|
|
493
|
-
} catch {}
|
|
494
|
-
}
|
|
475
|
+
reconcileJobFinally(job, finalStatus);
|
|
495
476
|
scheduleReap(session.id);
|
|
496
477
|
// SubagentStop: worker finished (terminal), regardless of outcome.
|
|
497
478
|
emitSubagentEvent('stop', agent, { session_id: session.id, tag, status: finalStatus });
|
|
@@ -510,6 +491,7 @@ export function createSpawnFlow({
|
|
|
510
491
|
progressWatchdogs,
|
|
511
492
|
startProgressIdleWatchdog,
|
|
512
493
|
turnStartStamper,
|
|
494
|
+
progressStamper,
|
|
513
495
|
prepareSpawn,
|
|
514
496
|
prepareSpawnInProcess,
|
|
515
497
|
runSpawn,
|