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
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
SESSION_READ_ACTION_SET,
|
|
27
27
|
} from './session-protocol.mjs';
|
|
28
28
|
import { readSingletonOwner } from '../runtime/shared/singleton-owner.mjs';
|
|
29
|
+
import { isPidAlive } from '../runtime/shared/pid-liveness.mjs';
|
|
29
30
|
import { resolveRuntimeRoot } from '../runtime/shared/runtime-root.mjs';
|
|
30
31
|
|
|
31
32
|
function runtimeRoot() {
|
|
@@ -86,6 +87,7 @@ const EVENT_STREAM_RECONNECT_MAX_MS = 30_000;
|
|
|
86
87
|
const EVENT_STREAM_LIVENESS_TIMEOUT_MS = 45_000;
|
|
87
88
|
const EVENT_STREAM_RECONNECT_BUDGET_MS = 10 * 60_000;
|
|
88
89
|
const DEFAULT_DAEMON_READY_TIMEOUT_MS = 15_000;
|
|
90
|
+
const DEFAULT_DAEMON_UPGRADE_TIMEOUT_MS = 120_000;
|
|
89
91
|
const DAEMON_OWNER_POLL_MS = 50;
|
|
90
92
|
// A daemon that restarted needs a moment to rebind its port, so one immediate
|
|
91
93
|
// re-attach often lands in the same gap the first call died in. Mirror the
|
|
@@ -146,13 +148,6 @@ export async function probeSessionHealth({ port, token, timeoutMs = 800 } = {})
|
|
|
146
148
|
} catch { return null; }
|
|
147
149
|
}
|
|
148
150
|
|
|
149
|
-
function isPidAlive(pid) {
|
|
150
|
-
const value = Number(pid);
|
|
151
|
-
if (!Number.isInteger(value) || value <= 0) return false;
|
|
152
|
-
try { process.kill(value, 0); return true; }
|
|
153
|
-
catch (error) { return error?.code === 'EPERM'; }
|
|
154
|
-
}
|
|
155
|
-
|
|
156
151
|
export function readSessionDiscovery(discoveryPath = sessionDiscoveryPath()) {
|
|
157
152
|
const readUnified = (candidate) => {
|
|
158
153
|
const parsed = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
@@ -208,9 +203,15 @@ export function sessionDaemonCompatibility(health, {
|
|
|
208
203
|
|
|
209
204
|
async function replaceLowerDaemon(discovery, initialHealth, { log }) {
|
|
210
205
|
const configuredTimeoutMs = Number(process.env.MIXDOG_DAEMON_UPGRADE_TIMEOUT_MS);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
206
|
+
// A stuck older daemon (a worker that never finishes draining) must not hang
|
|
207
|
+
// every newer client forever: the wait is always bounded, and the caller
|
|
208
|
+
// surfaces `daemonUpgradePending` instead of blocking indefinitely.
|
|
209
|
+
// Infinity/NaN are NOT a configuration — they are the unbounded wait this
|
|
210
|
+
// deadline exists to prevent, so they fall back to the default.
|
|
211
|
+
const boundedTimeoutMs = Number.isFinite(configuredTimeoutMs) && configuredTimeoutMs > 0
|
|
212
|
+
? Math.max(10_000, configuredTimeoutMs)
|
|
213
|
+
: DEFAULT_DAEMON_UPGRADE_TIMEOUT_MS;
|
|
214
|
+
const deadline = Date.now() + boundedTimeoutMs;
|
|
214
215
|
const result = await request({
|
|
215
216
|
port: discovery.port,
|
|
216
217
|
token: discovery.token,
|
|
@@ -638,7 +639,12 @@ export async function attachSession({
|
|
|
638
639
|
err.daemonTransportError = true;
|
|
639
640
|
throw err;
|
|
640
641
|
}
|
|
641
|
-
if (out && out.error)
|
|
642
|
+
if (out && out.error) {
|
|
643
|
+
// Preserve the daemon's machine-readable classification across the wire.
|
|
644
|
+
const err = new Error(out.error);
|
|
645
|
+
if (out.code) err.code = String(out.code);
|
|
646
|
+
throw err;
|
|
647
|
+
}
|
|
642
648
|
return out?.result;
|
|
643
649
|
}
|
|
644
650
|
|
|
@@ -751,6 +757,13 @@ async function ensureSharedAttachment({ cwd, log }) {
|
|
|
751
757
|
onFatal: (reason) => {
|
|
752
758
|
log(`daemon attachment lost (${reason})`);
|
|
753
759
|
if (shared === state) shared = null;
|
|
760
|
+
// The dead attachment still owns a server-side client record and its
|
|
761
|
+
// session subscriptions. Release it explicitly; otherwise every fatal
|
|
762
|
+
// re-attach cycle leaks one client (and one subscriber per session) on
|
|
763
|
+
// the daemon until its pid sweep runs.
|
|
764
|
+
void Promise.resolve()
|
|
765
|
+
.then(() => state.client?.close?.(`attachment lost: ${reason}`))
|
|
766
|
+
.catch((err) => log(`daemon attachment release failed: ${err?.message || err}`));
|
|
754
767
|
for (const bucket of views.values()) {
|
|
755
768
|
for (const view of [...bucket]) view.applyDetached(reason);
|
|
756
769
|
}
|
|
@@ -872,8 +885,19 @@ export async function createSession(options = {}) {
|
|
|
872
885
|
}
|
|
873
886
|
|
|
874
887
|
let resyncing = false;
|
|
888
|
+
// A revision gap that lands while a read is already in flight is NOT covered
|
|
889
|
+
// by that read: it must re-run afterwards, otherwise the view stays stale
|
|
890
|
+
// forever behind a coalesced request.
|
|
891
|
+
let resyncDirty = false;
|
|
892
|
+
let resyncDirtyReason = '';
|
|
893
|
+
let resyncFailed = false;
|
|
875
894
|
function resync(reason) {
|
|
876
|
-
if (disposed
|
|
895
|
+
if (disposed) return;
|
|
896
|
+
if (resyncing) {
|
|
897
|
+
resyncDirty = true;
|
|
898
|
+
resyncDirtyReason = String(reason || 'revision gap');
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
877
901
|
resyncing = true;
|
|
878
902
|
const requestAttachment = attachment;
|
|
879
903
|
void requestAttachment.client.read({
|
|
@@ -885,10 +909,28 @@ export async function createSession(options = {}) {
|
|
|
885
909
|
if (result && typeof result === 'object') resultAttachments.set(result, requestAttachment);
|
|
886
910
|
if (!applyBody(result, requestAttachment) && result?.revision !== undefined) {
|
|
887
911
|
log(`session ${sessionId} resync returned an unusable body`);
|
|
912
|
+
resyncDirty = true;
|
|
913
|
+
resyncFailed = true;
|
|
914
|
+
resyncDirtyReason = 'unusable resync body';
|
|
888
915
|
}
|
|
889
916
|
}).catch((err) => {
|
|
890
917
|
log(`session ${sessionId} resync after ${reason} failed: ${err?.message || err}`);
|
|
891
|
-
|
|
918
|
+
resyncDirty = true;
|
|
919
|
+
resyncFailed = true;
|
|
920
|
+
resyncDirtyReason = `retry after failed resync (${reason})`;
|
|
921
|
+
}).finally(() => {
|
|
922
|
+
resyncing = false;
|
|
923
|
+
if (!resyncDirty || disposed) return;
|
|
924
|
+
resyncDirty = false;
|
|
925
|
+
const retryReason = resyncDirtyReason || 'revision gap';
|
|
926
|
+
resyncDirtyReason = '';
|
|
927
|
+
if (!resyncFailed) { resync(retryReason); return; }
|
|
928
|
+
// A failing daemon must not spin: retry the coalesced gap on a short
|
|
929
|
+
// fixed delay instead of a tight loop.
|
|
930
|
+
resyncFailed = false;
|
|
931
|
+
const timer = setTimeout(() => { if (!disposed) resync(retryReason); }, 250);
|
|
932
|
+
timer.unref?.();
|
|
933
|
+
});
|
|
892
934
|
}
|
|
893
935
|
|
|
894
936
|
let recoveryPromise = null;
|
|
@@ -1038,6 +1080,13 @@ export async function createSession(options = {}) {
|
|
|
1038
1080
|
transitionChain = run.then(() => {}, () => {});
|
|
1039
1081
|
return run;
|
|
1040
1082
|
}
|
|
1083
|
+
// Mutations must not queue behind each other (ESC/abort stays immediate), but
|
|
1084
|
+
// they MUST observe the session a new/resume transition is rebinding to.
|
|
1085
|
+
// Joining the current tail of the transition chain — without extending it —
|
|
1086
|
+
// keeps a prompt from routing to the session that was just replaced.
|
|
1087
|
+
function afterPendingTransition() {
|
|
1088
|
+
return transitionChain;
|
|
1089
|
+
}
|
|
1041
1090
|
|
|
1042
1091
|
function remoteCall(method, args, callOptions = {}) {
|
|
1043
1092
|
if (disposed) return Promise.reject(new Error('This session view is disposed.'));
|
|
@@ -1047,21 +1096,28 @@ export async function createSession(options = {}) {
|
|
|
1047
1096
|
? 'session.configure'
|
|
1048
1097
|
: null;
|
|
1049
1098
|
if (!route) return Promise.reject(new TypeError(`session action ${method} is unavailable`));
|
|
1050
|
-
const targetSessionId = sessionId;
|
|
1051
|
-
const baseRevision = baseRevisionFor(attachment, targetSessionId);
|
|
1052
1099
|
const stableCallId = typeof callOptions.callId === 'string' && callOptions.callId.trim()
|
|
1053
1100
|
? callOptions.callId.trim()
|
|
1054
1101
|
: randomUUID();
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1102
|
+
const dispatch = async () => {
|
|
1103
|
+
if (disposed) throw new Error('This session view is disposed.');
|
|
1104
|
+
const targetSessionId = sessionId;
|
|
1105
|
+
const baseRevision = baseRevisionFor(attachment, targetSessionId);
|
|
1106
|
+
const result = await sendCall(route, {
|
|
1107
|
+
sessionId: targetSessionId,
|
|
1108
|
+
action: method,
|
|
1109
|
+
args,
|
|
1110
|
+
open: openParams,
|
|
1111
|
+
baseRevision,
|
|
1112
|
+
}, stableCallId);
|
|
1062
1113
|
if (!disposed && sessionId === targetSessionId) await applyResult(result, method);
|
|
1063
1114
|
return result?.value ?? null;
|
|
1064
|
-
}
|
|
1115
|
+
};
|
|
1116
|
+
// Reads stay immediate; a configure action is a mutation and waits out an
|
|
1117
|
+
// in-flight new/resume rebind.
|
|
1118
|
+
return route === 'session.configure'
|
|
1119
|
+
? afterPendingTransition().then(dispatch)
|
|
1120
|
+
: dispatch();
|
|
1065
1121
|
}
|
|
1066
1122
|
|
|
1067
1123
|
async function rebindTo(result, previousSessionId) {
|
|
@@ -1122,6 +1178,9 @@ export async function createSession(options = {}) {
|
|
|
1122
1178
|
if (disposed) throw new Error('This session view is disposed.');
|
|
1123
1179
|
const submissionId = String(options?.id || '').trim()
|
|
1124
1180
|
|| `session-submit-${process.pid}-${Date.now()}-${(submitSeq += 1)}`;
|
|
1181
|
+
// Never address the session a new/resume transition is replacing.
|
|
1182
|
+
await afterPendingTransition();
|
|
1183
|
+
if (disposed) throw new Error('This session view is disposed.');
|
|
1125
1184
|
const targetSessionId = sessionId;
|
|
1126
1185
|
const baseRevision = baseRevisionFor(attachment, targetSessionId);
|
|
1127
1186
|
const result = await sendCall('session.submit', {
|
|
@@ -1136,6 +1195,10 @@ export async function createSession(options = {}) {
|
|
|
1136
1195
|
}
|
|
1137
1196
|
|
|
1138
1197
|
async function abortAsync(options = {}) {
|
|
1198
|
+
if (disposed) return { aborted: false };
|
|
1199
|
+
// Abort skips the ordinary call queue but still targets the CURRENT
|
|
1200
|
+
// session: a rebind in flight owns the runtime the user is aborting.
|
|
1201
|
+
await afterPendingTransition();
|
|
1139
1202
|
if (disposed) return { aborted: false };
|
|
1140
1203
|
const result = await sendCall('session.abort', {
|
|
1141
1204
|
sessionId, open: openParams, options,
|
|
@@ -64,7 +64,6 @@ export const SESSION_CONFIGURE_ACTIONS = Object.freeze([
|
|
|
64
64
|
'cancelMediaJob',
|
|
65
65
|
'cancelOAuthProviderLogin',
|
|
66
66
|
'checkForUpdate',
|
|
67
|
-
'claimRemote',
|
|
68
67
|
'clear',
|
|
69
68
|
'compact',
|
|
70
69
|
'completeOnboarding',
|
|
@@ -148,7 +147,6 @@ export const SESSION_CONFIGURE_ACTIONS = Object.freeze([
|
|
|
148
147
|
'switchContext',
|
|
149
148
|
'taskControl',
|
|
150
149
|
'toggleFast',
|
|
151
|
-
'toggleRemote',
|
|
152
150
|
'toggleVoice',
|
|
153
151
|
'transcribeAudio',
|
|
154
152
|
'updatePlugin',
|
|
@@ -72,15 +72,16 @@ test('an unhealthy runtime worker is replaced and every runtime is recovered', a
|
|
|
72
72
|
const originalPid = host.status.worker.pid;
|
|
73
73
|
assert.equal((await first.submitAsync('trigger')).pid, originalPid);
|
|
74
74
|
await waitFor(
|
|
75
|
-
() => host.status.worker.pid
|
|
76
|
-
|
|
75
|
+
() => host.status.worker.pid
|
|
76
|
+
&& host.status.worker.pid !== originalPid
|
|
77
|
+
&& logs.some((line) => /recycling.*unhealthy/.test(line))
|
|
78
|
+
&& logs.some((line) => /recovered 2 runtime/.test(line)),
|
|
79
|
+
'replacement runtime worker recovery',
|
|
77
80
|
);
|
|
78
81
|
const healthyFirst = await first.submitAsync('healthy');
|
|
79
82
|
const healthySecond = await second.submitAsync('healthy');
|
|
80
83
|
assert.notEqual(healthyFirst.pid, originalPid);
|
|
81
84
|
assert.equal(healthySecond.pid, healthyFirst.pid);
|
|
82
|
-
assert.ok(logs.some((line) => /unhealthy; recycling/.test(line)));
|
|
83
|
-
assert.ok(logs.some((line) => /recovered 2 runtime/.test(line)));
|
|
84
85
|
} finally {
|
|
85
86
|
await host.close('test complete');
|
|
86
87
|
await rm(dir, { recursive: true, force: true });
|
|
@@ -30,6 +30,24 @@ const RUNTIME_METHODS = new Set([
|
|
|
30
30
|
'dispose',
|
|
31
31
|
]);
|
|
32
32
|
|
|
33
|
+
export function shouldRecycleSessionRuntimeWorker({
|
|
34
|
+
rssBytes = 0,
|
|
35
|
+
thresholdBytes = 0,
|
|
36
|
+
daemonBusy = 0,
|
|
37
|
+
pending = 0,
|
|
38
|
+
activeResources = 0,
|
|
39
|
+
now = Date.now(),
|
|
40
|
+
lastRecycleAt = 0,
|
|
41
|
+
cooldownMs = 0,
|
|
42
|
+
} = {}) {
|
|
43
|
+
return Number(thresholdBytes) > 0
|
|
44
|
+
&& Number(rssBytes) >= Number(thresholdBytes)
|
|
45
|
+
&& Number(daemonBusy) === 0
|
|
46
|
+
&& Number(pending) === 0
|
|
47
|
+
&& Number(activeResources) === 0
|
|
48
|
+
&& Number(now) - Number(lastRecycleAt) >= Math.max(0, Number(cooldownMs) || 0);
|
|
49
|
+
}
|
|
50
|
+
|
|
33
51
|
export function applyRuntimeStateFrame(previous, frame) {
|
|
34
52
|
if (frame?.full && typeof frame.full === 'object') return frame.full;
|
|
35
53
|
const patch = frame?.patch;
|
|
@@ -84,6 +102,10 @@ class SessionRuntimeWorker {
|
|
|
84
102
|
|
|
85
103
|
onMessage(message, child = this.child) {
|
|
86
104
|
if (!message || typeof message !== 'object') return;
|
|
105
|
+
// A superseded child (recycled, crashed, or replaced) can still flush
|
|
106
|
+
// queued frames after its replacement is live. Its late state/response must
|
|
107
|
+
// never outrank the replacement's revision or settle its pending calls.
|
|
108
|
+
if (child !== this.child) return;
|
|
87
109
|
if (message.type === 'token-native-prewarm') {
|
|
88
110
|
if (child === this.child) prewarmNativeTokenCounter();
|
|
89
111
|
return;
|
|
@@ -162,6 +184,12 @@ class SessionRuntimeWorker {
|
|
|
162
184
|
handleChildFailure(child, error) {
|
|
163
185
|
if (this.failedChildren.has(child)) return;
|
|
164
186
|
this.failedChildren.add(child);
|
|
187
|
+
// Same identity rule for failures: a retired child's late exit/error must
|
|
188
|
+
// not reject the replacement's in-flight calls or start a second recovery.
|
|
189
|
+
if (this.child && this.child !== child) {
|
|
190
|
+
this.log(`session runtime worker superseded child failed (ignored): ${error?.message || error}`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
165
193
|
if (this.child === child) this.child = null;
|
|
166
194
|
// A dead child cannot spawn: its machine-budget slots return immediately.
|
|
167
195
|
this.releaseAllSpawnLeases();
|
|
@@ -197,16 +225,29 @@ class SessionRuntimeWorker {
|
|
|
197
225
|
return false;
|
|
198
226
|
}
|
|
199
227
|
|
|
200
|
-
|
|
228
|
+
recycleChild(child, reason) {
|
|
201
229
|
if (this.closed || child !== this.child || this.recycling) return;
|
|
202
|
-
|
|
203
|
-
this.
|
|
204
|
-
|
|
230
|
+
this.log(`session runtime worker recycling pid=${child.pid || 'unknown'}: ${reason}`);
|
|
231
|
+
this.recycling = this.stopChild(reason)
|
|
232
|
+
.then(() => true)
|
|
205
233
|
.catch((error) => {
|
|
206
|
-
this.log(`session runtime worker
|
|
234
|
+
this.log(`session runtime worker recycle failed: ${error?.message || error}`);
|
|
207
235
|
try { child.kill?.(); } catch {}
|
|
236
|
+
return false;
|
|
208
237
|
})
|
|
209
238
|
.finally(() => { this.recycling = null; });
|
|
239
|
+
return this.recycling;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
recycleUnhealthy(child, detail = null) {
|
|
243
|
+
const reason = String(detail?.reason || 'session runtime worker reported unhealthy');
|
|
244
|
+
void this.recycleChild(child, `unhealthy: ${reason}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
recycleForMemory(rssBytes) {
|
|
248
|
+
const rssMb = Math.round(Number(rssBytes) / 1024 / 1024);
|
|
249
|
+
return this.recycleChild(this.child, `idle memory reclaim (${rssMb} MB RSS)`)
|
|
250
|
+
|| Promise.resolve(false);
|
|
210
251
|
}
|
|
211
252
|
|
|
212
253
|
requestRaw(type, payload = {}, timeoutMs = 10 * 60_000) {
|
|
@@ -331,9 +372,11 @@ class SessionRuntimeWorker {
|
|
|
331
372
|
});
|
|
332
373
|
try { await this.requestRaw('shutdown', { reason }, 8_000); } catch {}
|
|
333
374
|
try { child.disconnect?.(); } catch {}
|
|
375
|
+
// The child drains its accepted-input writes before exiting; a hard kill
|
|
376
|
+
// inside that window is exactly what loses them.
|
|
334
377
|
await Promise.race([
|
|
335
378
|
exitPromise,
|
|
336
|
-
new Promise((resolve) => setTimeout(resolve,
|
|
379
|
+
new Promise((resolve) => setTimeout(resolve, 2_000)),
|
|
337
380
|
]);
|
|
338
381
|
if (!exited) {
|
|
339
382
|
try { child.kill?.(); } catch {}
|
|
@@ -472,6 +515,7 @@ export function createSessionRuntimeHost({
|
|
|
472
515
|
cwd = process.cwd(),
|
|
473
516
|
env = process.env,
|
|
474
517
|
log = () => {},
|
|
518
|
+
canRecycle = () => true,
|
|
475
519
|
} = {}) {
|
|
476
520
|
const worker = new SessionRuntimeWorker({
|
|
477
521
|
workerEntry,
|
|
@@ -487,8 +531,23 @@ export function createSessionRuntimeHost({
|
|
|
487
531
|
|
|
488
532
|
// Runtime workload telemetry is refreshed lazily with a short TTL.
|
|
489
533
|
const WORKLOAD_TTL_MS = 2_000;
|
|
534
|
+
const configuredRecycleMb = Number(env.MIXDOG_SESSION_WORKER_RECYCLE_RSS_MB);
|
|
535
|
+
const MEMORY_RECYCLE_THRESHOLD_BYTES = (
|
|
536
|
+
Number.isFinite(configuredRecycleMb) && configuredRecycleMb >= 0
|
|
537
|
+
? configuredRecycleMb
|
|
538
|
+
: 384
|
|
539
|
+
) * 1024 * 1024;
|
|
540
|
+
const MEMORY_RECYCLE_INTERVAL_MS = Math.max(
|
|
541
|
+
5_000,
|
|
542
|
+
Number(env.MIXDOG_SESSION_WORKER_RECYCLE_INTERVAL_MS) || 60_000,
|
|
543
|
+
);
|
|
544
|
+
const MEMORY_RECYCLE_COOLDOWN_MS = Math.max(
|
|
545
|
+
60_000,
|
|
546
|
+
Number(env.MIXDOG_SESSION_WORKER_RECYCLE_COOLDOWN_MS) || 15 * 60_000,
|
|
547
|
+
);
|
|
490
548
|
let workloadCache = { refreshedAt: 0, worker: null };
|
|
491
549
|
let workloadRefresh = null;
|
|
550
|
+
let lastMemoryRecycleAt = Date.now();
|
|
492
551
|
function refreshRuntimeWorkload() {
|
|
493
552
|
if (workloadRefresh) return workloadRefresh;
|
|
494
553
|
if (!worker.child || worker.child.killed) {
|
|
@@ -512,6 +571,36 @@ export function createSessionRuntimeHost({
|
|
|
512
571
|
return workloadRefresh;
|
|
513
572
|
}
|
|
514
573
|
|
|
574
|
+
async function maybeRecycleWorkerMemory() {
|
|
575
|
+
if (closed || !worker.child || worker.child.killed || worker.recycling || worker.recovery) return;
|
|
576
|
+
let daemonBusy = 1;
|
|
577
|
+
try { daemonBusy = Number(canRecycle()) === 1 ? 0 : 1; } catch {}
|
|
578
|
+
if (daemonBusy !== 0) return;
|
|
579
|
+
await refreshRuntimeWorkload();
|
|
580
|
+
const snapshot = workloadCache.worker;
|
|
581
|
+
const active = snapshot?.resources?.active || {};
|
|
582
|
+
const activeResources = Object.values(active)
|
|
583
|
+
.reduce((sum, value) => sum + Math.max(0, Number(value) || 0), 0);
|
|
584
|
+
const now = Date.now();
|
|
585
|
+
if (!shouldRecycleSessionRuntimeWorker({
|
|
586
|
+
rssBytes: snapshot?.memory?.rssBytes,
|
|
587
|
+
thresholdBytes: MEMORY_RECYCLE_THRESHOLD_BYTES,
|
|
588
|
+
daemonBusy,
|
|
589
|
+
pending: worker.pending.size,
|
|
590
|
+
activeResources,
|
|
591
|
+
now,
|
|
592
|
+
lastRecycleAt: lastMemoryRecycleAt,
|
|
593
|
+
cooldownMs: MEMORY_RECYCLE_COOLDOWN_MS,
|
|
594
|
+
})) return;
|
|
595
|
+
lastMemoryRecycleAt = now;
|
|
596
|
+
await worker.recycleForMemory(snapshot.memory.rssBytes);
|
|
597
|
+
workloadCache = { refreshedAt: Date.now(), worker: null };
|
|
598
|
+
}
|
|
599
|
+
const memoryRecycleTimer = MEMORY_RECYCLE_THRESHOLD_BYTES > 0
|
|
600
|
+
? setInterval(() => { void maybeRecycleWorkerMemory(); }, MEMORY_RECYCLE_INTERVAL_MS)
|
|
601
|
+
: null;
|
|
602
|
+
memoryRecycleTimer?.unref?.();
|
|
603
|
+
|
|
515
604
|
return {
|
|
516
605
|
async create(options = {}) {
|
|
517
606
|
if (closed) throw new Error('session runtime host is closed');
|
|
@@ -534,6 +623,7 @@ export function createSessionRuntimeHost({
|
|
|
534
623
|
async close(reason = 'session runtime host closed') {
|
|
535
624
|
if (closed) return;
|
|
536
625
|
closed = true;
|
|
626
|
+
if (memoryRecycleTimer) clearInterval(memoryRecycleTimer);
|
|
537
627
|
await worker.close(reason);
|
|
538
628
|
},
|
|
539
629
|
get status() {
|
|
@@ -541,6 +631,11 @@ export function createSessionRuntimeHost({
|
|
|
541
631
|
return {
|
|
542
632
|
active: Boolean(status.pid),
|
|
543
633
|
worker: status,
|
|
634
|
+
memoryRecycle: {
|
|
635
|
+
thresholdBytes: MEMORY_RECYCLE_THRESHOLD_BYTES,
|
|
636
|
+
cooldownMs: MEMORY_RECYCLE_COOLDOWN_MS,
|
|
637
|
+
lastRecycleAt: lastMemoryRecycleAt,
|
|
638
|
+
},
|
|
544
639
|
};
|
|
545
640
|
},
|
|
546
641
|
};
|
|
@@ -10,11 +10,7 @@ export async function disposeSessionRuntimeRecord(records, record, args = []) {
|
|
|
10
10
|
} finally {
|
|
11
11
|
try { record.unsubscribe?.(); } catch {}
|
|
12
12
|
record.unsubscribe = null;
|
|
13
|
-
record.
|
|
14
|
-
record.projected = null;
|
|
15
|
-
record.published = null;
|
|
16
|
-
record.fields?.clear?.();
|
|
17
|
-
record.items?.clear?.();
|
|
13
|
+
record.publishedSource = null;
|
|
18
14
|
records?.delete?.(record.id);
|
|
19
15
|
}
|
|
20
16
|
}
|
|
@@ -27,10 +27,19 @@ let unhealthyReported = false;
|
|
|
27
27
|
const pendingAbortPressureChecks = new WeakSet();
|
|
28
28
|
const ABORT_PRESSURE_RETENTION_CHECK_MS = 30_000;
|
|
29
29
|
|
|
30
|
+
// Reporting is bound to DETECTION, not to a successful runtime call: an idle
|
|
31
|
+
// worker (or one whose every call fails) would otherwise never be recycled.
|
|
32
|
+
function reportUnhealthy() {
|
|
33
|
+
if (stopping || unhealthyReported || !unhealthyDetail) return;
|
|
34
|
+
unhealthyReported = true;
|
|
35
|
+
send({ type: 'unhealthy', detail: unhealthyDetail });
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
process.on(SESSION_RUNTIME_WORKER_UNHEALTHY_EVENT, (detail) => {
|
|
31
39
|
if (stopping || unhealthyDetail) return;
|
|
32
40
|
unhealthyDetail = sanitizeForWire(detail) || { reason: 'session runtime worker unhealthy' };
|
|
33
41
|
process.stderr.write(`[session-runtime-health] ${unhealthyDetail.reason || 'unhealthy'}\n`);
|
|
42
|
+
reportUnhealthy();
|
|
34
43
|
});
|
|
35
44
|
|
|
36
45
|
process.on('warning', (warning) => {
|
|
@@ -74,57 +83,21 @@ function errorBody(error) {
|
|
|
74
83
|
};
|
|
75
84
|
}
|
|
76
85
|
|
|
77
|
-
function projectState(record, raw) {
|
|
78
|
-
if (!raw || typeof raw !== 'object') return sanitizeForWire(raw);
|
|
79
|
-
if (record.source === raw) return record.projected;
|
|
80
|
-
const fields = record.fields;
|
|
81
|
-
const items = record.items;
|
|
82
|
-
const nextItems = new Map();
|
|
83
|
-
const out = {};
|
|
84
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
85
|
-
if (key === 'items' && Array.isArray(value)) {
|
|
86
|
-
out.items = value.map((item) => {
|
|
87
|
-
const cached = items.get(item);
|
|
88
|
-
if (cached !== undefined) {
|
|
89
|
-
nextItems.set(item, cached);
|
|
90
|
-
return cached;
|
|
91
|
-
}
|
|
92
|
-
const cloned = sanitizeForWire(item);
|
|
93
|
-
nextItems.set(item, cloned);
|
|
94
|
-
return cloned;
|
|
95
|
-
});
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
const cached = fields.get(key);
|
|
99
|
-
if (cached && cached.source === value) {
|
|
100
|
-
out[key] = cached.value;
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
const cloned = sanitizeForWire(value);
|
|
104
|
-
if (cloned === undefined) continue;
|
|
105
|
-
fields.set(key, { source: value, value: cloned });
|
|
106
|
-
out[key] = cloned;
|
|
107
|
-
}
|
|
108
|
-
record.items = nextItems;
|
|
109
|
-
record.source = raw;
|
|
110
|
-
record.projected = out;
|
|
111
|
-
return out;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
86
|
function publish(record, forceFull = false) {
|
|
115
87
|
if (!record || record.disposed) return;
|
|
116
|
-
const
|
|
117
|
-
const previous = record.
|
|
118
|
-
if (!forceFull &&
|
|
119
|
-
|
|
88
|
+
const source = record.runtime.getState?.() || {};
|
|
89
|
+
const previous = record.publishedSource;
|
|
90
|
+
if (!forceFull && source === previous) return;
|
|
91
|
+
const body = forceFull || !previous
|
|
92
|
+
? { full: sanitizeForWire(source) }
|
|
93
|
+
: { patch: sanitizeForWire(diffSessionState(previous, source)) };
|
|
94
|
+
record.publishedSource = source;
|
|
120
95
|
record.revision += 1;
|
|
121
96
|
send({
|
|
122
97
|
type: 'state',
|
|
123
98
|
runtimeId: record.id,
|
|
124
99
|
revision: record.revision,
|
|
125
|
-
...
|
|
126
|
-
? { full: projected }
|
|
127
|
-
: { patch: diffSessionState(previous, projected) }),
|
|
100
|
+
...body,
|
|
128
101
|
});
|
|
129
102
|
}
|
|
130
103
|
|
|
@@ -136,11 +109,7 @@ async function createRuntime(message) {
|
|
|
136
109
|
runtime,
|
|
137
110
|
unsubscribe: null,
|
|
138
111
|
disposed: false,
|
|
139
|
-
|
|
140
|
-
projected: null,
|
|
141
|
-
published: null,
|
|
142
|
-
fields: new Map(),
|
|
143
|
-
items: new Map(),
|
|
112
|
+
publishedSource: null,
|
|
144
113
|
revision: 0,
|
|
145
114
|
};
|
|
146
115
|
records.set(record.id, record);
|
|
@@ -170,7 +139,6 @@ async function prewarm() {
|
|
|
170
139
|
() => module.preloadSessionRuntimeModule?.(),
|
|
171
140
|
() => module.preloadAgentLoopRuntime?.(),
|
|
172
141
|
() => module.preloadKeychainSecrets?.(),
|
|
173
|
-
() => module.preloadProviderRuntime?.(),
|
|
174
142
|
() => module.preloadMemoryRuntime?.(),
|
|
175
143
|
].map((start) => Promise.resolve().then(start)));
|
|
176
144
|
return { ready: true };
|
|
@@ -185,14 +153,42 @@ async function workloadSnapshot() {
|
|
|
185
153
|
import('../runtime/shared/tool-workload-gates.mjs'),
|
|
186
154
|
import('../runtime/shared/resource-admission.mjs'),
|
|
187
155
|
]);
|
|
156
|
+
const memory = process.memoryUsage();
|
|
188
157
|
return {
|
|
189
158
|
runtimes: records.size,
|
|
159
|
+
memory: {
|
|
160
|
+
rssBytes: memory.rss,
|
|
161
|
+
heapTotalBytes: memory.heapTotal,
|
|
162
|
+
heapUsedBytes: memory.heapUsed,
|
|
163
|
+
externalBytes: memory.external,
|
|
164
|
+
arrayBufferBytes: memory.arrayBuffers,
|
|
165
|
+
},
|
|
190
166
|
childSpawns: gate.snapshot(),
|
|
191
167
|
toolIo: workload.toolWorkloadSnapshot(),
|
|
192
168
|
resources: resources.resourceAdmission.snapshot(),
|
|
193
169
|
};
|
|
194
170
|
}
|
|
195
171
|
|
|
172
|
+
// Accepted user input lives in the pending-message spool until its write
|
|
173
|
+
// settles. Exiting on top of an in-flight write silently loses it, so every
|
|
174
|
+
// exit path drains the spool first (bounded; a stuck write must not wedge
|
|
175
|
+
// shutdown).
|
|
176
|
+
async function exitAfterPendingWrites(code = 0) {
|
|
177
|
+
try {
|
|
178
|
+
const pendingMessages = await import(
|
|
179
|
+
'../runtime/agent/orchestrator/session/manager/pending-messages.mjs'
|
|
180
|
+
);
|
|
181
|
+
await pendingMessages.settlePendingMessageWrites?.({ timeoutMs: 1_500 });
|
|
182
|
+
} catch (error) {
|
|
183
|
+
try {
|
|
184
|
+
process.stderr.write(
|
|
185
|
+
`[session-runtime-worker] pending message drain failed: ${error?.message || error}\n`,
|
|
186
|
+
);
|
|
187
|
+
} catch {}
|
|
188
|
+
}
|
|
189
|
+
process.exit(code);
|
|
190
|
+
}
|
|
191
|
+
|
|
196
192
|
async function stopAll(reason = 'session runtime worker shutdown') {
|
|
197
193
|
if (stopping) return;
|
|
198
194
|
stopping = true;
|
|
@@ -231,20 +227,19 @@ process.on('message', (message) => {
|
|
|
231
227
|
throw new Error(`unknown session runtime message ${message.type}`);
|
|
232
228
|
})().then((value) => {
|
|
233
229
|
if (requestId) send({ type: 'response', requestId, ok: true, value: value ?? null });
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
send({ type: 'unhealthy', detail: unhealthyDetail });
|
|
237
|
-
}
|
|
238
|
-
if (message.type === 'shutdown') setImmediate(() => process.exit(0));
|
|
230
|
+
reportUnhealthy();
|
|
231
|
+
if (message.type === 'shutdown') setImmediate(() => { void exitAfterPendingWrites(0); });
|
|
239
232
|
}).catch((error) => {
|
|
240
233
|
if (requestId) send({ type: 'response', requestId, ok: false, error: errorBody(error) });
|
|
234
|
+
reportUnhealthy();
|
|
241
235
|
});
|
|
242
236
|
});
|
|
243
237
|
|
|
244
238
|
process.on('disconnect', () => {
|
|
245
|
-
void stopAll('session runtime parent disconnected')
|
|
239
|
+
void stopAll('session runtime parent disconnected')
|
|
240
|
+
.finally(() => exitAfterPendingWrites(0));
|
|
246
241
|
});
|
|
247
242
|
|
|
248
243
|
process.on('SIGTERM', () => {
|
|
249
|
-
void stopAll('session runtime SIGTERM').finally(() =>
|
|
244
|
+
void stopAll('session runtime SIGTERM').finally(() => exitAfterPendingWrites(0));
|
|
250
245
|
});
|
|
@@ -318,7 +318,10 @@ export function createSessionService({
|
|
|
318
318
|
// so identity is a sound cache key. Without this the whole transcript was
|
|
319
319
|
// re-sanitized on every call AND every published frame.
|
|
320
320
|
if (raw && entry.snapshotSource === raw) return entry.snapshotCache;
|
|
321
|
-
|
|
321
|
+
// Runtime-worker IPC has already produced a wire-safe graph. Reusing it
|
|
322
|
+
// removes the daemon's second full transcript clone; in-process runtimes
|
|
323
|
+
// keep the sanitizer boundary below.
|
|
324
|
+
const cloned = entry.runtime?.isWireSafe === true ? raw : projectState(entry, raw);
|
|
322
325
|
entry.snapshotSource = raw;
|
|
323
326
|
entry.snapshotCache = cloned;
|
|
324
327
|
return cloned;
|