mixdog 0.9.147 → 0.9.149
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 +5 -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-codex-identity.test.mjs +32 -9
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +78 -33
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +21 -0
- 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 +20 -11
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +112 -36
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +112 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +130 -60
- 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 +69 -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/ask-session.mjs +43 -5
- 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/session-lifecycle.mjs +2 -0
- 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 +46 -9
- 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-integrity.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +180 -31
- 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/pristine-execution-contract.json +0 -1
- 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/session-runtime/session-lifecycle.mjs +9 -0
- 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 +52 -70
- 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
|
@@ -204,34 +204,48 @@ export function createTagRegistry({
|
|
|
204
204
|
if (id) removeWorkerRow({ sessionId: id });
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
function tombstoneTerminalSession(tag, sessionId, session = null) {
|
|
207
|
+
function tombstoneTerminalSession(tag, sessionId, session = null, expectedReapAt = '') {
|
|
208
208
|
const value = clean(tag);
|
|
209
209
|
const id = clean(sessionId);
|
|
210
210
|
if (!value || !id) {
|
|
211
|
-
forgetTerminalSession(value, id);
|
|
212
|
-
return;
|
|
211
|
+
if (!expectedReapAt) forgetTerminalSession(value, id);
|
|
212
|
+
return false;
|
|
213
213
|
}
|
|
214
|
-
|
|
215
|
-
tags.delete(value);
|
|
216
|
-
tagAgents.delete(value);
|
|
217
|
-
tagCwds.delete(value);
|
|
218
|
-
}
|
|
219
|
-
const tombstone = {
|
|
220
|
-
tag: value,
|
|
221
|
-
agent: clean(session?.agent) || null,
|
|
222
|
-
cwd: clean(session?.cwd) || null,
|
|
223
|
-
clientHostPid: positiveInt(session?.clientHostPid),
|
|
224
|
-
reapedAt: new Date().toISOString(),
|
|
225
|
-
};
|
|
214
|
+
let applied = false;
|
|
226
215
|
flushWorkerIndexMutations();
|
|
227
216
|
writeWorkerRows((byKey, tombstonesByKey, priorityTombstoneKeys) => {
|
|
228
|
-
|
|
229
|
-
|
|
217
|
+
const matches = [...byKey.entries()]
|
|
218
|
+
.filter(([, row]) => clean(row.sessionId) === id);
|
|
219
|
+
const expected = expectedReapAt
|
|
220
|
+
? matches.find(([, row]) =>
|
|
221
|
+
clean(row.reapAt) === expectedReapAt
|
|
222
|
+
&& isTerminalWorkerStatus(row.status || row.stage))
|
|
223
|
+
: null;
|
|
224
|
+
// A stale timer from an earlier turn must never reap newer work.
|
|
225
|
+
if (expectedReapAt && !expected) return;
|
|
226
|
+
const source = expected?.[1] || matches[0]?.[1] || {};
|
|
227
|
+
const tombstone = {
|
|
228
|
+
tag: value,
|
|
229
|
+
agent: clean(session?.agent || source.agent) || null,
|
|
230
|
+
cwd: clean(session?.cwd || source.cwd) || null,
|
|
231
|
+
clientHostPid: positiveInt(session?.clientHostPid || source.clientHostPid),
|
|
232
|
+
reapedAt: new Date().toISOString(),
|
|
233
|
+
};
|
|
234
|
+
for (const [key] of matches) {
|
|
235
|
+
byKey.delete(key);
|
|
230
236
|
}
|
|
231
237
|
const tombstoneKey = tagTombstoneKey(tombstone);
|
|
232
238
|
tombstonesByKey.set(tombstoneKey, tombstone);
|
|
233
239
|
priorityTombstoneKeys.add(tombstoneKey);
|
|
240
|
+
applied = true;
|
|
234
241
|
});
|
|
242
|
+
if (!applied) return false;
|
|
243
|
+
if (tags.get(value) === id) {
|
|
244
|
+
tags.delete(value);
|
|
245
|
+
tagAgents.delete(value);
|
|
246
|
+
tagCwds.delete(value);
|
|
247
|
+
}
|
|
248
|
+
return true;
|
|
235
249
|
}
|
|
236
250
|
|
|
237
251
|
function tagTombstoneForTag(tag, context = {}) {
|
|
@@ -256,27 +270,93 @@ export function createTagRegistry({
|
|
|
256
270
|
return true;
|
|
257
271
|
}
|
|
258
272
|
|
|
259
|
-
function
|
|
260
|
-
|
|
273
|
+
function reapTerminalRow(row) {
|
|
274
|
+
const sessionId = clean(row?.sessionId);
|
|
275
|
+
const reapAt = clean(row?.reapAt);
|
|
276
|
+
const tag = clean(row?.tag) || tagForSession(sessionId);
|
|
277
|
+
if (!sessionId || !reapAt || !tag) return false;
|
|
278
|
+
const session = getLiveSession(sessionId);
|
|
279
|
+
if (!tombstoneTerminalSession(tag, sessionId, session, reapAt)) return false;
|
|
280
|
+
try { mgr.hideSessionFromList?.(sessionId); } catch {}
|
|
281
|
+
clearAgentStatuslineRoute(sessionId);
|
|
282
|
+
// Reaping expires only the reusable tag/runtime lease. The transcript is
|
|
283
|
+
// user-visible from the parent task's agent tab, so terminal cleanup must
|
|
284
|
+
// never tombstone it ahead of that parent.
|
|
285
|
+
try { mgr.unloadSessionRuntime?.(sessionId, 'terminal-reap'); } catch {}
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function schedulePersistedReap(row) {
|
|
290
|
+
const sessionId = clean(row?.sessionId);
|
|
291
|
+
const reapAt = clean(row?.reapAt);
|
|
292
|
+
const deadline = Date.parse(reapAt) || 0;
|
|
293
|
+
if (!sessionId || !deadline) return false;
|
|
261
294
|
cancelReap(sessionId);
|
|
262
|
-
const reapProvider = provider || getLiveSession(sessionId)?.provider || null;
|
|
263
|
-
const reapMs = resolveAgentTerminalReapMs(cfgMod.loadConfig(), reapProvider);
|
|
264
|
-
if (reapMs == null) return;
|
|
265
295
|
const handle = setTimeout(() => {
|
|
266
296
|
reapTimers.delete(sessionId);
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const tag = tagForSession(sessionId);
|
|
270
|
-
tombstoneTerminalSession(tag, sessionId, session);
|
|
271
|
-
clearAgentStatuslineRoute(sessionId);
|
|
272
|
-
// Reaping expires only the reusable tag/runtime lease. The transcript is
|
|
273
|
-
// user-visible from the parent task's agent tab, so terminal cleanup must
|
|
274
|
-
// never tombstone it ahead of that parent. The normal completion path
|
|
275
|
-
// unloads immediately; this delayed call is an idempotent race fallback.
|
|
276
|
-
try { mgr.unloadSessionRuntime?.(sessionId, 'terminal-reap'); } catch {}
|
|
277
|
-
}, reapMs);
|
|
297
|
+
reapTerminalRow(row);
|
|
298
|
+
}, Math.max(0, deadline - Date.now()));
|
|
278
299
|
handle.unref?.();
|
|
279
300
|
reapTimers.set(sessionId, handle);
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function scheduleReap(sessionId, provider = null) {
|
|
305
|
+
const id = clean(sessionId);
|
|
306
|
+
if (!id) return false;
|
|
307
|
+
cancelReap(id);
|
|
308
|
+
// The terminal upsert is deferred to batch parallel fanout writes. Flush it
|
|
309
|
+
// before stamping the deadline so reapAt and the terminal state land in one
|
|
310
|
+
// durable lifecycle order.
|
|
311
|
+
flushWorkerIndexMutations();
|
|
312
|
+
const row = readWorkerRows().find((entry) => clean(entry.sessionId) === id);
|
|
313
|
+
if (!row || !isTerminalWorkerStatus(row.status || row.stage)) return false;
|
|
314
|
+
const reapProvider = clean(provider) || clean(row.provider)
|
|
315
|
+
|| clean(getLiveSession(id)?.provider) || null;
|
|
316
|
+
const reapMs = resolveAgentTerminalReapMs(cfgMod.loadConfig(), reapProvider);
|
|
317
|
+
const reapAt = reapMs == null ? null : new Date(Date.now() + reapMs).toISOString();
|
|
318
|
+
let persisted = null;
|
|
319
|
+
writeWorkerRows((byKey) => {
|
|
320
|
+
for (const [key, current] of byKey.entries()) {
|
|
321
|
+
if (clean(current.sessionId) !== id) continue;
|
|
322
|
+
if (!isTerminalWorkerStatus(current.status || current.stage)) return;
|
|
323
|
+
persisted = { ...current, reapAt };
|
|
324
|
+
byKey.set(key, persisted);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
return persisted?.reapAt ? schedulePersistedReap(persisted) : false;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Rebuild child terminal timers after a process restart. Legacy rows have
|
|
332
|
+
* no reapAt, so derive the original absolute deadline from their frozen
|
|
333
|
+
* terminal stamp instead of granting a fresh full lease on every boot. */
|
|
334
|
+
function recoverTerminalReaps() {
|
|
335
|
+
flushWorkerIndexMutations();
|
|
336
|
+
const now = Date.now();
|
|
337
|
+
const recovered = [];
|
|
338
|
+
writeWorkerRows((byKey) => {
|
|
339
|
+
for (const [key, row] of byKey.entries()) {
|
|
340
|
+
if (!isTerminalWorkerStatus(row.status || row.stage)) continue;
|
|
341
|
+
let reapAt = clean(row.reapAt);
|
|
342
|
+
if (!(Date.parse(reapAt) > 0)) {
|
|
343
|
+
const reapMs = resolveAgentTerminalReapMs(cfgMod.loadConfig(), row.provider);
|
|
344
|
+
if (reapMs == null) continue;
|
|
345
|
+
const terminalAt = workerRowTime(row);
|
|
346
|
+
// A timestamp-free legacy row cannot prove that its grace already
|
|
347
|
+
// elapsed. Give it one normal window from recovery; timestamped rows
|
|
348
|
+
// keep their original completion-based deadline.
|
|
349
|
+
reapAt = new Date((terminalAt > 0 ? terminalAt : now) + reapMs).toISOString();
|
|
350
|
+
}
|
|
351
|
+
const next = { ...row, reapAt };
|
|
352
|
+
byKey.set(key, next);
|
|
353
|
+
recovered.push(next);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
for (const row of recovered) {
|
|
357
|
+
if ((Date.parse(row.reapAt) || 0) <= now) reapTerminalRow(row);
|
|
358
|
+
else schedulePersistedReap(row);
|
|
359
|
+
}
|
|
280
360
|
}
|
|
281
361
|
|
|
282
362
|
function transitionStaleNonterminalRows(context = {}) {
|
|
@@ -321,6 +401,8 @@ export function createTagRegistry({
|
|
|
321
401
|
return true;
|
|
322
402
|
}
|
|
323
403
|
|
|
404
|
+
recoverTerminalReaps();
|
|
405
|
+
|
|
324
406
|
return {
|
|
325
407
|
readAllTagTombstones,
|
|
326
408
|
readTagTombstones,
|
|
@@ -351,6 +433,7 @@ export function createTagRegistry({
|
|
|
351
433
|
consumeTagTombstone,
|
|
352
434
|
cancelReap,
|
|
353
435
|
scheduleReap,
|
|
436
|
+
recoverTerminalReaps,
|
|
354
437
|
transitionStaleNonterminalRows,
|
|
355
438
|
};
|
|
356
439
|
}
|
|
@@ -22,14 +22,14 @@ export const AGENT_TOOL = {
|
|
|
22
22
|
openWorldHint: true,
|
|
23
23
|
agentHidden: true,
|
|
24
24
|
},
|
|
25
|
-
description: 'Run scoped agent work as background tasks. spawn/send return task_id immediately
|
|
25
|
+
description: 'Run scoped agent work as background tasks. spawn/send return task_id immediately. Reuse one tag for one scope for its whole lifetime; give distinct tags only to independent scopes. Never mint a new tag because a session expired or was lost — same-tag spawn respawns it with the full brief. Wait for completion notifications; use status/read only for manual recovery.',
|
|
26
26
|
inputSchema: {
|
|
27
27
|
type: 'object',
|
|
28
28
|
properties: {
|
|
29
29
|
type: { type: 'string', enum: ['spawn', 'send', 'list', 'close', 'cancel', 'status', 'read', 'cleanup'], description: 'Required action. New spawn requires agent and one of prompt/message/file; send requires tag and one of prompt/message/file; read/status/close/cancel require task_id or tag; list/cleanup require no target.' },
|
|
30
30
|
task_id: { type: 'string', description: 'Task ID returned by spawn/send; target for read/status/close/cancel.' },
|
|
31
31
|
agent: { type: 'string', description: 'Workflow agent id. Required for a new spawn; same-tag reuse can inherit it.' },
|
|
32
|
-
tag: { type: 'string', description: 'Stable scope handle. Reuse for the same scope; send requires it, while spawn can generate one when omitted.' },
|
|
32
|
+
tag: { type: 'string', description: 'Stable scope handle. Reuse the same tag for the same scope; never derive a variant by appending a number or suffix. send requires it, while spawn can generate one when omitted.' },
|
|
33
33
|
prompt: { type: 'string', description: 'Spawn task brief; also accepted for send.' },
|
|
34
34
|
message: { type: 'string', description: 'Send follow-up; also accepted for spawn.' },
|
|
35
35
|
file: { type: 'string', description: 'Path to task text; alternative to prompt/message for spawn/send.' },
|
|
@@ -7,17 +7,14 @@ import { readFileSync, statSync } from 'node:fs';
|
|
|
7
7
|
import { resolve } from 'node:path';
|
|
8
8
|
|
|
9
9
|
import { updateJsonAtomicSync } from '../../runtime/shared/atomic-file.mjs';
|
|
10
|
-
import { resolveAgentTerminalReapMs } from '../../session-runtime/config-helpers.mjs';
|
|
11
10
|
import { WORKER_INDEX_FILE } from './tool-def.mjs';
|
|
12
11
|
import { agentTagOf, clean, positiveInt, rowMatchesContext } from './helpers.mjs';
|
|
13
12
|
import {
|
|
14
13
|
applyWorkerRowUpsert,
|
|
15
|
-
isDeadWorkerStatus,
|
|
16
14
|
isLeadPoolAgent,
|
|
17
15
|
normalizeTagTombstones,
|
|
18
16
|
tagTombstoneKey,
|
|
19
17
|
workerRowKey,
|
|
20
|
-
workerRowTime,
|
|
21
18
|
} from './worker-rows.mjs';
|
|
22
19
|
|
|
23
20
|
export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCwds }) {
|
|
@@ -33,15 +30,11 @@ export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCw
|
|
|
33
30
|
return dataDir ? resolve(dataDir, WORKER_INDEX_FILE) : null;
|
|
34
31
|
}
|
|
35
32
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
33
|
+
// Reaping is an explicit lifecycle mutation owned by tag-registry. Silently
|
|
34
|
+
// filtering an expired row here would skip its tombstone and make persisted
|
|
35
|
+
// state disagree with the reusable-tag lease.
|
|
38
36
|
function keepWorkerRow(row = {}) {
|
|
39
|
-
|
|
40
|
-
const t = workerRowTime(row);
|
|
41
|
-
if (!t) return true;
|
|
42
|
-
if (!isDeadWorkerStatus(row.status || row.stage)) return true;
|
|
43
|
-
const reapMs = resolveAgentTerminalReapMs(cfgMod.loadConfig(), row.provider);
|
|
44
|
-
return reapMs == null || Date.now() - t < reapMs;
|
|
37
|
+
return Boolean(clean(row.tag) && clean(row.sessionId));
|
|
45
38
|
}
|
|
46
39
|
|
|
47
40
|
function normalizeWorkerRows(value) {
|
|
@@ -69,6 +62,7 @@ export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCw
|
|
|
69
62
|
lastUsedAt: clean(row.lastUsedAt) || null,
|
|
70
63
|
finishedAt: clean(row.finishedAt) || null,
|
|
71
64
|
turnStartedAt: clean(row.turnStartedAt) || null,
|
|
65
|
+
reapAt: clean(row.reapAt) || null,
|
|
72
66
|
clientHostPid: positiveInt(row.clientHostPid),
|
|
73
67
|
cwd: clean(row.cwd) || null,
|
|
74
68
|
task_id: clean(row.task_id || row.taskId) || null,
|
|
@@ -221,6 +215,9 @@ export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCw
|
|
|
221
215
|
// Turn dispatch stamps this; terminal upserts leave it null and the
|
|
222
216
|
// merge in applyWorkerRowUpsert preserves the running turn's value.
|
|
223
217
|
turnStartedAt: clean(extra.turnStartedAt) || null,
|
|
218
|
+
// Active/new work clears the previous lease. Terminal settlement writes
|
|
219
|
+
// the new absolute deadline immediately afterwards via scheduleReap().
|
|
220
|
+
reapAt: clean(extra.reapAt) || null,
|
|
224
221
|
clientHostPid: positiveInt(extra.clientHostPid) || positiveInt(session?.clientHostPid),
|
|
225
222
|
cwd: clean(session?.cwd) || clean(extra.cwd) || null,
|
|
226
223
|
task_id: clean(extra.task_id || extra.taskId) || null,
|
|
@@ -21,7 +21,9 @@ export function workerRowTime(row = {}) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export function isTerminalWorkerStatus(status) {
|
|
24
|
-
return /^(idle|closed|completed|failed|error|cancelled|canceled|killed|timeout)$/i.test(
|
|
24
|
+
return /^(idle|closed|complete|completed|done|success|fail|failed|error|cancelled|canceled|killed|timeout)$/i.test(
|
|
25
|
+
clean(status),
|
|
26
|
+
);
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/** Finished / gone. Idle is living and must stay in the process-global pool. */
|
|
@@ -100,6 +102,7 @@ export function workerRowToSession(row = {}) {
|
|
|
100
102
|
createdAt: row.createdAt || null,
|
|
101
103
|
updatedAt: row.updatedAt || null,
|
|
102
104
|
lastUsedAt: row.lastUsedAt || null,
|
|
105
|
+
reapAt: row.reapAt || null,
|
|
103
106
|
clientHostPid: row.clientHostPid || null,
|
|
104
107
|
cwd: row.cwd || null,
|
|
105
108
|
permission: row.permission || null,
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
cleanupBackgroundTasks,
|
|
6
6
|
getBackgroundTask,
|
|
7
7
|
listBackgroundTasks,
|
|
8
|
-
reconcileBackgroundTask,
|
|
9
8
|
startBackgroundTask,
|
|
10
9
|
sanitizeTaskMeta,
|
|
11
10
|
taskIdFromArgs,
|
|
@@ -51,6 +50,12 @@ import { createProviderInit } from './agent-tool/provider-init.mjs';
|
|
|
51
50
|
import { createNotify } from './agent-tool/notify.mjs';
|
|
52
51
|
import { createTagRegistry } from './agent-tool/tag-registry.mjs';
|
|
53
52
|
import { createJobViews } from './agent-tool/job-views.mjs';
|
|
53
|
+
import {
|
|
54
|
+
reconcileJobFinally,
|
|
55
|
+
reconcileJobStreamStalled,
|
|
56
|
+
reconcileJobTerminalResult,
|
|
57
|
+
reconcileJobWatchdogPartial,
|
|
58
|
+
} from './agent-tool/job-task-reconcile.mjs';
|
|
54
59
|
import { createSpawnFlow } from './agent-tool/spawn-flow.mjs';
|
|
55
60
|
import { resolveAgentSpawnPreset } from './agent-tool/spawn-preset.mjs';
|
|
56
61
|
import {
|
|
@@ -203,6 +208,7 @@ export function createStandaloneAgent({
|
|
|
203
208
|
progressWatchdogs,
|
|
204
209
|
startProgressIdleWatchdog,
|
|
205
210
|
turnStartStamper,
|
|
211
|
+
progressStamper,
|
|
206
212
|
prepareSpawn,
|
|
207
213
|
prepareSpawnInProcess,
|
|
208
214
|
runSpawn,
|
|
@@ -259,6 +265,7 @@ export function createStandaloneAgent({
|
|
|
259
265
|
// the watchdog sweep re-stamps turnStartedAt at each turn boundary.
|
|
260
266
|
const watchdog = startProgressIdleWatchdog(sessionId, resolveAgentWatchdogPolicy(sendAgent), sendAgent, {
|
|
261
267
|
onTurnStart: turnStartStamper(session, tag),
|
|
268
|
+
onProgress: progressStamper(session, tag),
|
|
262
269
|
});
|
|
263
270
|
const turnReview = createTurnReviewCollector(session, tag, sendAgent, notifyContext || {});
|
|
264
271
|
let finalStatus = 'idle';
|
|
@@ -294,24 +301,7 @@ export function createStandaloneAgent({
|
|
|
294
301
|
const value = completionValue(terminalResult);
|
|
295
302
|
if (job) job._terminalResultValue = value;
|
|
296
303
|
notifyOwnerAgentCompletionEarly(job, value, notifyContext || {});
|
|
297
|
-
|
|
298
|
-
// post-result save can't strand the task in `running`. Idempotent.
|
|
299
|
-
if (job?.taskId) {
|
|
300
|
-
try {
|
|
301
|
-
reconcileBackgroundTask(job.taskId, value.error
|
|
302
|
-
? {
|
|
303
|
-
status: 'failed',
|
|
304
|
-
result: value,
|
|
305
|
-
error: value.error,
|
|
306
|
-
terminalReason: 'agent-empty-final',
|
|
307
|
-
}
|
|
308
|
-
: {
|
|
309
|
-
status: 'completed',
|
|
310
|
-
result: value,
|
|
311
|
-
terminalReason: 'agent-terminal-result',
|
|
312
|
-
});
|
|
313
|
-
} catch {}
|
|
314
|
-
}
|
|
304
|
+
reconcileJobTerminalResult(job, value);
|
|
315
305
|
},
|
|
316
306
|
} : {}),
|
|
317
307
|
});
|
|
@@ -338,30 +328,11 @@ export function createStandaloneAgent({
|
|
|
338
328
|
stallAbort: true,
|
|
339
329
|
};
|
|
340
330
|
if (job) job._terminalResultValue = value;
|
|
341
|
-
|
|
342
|
-
try {
|
|
343
|
-
reconcileBackgroundTask(job.taskId, {
|
|
344
|
-
status: 'completed',
|
|
345
|
-
result: value,
|
|
346
|
-
terminalReason: 'agent-watchdog-partial',
|
|
347
|
-
});
|
|
348
|
-
} catch {}
|
|
349
|
-
}
|
|
331
|
+
reconcileJobWatchdogPartial(job, value);
|
|
350
332
|
return value;
|
|
351
333
|
}
|
|
352
334
|
finalStatus = 'error';
|
|
353
|
-
|
|
354
|
-
// result — reconcile to `failed` so the owner is notified rather than the
|
|
355
|
-
// task hanging in `running`. Idempotent (see runSpawn note).
|
|
356
|
-
if (job?.taskId && job._terminalResultValue === undefined) {
|
|
357
|
-
try {
|
|
358
|
-
reconcileBackgroundTask(job.taskId, {
|
|
359
|
-
status: 'failed',
|
|
360
|
-
error,
|
|
361
|
-
terminalReason: 'agent-stream-stalled',
|
|
362
|
-
});
|
|
363
|
-
} catch {}
|
|
364
|
-
}
|
|
335
|
+
reconcileJobStreamStalled(job, error);
|
|
365
336
|
throw error;
|
|
366
337
|
} finally {
|
|
367
338
|
turnReview.complete();
|
|
@@ -371,20 +342,7 @@ export function createStandaloneAgent({
|
|
|
371
342
|
stage: finalStatus,
|
|
372
343
|
finishedAt: new Date().toISOString(),
|
|
373
344
|
});
|
|
374
|
-
|
|
375
|
-
// step hung/threw after a terminal result was already produced.
|
|
376
|
-
if (job && job._terminalResultValue !== undefined) {
|
|
377
|
-
try {
|
|
378
|
-
reconcileBackgroundTask(job.taskId, {
|
|
379
|
-
status: finalStatus === 'error' ? 'failed' : 'completed',
|
|
380
|
-
result: job._terminalResultValue,
|
|
381
|
-
...(finalStatus === 'error' && job._terminalResultValue?.error
|
|
382
|
-
? { error: job._terminalResultValue.error }
|
|
383
|
-
: {}),
|
|
384
|
-
terminalReason: 'agent-finally-reconcile',
|
|
385
|
-
});
|
|
386
|
-
} catch {}
|
|
387
|
-
}
|
|
345
|
+
reconcileJobFinally(job, finalStatus);
|
|
388
346
|
scheduleReap(sessionId);
|
|
389
347
|
// Same lifecycle as a fresh spawn: the transcript/tag stays resumable,
|
|
390
348
|
// while heavy process-local runtime state is reclaimed immediately.
|
|
@@ -458,7 +416,6 @@ export function createStandaloneAgent({
|
|
|
458
416
|
}
|
|
459
417
|
cancelReap(sessionId);
|
|
460
418
|
const tag = tagForSession(sessionId);
|
|
461
|
-
forgetTerminalSession(tag, sessionId);
|
|
462
419
|
clearAgentStatuslineRoute(sessionId);
|
|
463
420
|
// Cancel any running background task bound to this session BEFORE closing
|
|
464
421
|
// the session. Otherwise closeSession rejects the in-flight runSpawn with
|
|
@@ -470,8 +427,16 @@ export function createStandaloneAgent({
|
|
|
470
427
|
if (row.sessionId !== sessionId && row.tag !== target) continue;
|
|
471
428
|
cancelBackgroundTask(row.task_id, 'cancelled by agent close');
|
|
472
429
|
}
|
|
430
|
+
// Close (and stamp cancelStatus) BEFORE dropping the worker row. Removing
|
|
431
|
+
// the row first left the next pool read with only a leftover heartbeat,
|
|
432
|
+
// which republished the session as `running`.
|
|
473
433
|
const ok = mgr.closeSession(sessionId, 'cli-agent-close');
|
|
474
434
|
if (task?.taskId) cancelBackgroundTask(task.taskId, 'cancelled by agent close');
|
|
435
|
+
// Drop the row only once the cancellation is DURABLY recorded. A close that
|
|
436
|
+
// failed still forgot the row, leaving the next pool read with a fresh
|
|
437
|
+
// heartbeat and no cancelled state — which republished the session as
|
|
438
|
+
// `running`, the exact lingering row this whole path exists to prevent.
|
|
439
|
+
if (ok) forgetTerminalSession(tag, sessionId);
|
|
475
440
|
return { closed: ok, tag, sessionId, task_id: task?.taskId || null };
|
|
476
441
|
}
|
|
477
442
|
|
|
@@ -41,13 +41,32 @@ export function createProgressWatchdogRegistry({ mgr }) {
|
|
|
41
41
|
state.lastAskStartedAt = askStartedAt;
|
|
42
42
|
try { state.onTurnStart?.(askStartedAt); } catch { /* bookkeeping only */ }
|
|
43
43
|
}
|
|
44
|
-
const abortErr = snapshot ? evaluateAgentWatchdogAbort(snapshot, now, watchdogPolicy) : null;
|
|
45
44
|
const sess = typeof mgr.getSession === 'function' ? mgr.getSession(sessionId) : null;
|
|
46
45
|
const iteration = typeof sess?.lastIterationIndex === 'number' ? sess.lastIterationIndex : null;
|
|
46
|
+
// In-turn progress. The worker index is otherwise stamped only at spawn and
|
|
47
|
+
// at turn boundaries, so a FIRST turn that runs dozens of tool iterations
|
|
48
|
+
// leaves the Agents panel frozen on its start stamp for minutes (user
|
|
49
|
+
// report: 위임한 세션이 안 도는 것처럼 보인다). This sweep already holds the
|
|
50
|
+
// live session, so reporting the change costs no extra read.
|
|
51
|
+
if (state.onProgress) {
|
|
52
|
+
const messages = Array.isArray(sess?.messages) ? sess.messages.length : 0;
|
|
53
|
+
const signature = `${iteration ?? ''}:${messages}`;
|
|
54
|
+
if (signature !== state.lastProgressSignature) {
|
|
55
|
+
state.lastProgressSignature = signature;
|
|
56
|
+
try { state.onProgress({ iteration, messages }); } catch { /* bookkeeping only */ }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const abortErr = snapshot ? evaluateAgentWatchdogAbort(snapshot, now, watchdogPolicy) : null;
|
|
47
60
|
// No progress snapshot: fall back to the coarse last-progress timestamp so a
|
|
48
61
|
// session that never reports structured progress can still stall out.
|
|
49
62
|
if (!abortErr && !snapshot) {
|
|
50
|
-
|
|
63
|
+
// start() admits a manager that provides EITHER progress source, so this
|
|
64
|
+
// fallback must not assume the coarse one exists: throwing here escaped
|
|
65
|
+
// the shared interval and disabled watchdog coverage for every session.
|
|
66
|
+
let reported = 0;
|
|
67
|
+
if (typeof mgr.getSessionLastProgressAt === 'function') {
|
|
68
|
+
try { reported = Number(mgr.getSessionLastProgressAt(sessionId)) || 0; } catch { reported = 0; }
|
|
69
|
+
}
|
|
51
70
|
const last = reported || anchorTs;
|
|
52
71
|
if (watchdogPolicy.idleStaleMs > 0 && now - last > watchdogPolicy.idleStaleMs) {
|
|
53
72
|
abortAgentProgressWatchdog(controller, {
|
|
@@ -80,7 +99,10 @@ export function createProgressWatchdogRegistry({ mgr }) {
|
|
|
80
99
|
function ensureTimer() {
|
|
81
100
|
if (timer || watched.size === 0) return;
|
|
82
101
|
timer = setInterval(() => {
|
|
83
|
-
|
|
102
|
+
// One session's failure must never cancel the sweep for the others.
|
|
103
|
+
for (const state of [...watched.values()]) {
|
|
104
|
+
try { check(state); } catch { /* watchdog is best-effort */ }
|
|
105
|
+
}
|
|
84
106
|
}, WATCHDOG_SWEEP_INTERVAL_MS);
|
|
85
107
|
timer.unref?.();
|
|
86
108
|
}
|
|
@@ -88,7 +110,7 @@ export function createProgressWatchdogRegistry({ mgr }) {
|
|
|
88
110
|
return {
|
|
89
111
|
/** Watch a session until the returned handle is stopped; null when the
|
|
90
112
|
* policy is off or the manager cannot link an abort signal. */
|
|
91
|
-
start(sessionId, watchdogPolicy, agent = null, { onTurnStart = null } = {}) {
|
|
113
|
+
start(sessionId, watchdogPolicy, agent = null, { onTurnStart = null, onProgress = null } = {}) {
|
|
92
114
|
if (!sessionId || !agentWatchdogPolicyActive(watchdogPolicy)) return null;
|
|
93
115
|
if (typeof mgr.getSessionProgressSnapshot !== 'function'
|
|
94
116
|
&& typeof mgr.getSessionLastProgressAt !== 'function') return null;
|
|
@@ -103,7 +125,9 @@ export function createProgressWatchdogRegistry({ mgr }) {
|
|
|
103
125
|
controller,
|
|
104
126
|
anchorTs,
|
|
105
127
|
onTurnStart: typeof onTurnStart === 'function' ? onTurnStart : null,
|
|
128
|
+
onProgress: typeof onProgress === 'function' ? onProgress : null,
|
|
106
129
|
lastAskStartedAt: 0,
|
|
130
|
+
lastProgressSignature: '',
|
|
107
131
|
};
|
|
108
132
|
watched.set(sessionId, state);
|
|
109
133
|
ensureTimer();
|
|
@@ -8,24 +8,13 @@
|
|
|
8
8
|
import http from 'node:http';
|
|
9
9
|
import { randomUUID } from 'node:crypto';
|
|
10
10
|
import { readFileSync } from 'node:fs';
|
|
11
|
-
|
|
12
|
-
function parsePid(value) {
|
|
13
|
-
const n = Number(value);
|
|
14
|
-
return Number.isInteger(n) && n > 0 ? n : null;
|
|
15
|
-
}
|
|
11
|
+
import { isPidAlive, parsePid } from '../runtime/shared/pid-liveness.mjs';
|
|
16
12
|
|
|
17
13
|
function parsePort(value) {
|
|
18
14
|
const n = Number(value);
|
|
19
15
|
return Number.isInteger(n) && n > 0 && n < 65536 ? n : null;
|
|
20
16
|
}
|
|
21
17
|
|
|
22
|
-
function isPidAlive(pid) {
|
|
23
|
-
const n = parsePid(pid);
|
|
24
|
-
if (!n) return false;
|
|
25
|
-
try { process.kill(n, 0); return true; }
|
|
26
|
-
catch (error) { return error?.code === 'EPERM'; }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
18
|
// Read + validate the discovery file. Returns null when missing/corrupt or when
|
|
30
19
|
// the recorded pid is dead (stale daemon) so the caller reclaims + respawns.
|
|
31
20
|
export function readChannelDiscovery(discoveryPath) {
|
|
@@ -153,6 +142,11 @@ export async function attachChannel({
|
|
|
153
142
|
if (!isExpectedDaemon(initialHealth)) throw staleDiscoveryError('daemon discovery pid does not match health');
|
|
154
143
|
|
|
155
144
|
let reg;
|
|
145
|
+
// A stable registration id even on the FIRST attach: if this response is
|
|
146
|
+
// lost, the daemon reaps the never-acknowledged token through its replay TTL
|
|
147
|
+
// instead of holding an unreapable client against a live pid forever (which
|
|
148
|
+
// blocks daemon self-shutdown).
|
|
149
|
+
const initialRegistrationId = randomUUID();
|
|
156
150
|
try {
|
|
157
151
|
reg = await request({
|
|
158
152
|
port,
|
|
@@ -161,7 +155,10 @@ export async function attachChannel({
|
|
|
161
155
|
path: '/client/register',
|
|
162
156
|
// Channel attachment observes the current owner and never claims the
|
|
163
157
|
// manual routing seat merely by opening the transport.
|
|
164
|
-
body: {
|
|
158
|
+
body: {
|
|
159
|
+
leadPid, cwd, passive: true, restoreSessionId,
|
|
160
|
+
registrationId: initialRegistrationId,
|
|
161
|
+
},
|
|
165
162
|
timeoutMs: 3000,
|
|
166
163
|
agent: controlAgent,
|
|
167
164
|
});
|
|
@@ -186,7 +183,12 @@ export async function attachChannel({
|
|
|
186
183
|
let reconnectReplaceToken = null;
|
|
187
184
|
let lifecycle = 0;
|
|
188
185
|
let stableTimer = null;
|
|
186
|
+
let livenessTimer = null;
|
|
189
187
|
let closePromise = null;
|
|
188
|
+
// The daemon writes a `: ka` comment every 15s. A half-open socket delivers
|
|
189
|
+
// no bytes and no FIN, so without this deadline the stream stays "connected"
|
|
190
|
+
// forever and the client never reconnects.
|
|
191
|
+
const STREAM_LIVENESS_MS = 45_000;
|
|
190
192
|
// Bounded reconnect is only for a verified-live daemon's transient SSE loss.
|
|
191
193
|
// A stale/dead endpoint signals onFatal immediately so the owner re-reads
|
|
192
194
|
// discovery instead of spinning against the captured port.
|
|
@@ -220,6 +222,10 @@ export async function attachChannel({
|
|
|
220
222
|
if (stableTimer) { try { clearTimeout(stableTimer); } catch {} stableTimer = null; }
|
|
221
223
|
}
|
|
222
224
|
|
|
225
|
+
function clearLivenessTimer() {
|
|
226
|
+
if (livenessTimer) { try { clearTimeout(livenessTimer); } catch {} livenessTimer = null; }
|
|
227
|
+
}
|
|
228
|
+
|
|
223
229
|
function signalFatal(reason) {
|
|
224
230
|
if (closed || fatal) return;
|
|
225
231
|
fatal = true;
|
|
@@ -227,6 +233,7 @@ export async function attachChannel({
|
|
|
227
233
|
lifecycle++;
|
|
228
234
|
if (reconnectTimer) { try { clearTimeout(reconnectTimer); } catch {} reconnectTimer = null; }
|
|
229
235
|
clearStableTimer();
|
|
236
|
+
clearLivenessTimer();
|
|
230
237
|
try { sseReq?.destroy?.(); } catch {}
|
|
231
238
|
log(`sse stale endpoint (${reason}); signalling re-attach`);
|
|
232
239
|
try { onFatal(reason); } catch {}
|
|
@@ -251,6 +258,9 @@ export async function attachChannel({
|
|
|
251
258
|
|
|
252
259
|
function openStream() {
|
|
253
260
|
if (closed) return;
|
|
261
|
+
// A liveness expiry is a transient stream loss (reconnect), not a dead
|
|
262
|
+
// endpoint: keep the destroy() it triggers off the fatal path.
|
|
263
|
+
let livenessLost = false;
|
|
254
264
|
const req = http.request({
|
|
255
265
|
hostname: '127.0.0.1',
|
|
256
266
|
port,
|
|
@@ -278,9 +288,22 @@ export async function attachChannel({
|
|
|
278
288
|
if (!closed && !fatal && req === sseReq) reconnectAttempts = 0;
|
|
279
289
|
}, STABLE_STREAM_MS);
|
|
280
290
|
stableTimer.unref?.();
|
|
291
|
+
const armLiveness = () => {
|
|
292
|
+
clearLivenessTimer();
|
|
293
|
+
livenessTimer = setTimeout(() => {
|
|
294
|
+
if (closed || fatal || req !== sseReq) return;
|
|
295
|
+
livenessLost = true;
|
|
296
|
+
handleStreamLoss(`sse liveness timeout after ${STREAM_LIVENESS_MS}ms`);
|
|
297
|
+
try { req.destroy(new Error('channel SSE liveness timeout')); } catch {}
|
|
298
|
+
}, STREAM_LIVENESS_MS);
|
|
299
|
+
livenessTimer.unref?.();
|
|
300
|
+
};
|
|
301
|
+
armLiveness();
|
|
281
302
|
let buf = '';
|
|
282
303
|
res.on('data', (chunk) => {
|
|
283
304
|
if (req !== sseReq || closed) return;
|
|
305
|
+
// Any byte, including a `: ka` keepalive comment, proves liveness.
|
|
306
|
+
armLiveness();
|
|
284
307
|
buf += chunk;
|
|
285
308
|
let idx;
|
|
286
309
|
while ((idx = buf.indexOf('\n\n')) >= 0) {
|
|
@@ -299,18 +322,21 @@ export async function attachChannel({
|
|
|
299
322
|
res.on('end', () => {
|
|
300
323
|
if (req === sseReq) {
|
|
301
324
|
clearStableTimer();
|
|
325
|
+
clearLivenessTimer();
|
|
302
326
|
handleStreamLoss('sse ended');
|
|
303
327
|
}
|
|
304
328
|
});
|
|
305
329
|
res.on('error', () => {
|
|
306
330
|
if (req === sseReq) {
|
|
307
331
|
clearStableTimer();
|
|
332
|
+
clearLivenessTimer();
|
|
308
333
|
handleStreamLoss('sse error');
|
|
309
334
|
}
|
|
310
335
|
});
|
|
311
336
|
});
|
|
312
337
|
req.on('error', () => {
|
|
313
|
-
if (req
|
|
338
|
+
if (req !== sseReq || livenessLost) return;
|
|
339
|
+
signalFatal('sse req error');
|
|
314
340
|
});
|
|
315
341
|
sseReq = req;
|
|
316
342
|
req.end();
|
|
@@ -384,7 +410,12 @@ export async function attachChannel({
|
|
|
384
410
|
err.daemonTransportError = true;
|
|
385
411
|
throw err;
|
|
386
412
|
}
|
|
387
|
-
if (out && out.error)
|
|
413
|
+
if (out && out.error) {
|
|
414
|
+
// Preserve the daemon's machine-readable classification across the wire.
|
|
415
|
+
const err = new Error(out.error);
|
|
416
|
+
if (out.code) err.code = String(out.code);
|
|
417
|
+
throw err;
|
|
418
|
+
}
|
|
388
419
|
return out?.result;
|
|
389
420
|
}
|
|
390
421
|
|
|
@@ -394,6 +425,7 @@ export async function attachChannel({
|
|
|
394
425
|
lifecycle++;
|
|
395
426
|
if (reconnectTimer) { try { clearTimeout(reconnectTimer); } catch {} reconnectTimer = null; }
|
|
396
427
|
clearStableTimer();
|
|
428
|
+
clearLivenessTimer();
|
|
397
429
|
try { sseReq?.destroy?.(); } catch {}
|
|
398
430
|
// A re-register already in flight can mint a fresh token after close().
|
|
399
431
|
// Await it so its stale branch deregisters that token before close resolves.
|