mixdog 0.9.95 → 0.9.97
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/LICENSES/Apache-2.0.txt +201 -0
- package/LICENSES/MIT.txt +56 -0
- package/LICENSES/codex-NOTICE.txt +6 -0
- package/NOTICE.md +46 -9
- package/README.md +0 -17
- package/package.json +16 -12
- package/scripts/.tmp-coverage-report.mjs +96 -0
- package/scripts/build-tui.mjs +17 -1
- package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
- package/scripts/fixtures/patch-replay-corpus.json +98 -0
- package/src/defaults/mixdog-config.template.json +5 -8
- package/src/defaults/skills/setup/SKILL.md +1 -1
- package/src/lib/keychain-cjs.cjs +1 -1
- package/src/lib/mixdog-debug.cjs +2 -3
- package/src/rules/agent/30-explorer.md +7 -4
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-tool.md +0 -3
- package/src/rules/shared/01-tool.md +21 -29
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
- package/src/runtime/agent/orchestrator/config.mjs +246 -67
- package/src/runtime/agent/orchestrator/mcp/client.mjs +104 -41
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +36 -10
- package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -33
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +21 -29
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +7 -10
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +45 -25
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +71 -42
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +4 -33
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
- package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +116 -26
- package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +12 -26
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -33
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +42 -45
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +27 -9
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +142 -26
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +47 -19
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -20
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +12 -11
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +63 -9
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
- package/src/runtime/channels/lib/config.mjs +34 -34
- package/src/runtime/channels/lib/inbound-handler.mjs +8 -8
- package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
- package/src/runtime/channels/lib/memory-client.mjs +31 -157
- package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
- package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
- package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
- package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
- package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
- package/src/runtime/channels/lib/scheduler.mjs +2 -2
- package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
- package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
- package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
- package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
- package/src/runtime/channels/lib/worker-main.mjs +38 -38
- package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
- package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +76 -248
- package/src/runtime/memory/lib/agent-ipc.mjs +146 -81
- package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +13 -34
- package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
- package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
- package/src/runtime/memory/lib/http-router.mjs +9 -6
- package/src/runtime/memory/lib/ko-morph.mjs +3 -20
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
- package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
- package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
- package/src/runtime/memory/lib/memory-log.mjs +1 -1
- package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/pg/process.mjs +32 -9
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/memory/tool-defs.mjs +1 -5
- package/src/runtime/search/index.mjs +2 -2
- package/src/runtime/search/tool-defs.mjs +2 -2
- package/src/runtime/shared/abort-race.mjs +70 -0
- package/src/runtime/shared/agent-route-config.mjs +119 -0
- package/src/runtime/shared/atomic-file.mjs +80 -4
- package/src/runtime/shared/channel-notification-routing.mjs +2 -2
- package/src/runtime/shared/child-guardian.mjs +204 -108
- package/src/runtime/shared/child-spawn-gate.mjs +139 -72
- package/src/runtime/shared/config.mjs +120 -47
- package/src/runtime/shared/llm/index.mjs +1 -1
- package/src/runtime/shared/owner-fair-gate.mjs +134 -0
- package/src/runtime/shared/resource-admission.mjs +150 -100
- package/src/runtime/shared/stream-progress.mjs +6 -0
- package/src/runtime/shared/tool-execution-owner.mjs +12 -0
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +91 -27
- package/src/runtime/shared/tool-workload-gates.mjs +29 -0
- package/src/runtime/shared/turn-snapshot.mjs +47 -6
- package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
- package/src/session-runtime/channel-config-api.mjs +5 -5
- package/src/session-runtime/config-helpers.mjs +39 -37
- package/src/session-runtime/config-lifecycle.mjs +27 -27
- package/src/session-runtime/lifecycle-api.mjs +22 -3
- package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
- package/src/session-runtime/model-route-api.mjs +7 -5
- package/src/session-runtime/prewarm.mjs +1 -1
- package/src/session-runtime/provider-auth-api.mjs +0 -7
- package/src/session-runtime/provider-models.mjs +1 -1
- package/src/session-runtime/remote-control.mjs +6 -7
- package/src/session-runtime/runtime-core.mjs +63 -39
- package/src/session-runtime/runtime-tunables.mjs +4 -4
- package/src/session-runtime/self-update.mjs +1 -1
- package/src/session-runtime/session-lifecycle.mjs +45 -17
- package/src/session-runtime/session-title.mjs +1 -1
- package/src/session-runtime/session-turn-api.mjs +96 -24
- package/src/session-runtime/settings-api.mjs +12 -24
- package/src/session-runtime/tool-defs.mjs +3 -3
- package/src/session-runtime/workflow-agents-api.mjs +55 -81
- package/src/session-runtime/workflow.mjs +14 -24
- package/src/standalone/agent-dispatch-broker.mjs +198 -0
- package/src/standalone/agent-tool/helpers.mjs +2 -2
- package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
- package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
- package/src/standalone/agent-tool/tool-def.mjs +0 -14
- package/src/standalone/channel-admin.mjs +53 -64
- package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +33 -19
- package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +127 -46
- package/src/standalone/channel-worker.mjs +124 -647
- package/src/standalone/{backend-daemon.mjs → daemon.mjs} +270 -154
- package/src/standalone/explore-tool.mjs +187 -70
- package/src/standalone/fair-call-scheduler.mjs +264 -0
- package/src/standalone/memory-runtime-proxy.mjs +58 -1
- package/src/standalone/session-client.mjs +1134 -0
- package/src/standalone/session-protocol.mjs +192 -0
- package/src/standalone/session-runtime-pool.mjs +393 -0
- package/src/standalone/session-runtime-record.mjs +20 -0
- package/src/standalone/session-runtime-worker.mjs +203 -0
- package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +481 -224
- package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +390 -121
- package/src/standalone/session-wire.mjs +55 -0
- package/src/tui/App.jsx +34 -21
- package/src/tui/app/app-format.mjs +1 -1
- package/src/tui/app/app-view.jsx +5 -2
- package/src/tui/app/channel-pickers.mjs +41 -41
- package/src/tui/app/core-memory-picker.mjs +19 -19
- package/src/tui/app/doctor.mjs +5 -11
- package/src/tui/app/input-parsers.mjs +2 -2
- package/src/tui/app/message-selector.mjs +103 -0
- package/src/tui/app/onboarding-steps.mjs +3 -4
- package/src/tui/app/project-picker.mjs +80 -56
- package/src/tui/app/prompt-submit.mjs +48 -43
- package/src/tui/app/resume-picker.mjs +2 -2
- package/src/tui/app/route-pickers.mjs +3 -1
- package/src/tui/app/settings-picker.mjs +31 -33
- package/src/tui/app/transcript-window.mjs +38 -0
- package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
- package/src/tui/app/use-prompt-handlers.mjs +86 -21
- package/src/tui/app/use-prompt-hint.mjs +3 -2
- package/src/tui/app/use-prompt-queue-history.mjs +10 -3
- package/src/tui/app/use-transcript-scroll.mjs +10 -3
- package/src/tui/app/use-transcript-window.mjs +50 -17
- package/src/tui/components/PromptInput.jsx +71 -38
- package/src/tui/components/Spinner.jsx +1 -1
- package/src/tui/components/ToolExecution.jsx +1 -1
- package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
- package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
- package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
- package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
- package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
- package/src/tui/dist/index.mjs +1031 -1676
- package/src/tui/hooks/useSession.mjs +16 -0
- package/src/tui/index.jsx +12 -13
- package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
- package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
- package/src/tui/{engine → session}/boot-profile.mjs +2 -2
- package/src/tui/{engine → session}/context-state.mjs +3 -3
- package/src/tui/{engine → session}/labels.mjs +2 -2
- package/src/tui/{engine → session}/live-share.mjs +38 -4
- package/src/tui/{engine → session}/notice-text.mjs +2 -2
- package/src/tui/{engine → session}/notification-plan.mjs +3 -3
- package/src/tui/session/oauth-flows.mjs +140 -0
- package/src/tui/{engine → session}/prompt-history.mjs +3 -3
- package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
- package/src/tui/{engine → session}/render-timing.mjs +2 -2
- package/src/tui/{engine → session}/session-api-ext.mjs +30 -20
- package/src/tui/{engine → session}/session-api.mjs +110 -93
- package/src/tui/{engine → session}/session-flow.mjs +8 -2
- package/src/tui/{engine → session}/session-stats.mjs +3 -3
- package/src/tui/{engine → session}/tool-approval.mjs +3 -3
- package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
- package/src/tui/{engine → session}/tool-card-results.mjs +8 -6
- package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
- package/src/tui/{engine → session}/tool-result-text.mjs +13 -3
- package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
- package/src/tui/{engine → session}/turn.mjs +38 -176
- package/src/tui/{engine-local-session.mjs → session-local.mjs} +103 -103
- package/src/tui/session.mjs +22 -0
- package/src/tui/spinner-meta.mjs +4 -4
- package/src/tui/spinner-verbs.mjs +51 -3
- package/src/ui/statusline-segments.mjs +1 -1
- package/src/ui/statusline.mjs +1 -1
- package/vendor/ink/build/components/App.js +10 -1
- package/src/session-runtime/memory-daemon-probe.mjs +0 -61
- package/src/standalone/engine-daemon-client.mjs +0 -894
- package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
- package/src/standalone/engine-daemon-protocol.mjs +0 -33
- package/src/tui/engine/turn-watchdog.mjs +0 -92
- package/src/tui/engine.mjs +0 -22
- package/src/tui/hooks/useEngine.mjs +0 -16
- /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
- /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
- /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
- /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
- /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const LEGACY_AGENT_ROUTE_SLOTS = Object.freeze({
|
|
2
|
+
explore: Object.freeze({ workflow: 'explorer', maintenance: 'explore' }),
|
|
3
|
+
maintainer: Object.freeze({ alias: 'maintenance', workflow: 'memory', maintenance: 'memory' }),
|
|
4
|
+
worker: Object.freeze({ workflow: 'agent' }),
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
const REDUNDANT_WORKFLOW_PRESET_IDS = new Set([
|
|
8
|
+
'workflow-agent',
|
|
9
|
+
'workflow-explorer',
|
|
10
|
+
'workflow-memory',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
function record(value) {
|
|
14
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isCompleteAgentRoute(value) {
|
|
18
|
+
return !!value
|
|
19
|
+
&& typeof value === 'object'
|
|
20
|
+
&& !Array.isArray(value)
|
|
21
|
+
&& !!String(value.provider || '').trim()
|
|
22
|
+
&& !!String(value.model || '').trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function generatedPresetAgentId(preset) {
|
|
26
|
+
const id = String(preset?.id || '').trim().toLowerCase();
|
|
27
|
+
if (id === 'workflow-agent') return 'worker';
|
|
28
|
+
if (id === 'workflow-explorer') return 'explore';
|
|
29
|
+
if (id === 'workflow-memory') return 'maintainer';
|
|
30
|
+
if (!id.startsWith('workflow-agent-')) return '';
|
|
31
|
+
const suffix = id.slice('workflow-agent-'.length);
|
|
32
|
+
if (suffix === 'explorer' || suffix === 'explore') return 'explore';
|
|
33
|
+
if (suffix === 'maintenance' || suffix === 'memory' || suffix === 'maintainer') return 'maintainer';
|
|
34
|
+
return suffix;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function routeFromGeneratedPreset(preset) {
|
|
38
|
+
const provider = String(preset?.provider || '').trim();
|
|
39
|
+
const model = String(preset?.model || '').trim();
|
|
40
|
+
if (!provider || !model) return null;
|
|
41
|
+
const route = { provider, model };
|
|
42
|
+
const effort = String(preset?.effort || '').trim();
|
|
43
|
+
if (effort) route.effort = effort;
|
|
44
|
+
if (preset?.fast === true) route.fast = true;
|
|
45
|
+
return route;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function configuredAgentRouteCandidates(config, agentId) {
|
|
49
|
+
const id = String(agentId || '').trim();
|
|
50
|
+
if (!id) return [];
|
|
51
|
+
const agents = record(config?.agents);
|
|
52
|
+
const legacy = LEGACY_AGENT_ROUTE_SLOTS[id] || null;
|
|
53
|
+
const candidates = [agents[id]];
|
|
54
|
+
if (legacy?.alias) candidates.push(agents[legacy.alias]);
|
|
55
|
+
if (legacy?.workflow) candidates.push(record(config?.workflowRoutes)[legacy.workflow]);
|
|
56
|
+
if (legacy?.maintenance) candidates.push(record(config?.maintenance)[legacy.maintenance]);
|
|
57
|
+
return candidates.filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function canonicalizeAgentRoutes(config = {}) {
|
|
61
|
+
const sourceAgents = record(config.agents);
|
|
62
|
+
const agents = Object.fromEntries(
|
|
63
|
+
Object.entries(sourceAgents).filter(([, route]) => isCompleteAgentRoute(route)),
|
|
64
|
+
);
|
|
65
|
+
for (const id of Object.keys(LEGACY_AGENT_ROUTE_SLOTS)) {
|
|
66
|
+
if (agents[id]) continue;
|
|
67
|
+
const candidate = configuredAgentRouteCandidates({ ...config, agents: sourceAgents }, id)
|
|
68
|
+
.find((route) => isCompleteAgentRoute(route));
|
|
69
|
+
if (candidate) agents[id] = candidate;
|
|
70
|
+
}
|
|
71
|
+
delete agents.maintenance;
|
|
72
|
+
// Older settings saves generated one preset per fixed/custom agent. Promote
|
|
73
|
+
// a preset only when no higher-priority canonical/legacy route exists, then
|
|
74
|
+
// the redundant preset can be removed without discarding user assignments.
|
|
75
|
+
for (const preset of Array.isArray(config?.presets) ? config.presets : []) {
|
|
76
|
+
const id = generatedPresetAgentId(preset);
|
|
77
|
+
if (!id || agents[id]) continue;
|
|
78
|
+
const route = routeFromGeneratedPreset(preset);
|
|
79
|
+
if (route) agents[id] = route;
|
|
80
|
+
}
|
|
81
|
+
return agents;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function isRedundantGeneratedRoutePreset(preset, defaultPreset = null) {
|
|
85
|
+
const id = String(preset?.id || '').trim();
|
|
86
|
+
if (!id || id === String(defaultPreset || '').trim()) return false;
|
|
87
|
+
return REDUNDANT_WORKFLOW_PRESET_IDS.has(id) || id.startsWith('workflow-agent-');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function canonicalizeAgentRouteStorage(config = {}) {
|
|
91
|
+
const {
|
|
92
|
+
workflowRoutes: _legacyWorkflowRoutes,
|
|
93
|
+
...rest
|
|
94
|
+
} = config || {};
|
|
95
|
+
const maintenance = { ...record(config?.maintenance) };
|
|
96
|
+
delete maintenance.explore;
|
|
97
|
+
delete maintenance.memory;
|
|
98
|
+
const presets = Array.isArray(config?.presets)
|
|
99
|
+
? config.presets.filter((preset) => !isRedundantGeneratedRoutePreset(preset, config?.default))
|
|
100
|
+
: [];
|
|
101
|
+
return {
|
|
102
|
+
...rest,
|
|
103
|
+
agents: canonicalizeAgentRoutes(config),
|
|
104
|
+
maintenance,
|
|
105
|
+
presets,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function agentRouteStorageNeedsMigration(config = {}) {
|
|
110
|
+
const agents = record(config?.agents);
|
|
111
|
+
const maintenance = record(config?.maintenance);
|
|
112
|
+
return Object.prototype.hasOwnProperty.call(config || {}, 'workflowRoutes')
|
|
113
|
+
|| Object.prototype.hasOwnProperty.call(agents, 'maintenance')
|
|
114
|
+
|| Object.prototype.hasOwnProperty.call(maintenance, 'explore')
|
|
115
|
+
|| Object.prototype.hasOwnProperty.call(maintenance, 'memory')
|
|
116
|
+
|| Object.values(agents).some((route) => !isCompleteAgentRoute(route))
|
|
117
|
+
|| (Array.isArray(config?.presets)
|
|
118
|
+
&& config.presets.some((preset) => isRedundantGeneratedRoutePreset(preset, config?.default)));
|
|
119
|
+
}
|
|
@@ -28,6 +28,55 @@ import { AsyncLocalStorage } from 'node:async_hooks';
|
|
|
28
28
|
|
|
29
29
|
const _execFileAsync = promisify(execFile);
|
|
30
30
|
|
|
31
|
+
// Async atomic writes from many daemon sessions share one disk. Keep enough
|
|
32
|
+
// parallelism for SSD throughput, then use additive-increase/multiplicative-
|
|
33
|
+
// decrease when latency shows the filesystem is saturated. This gate never
|
|
34
|
+
// touches synchronous compatibility paths and never revokes in-flight writes.
|
|
35
|
+
const _ioCeiling = Math.max(1, Math.floor(Number(process.env.MIXDOG_FILE_IO_MAX_CONCURRENCY) || 16));
|
|
36
|
+
let _ioLimit = Math.min(4, _ioCeiling);
|
|
37
|
+
let _ioActive = 0;
|
|
38
|
+
let _ioFastStreak = 0;
|
|
39
|
+
const _ioQueue = [];
|
|
40
|
+
|
|
41
|
+
function _drainAdaptiveFileIo() {
|
|
42
|
+
while (_ioActive < _ioLimit && _ioQueue.length > 0) {
|
|
43
|
+
const item = _ioQueue.shift();
|
|
44
|
+
_ioActive += 1;
|
|
45
|
+
const startedAt = Date.now();
|
|
46
|
+
Promise.resolve()
|
|
47
|
+
.then(item.run)
|
|
48
|
+
.then(item.resolve, item.reject)
|
|
49
|
+
.finally(() => {
|
|
50
|
+
const elapsed = Date.now() - startedAt;
|
|
51
|
+
_ioActive = Math.max(0, _ioActive - 1);
|
|
52
|
+
if (elapsed >= 250) {
|
|
53
|
+
_ioLimit = Math.max(1, Math.ceil(_ioLimit / 2));
|
|
54
|
+
_ioFastStreak = 0;
|
|
55
|
+
} else if (elapsed <= 40 && _ioLimit < _ioCeiling) {
|
|
56
|
+
_ioFastStreak += 1;
|
|
57
|
+
if (_ioFastStreak >= _ioLimit * 4) {
|
|
58
|
+
_ioLimit += 1;
|
|
59
|
+
_ioFastStreak = 0;
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
_ioFastStreak = 0;
|
|
63
|
+
}
|
|
64
|
+
_drainAdaptiveFileIo();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function _runAdaptiveFileIo(run) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
_ioQueue.push({ run, resolve, reject });
|
|
72
|
+
_drainAdaptiveFileIo();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function adaptiveFileIoSnapshot() {
|
|
77
|
+
return { active: _ioActive, queued: _ioQueue.length, limit: _ioLimit, ceiling: _ioCeiling };
|
|
78
|
+
}
|
|
79
|
+
|
|
31
80
|
const RETRY_CODES = new Set(['EPERM', 'EACCES', 'EBUSY', 'EEXIST']);
|
|
32
81
|
const LOCK_WAIT_CODES = new Set(['EEXIST', 'EPERM', 'EACCES', 'EBUSY']);
|
|
33
82
|
const DEFAULT_BACKOFFS_MS = Object.freeze([25, 50, 100, 200, 400, 800, 1200, 1600]);
|
|
@@ -79,6 +128,28 @@ function _describeLockHolder(lockPath) {
|
|
|
79
128
|
}
|
|
80
129
|
}
|
|
81
130
|
|
|
131
|
+
// Lock-wait watchdog. A cross-process file lock is invisible in a profile: the
|
|
132
|
+
// only symptom is "everything stopped". One bounded line per path names the
|
|
133
|
+
// path and the wait, so the next stall is diagnosed from the log instead of a
|
|
134
|
+
// live repro.
|
|
135
|
+
const LOCK_WAIT_WARN_MS = 500;
|
|
136
|
+
const LOCK_WAIT_WARN_INTERVAL_MS = 10_000;
|
|
137
|
+
const _lockWaitWarnedAt = new Map();
|
|
138
|
+
function _reportLockWait(lockPath, waitedMs, mode) {
|
|
139
|
+
if (waitedMs < LOCK_WAIT_WARN_MS) return;
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
if (now - (_lockWaitWarnedAt.get(lockPath) || 0) < LOCK_WAIT_WARN_INTERVAL_MS) return;
|
|
142
|
+
_lockWaitWarnedAt.set(lockPath, now);
|
|
143
|
+
if (_lockWaitWarnedAt.size > 64) {
|
|
144
|
+
for (const [path, at] of _lockWaitWarnedAt) {
|
|
145
|
+
if (now - at >= LOCK_WAIT_WARN_INTERVAL_MS * 6) _lockWaitWarnedAt.delete(path);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
process.stderr.write(`[atomic-file] ${mode} lock wait ${waitedMs}ms: ${lockPath}\n`);
|
|
150
|
+
} catch { /* diagnostics only */ }
|
|
151
|
+
}
|
|
152
|
+
|
|
82
153
|
export function renameWithRetrySync(src, dst, opts = {}) {
|
|
83
154
|
const backoffs = Array.isArray(opts.backoffs) && opts.backoffs.length > 0
|
|
84
155
|
? opts.backoffs
|
|
@@ -129,6 +200,7 @@ export function withFileLockSync(lockPath, fn, opts = {}) {
|
|
|
129
200
|
}
|
|
130
201
|
const deadline = Date.now() + timeoutMs;
|
|
131
202
|
mkdirSync(dirname(lockPath), { recursive: true });
|
|
203
|
+
const waitStartedAt = Date.now();
|
|
132
204
|
let attempt = 0;
|
|
133
205
|
let lastErr = null;
|
|
134
206
|
while (true) {
|
|
@@ -168,6 +240,7 @@ export function withFileLockSync(lockPath, fn, opts = {}) {
|
|
|
168
240
|
// `<pid> <ts> <token>` is a superset of the old `<pid> <ts>`; a
|
|
169
241
|
// tokenless (old-format) lock is still read as pid-authoritative.
|
|
170
242
|
try { writeFileSync(fd, `${process.pid} ${Date.now()} ${_OWNER_TOKEN}\n`, 'utf8'); } catch {}
|
|
243
|
+
_reportLockWait(lockPath, Date.now() - waitStartedAt, 'sync');
|
|
171
244
|
// For secret-bearing critical sections, the lock file sits beside
|
|
172
245
|
// the secret in the same (shared-home) directory; clamp it owner-only
|
|
173
246
|
// too. Fail-closed: an unenforceable ACL aborts before fn() runs.
|
|
@@ -362,6 +435,7 @@ async function _withOsFileLock(lockPath, fn, opts = {}) {
|
|
|
362
435
|
const staleMs = Number.isFinite(opts.staleMs) ? opts.staleMs : 30000;
|
|
363
436
|
const deadline = Date.now() + timeoutMs;
|
|
364
437
|
mkdirSync(dirname(lockPath), { recursive: true });
|
|
438
|
+
const waitStartedAt = Date.now();
|
|
365
439
|
let attempt = 0;
|
|
366
440
|
let lastErr = null;
|
|
367
441
|
while (true) {
|
|
@@ -393,6 +467,7 @@ async function _withOsFileLock(lockPath, fn, opts = {}) {
|
|
|
393
467
|
continue;
|
|
394
468
|
}
|
|
395
469
|
try { writeFileSync(fd, `${process.pid} ${Date.now()} ${_OWNER_TOKEN}\n`, 'utf8'); } catch {}
|
|
470
|
+
_reportLockWait(lockPath, Date.now() - waitStartedAt, 'async');
|
|
396
471
|
_osHeldPaths.add(lockPath);
|
|
397
472
|
try {
|
|
398
473
|
// Async ACL enforcement (promisified execFile) so a secret-bearing
|
|
@@ -738,10 +813,11 @@ export async function writeFileAtomicAsync(filePath, data, opts = {}) {
|
|
|
738
813
|
throw err;
|
|
739
814
|
}
|
|
740
815
|
};
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
816
|
+
return _runAdaptiveFileIo(() => (
|
|
817
|
+
opts.lock === true
|
|
818
|
+
? withFileLock(`${filePath}.lock`, run, opts)
|
|
819
|
+
: run()
|
|
820
|
+
));
|
|
745
821
|
}
|
|
746
822
|
|
|
747
823
|
export function writeJsonAtomicAsync(filePath, value, opts = {}) {
|
|
@@ -7,8 +7,8 @@ export function channelNotificationModelContent(params = {}) {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export function shouldMirrorChannelNotificationToPending(meta = {}) {
|
|
10
|
-
//
|
|
11
|
-
//
|
|
10
|
+
// Every inbound channel/schedule notification flows through the single
|
|
11
|
+
// queue — when no live
|
|
12
12
|
// listener handled it (engine not subscribed yet, pane disposed, headless),
|
|
13
13
|
// it is mirrored into the session pending queue for the next turn instead
|
|
14
14
|
// of being dropped. Silent-to-agent stays excluded; the upstream content
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
4
5
|
import { detachedSpawnOpts } from './spawn-flags.mjs';
|
|
5
6
|
|
|
6
7
|
function positiveInt(value) {
|
|
@@ -8,13 +9,6 @@ function positiveInt(value) {
|
|
|
8
9
|
return Number.isInteger(n) && n > 0 ? n : null;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
const DEFAULT_MIN_FREE_MEMORY_MB = 1024;
|
|
12
|
-
|
|
13
|
-
export function childGuardianMemoryFloorMb(env = process.env) {
|
|
14
|
-
const n = Math.floor(Number(env.MIXDOG_MIN_FREE_MEMORY_MB));
|
|
15
|
-
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_MIN_FREE_MEMORY_MB;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
12
|
export function childGuardianSpawnEnv(env = process.env) {
|
|
19
13
|
return {
|
|
20
14
|
PATH: env.PATH || '',
|
|
@@ -29,40 +23,25 @@ export function childGuardianSpawnEnv(env = process.env) {
|
|
|
29
23
|
};
|
|
30
24
|
}
|
|
31
25
|
|
|
32
|
-
function
|
|
33
|
-
parentPid,
|
|
34
|
-
childPid,
|
|
35
|
-
childGroupPid,
|
|
36
|
-
platform,
|
|
37
|
-
pollMs,
|
|
38
|
-
orphanGraceMs,
|
|
39
|
-
forceGraceMs,
|
|
40
|
-
minFreeMemoryMb,
|
|
41
|
-
receiptPath,
|
|
42
|
-
}) {
|
|
26
|
+
function guardianBrokerScript() {
|
|
43
27
|
return `
|
|
44
28
|
const { spawnSync } = require('node:child_process');
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const pollMs = ${JSON.stringify(pollMs)};
|
|
51
|
-
const orphanGraceMs = ${JSON.stringify(orphanGraceMs)};
|
|
52
|
-
const forceGraceMs = ${JSON.stringify(forceGraceMs)};
|
|
53
|
-
const minFreeMemoryBytes = ${JSON.stringify(minFreeMemoryMb * 1024 * 1024)};
|
|
54
|
-
const receiptPath = ${JSON.stringify(receiptPath || null)};
|
|
29
|
+
const platform = ${JSON.stringify(process.platform)};
|
|
30
|
+
const targets = new Map();
|
|
31
|
+
let input = '';
|
|
32
|
+
let everHadTarget = false;
|
|
33
|
+
let emptySince = 0;
|
|
55
34
|
// Kill receipt: guardian kills are otherwise indistinguishable from a crashed
|
|
56
35
|
// wrapper ("process exited without reporting an exit code"). Written BEFORE
|
|
57
36
|
// the kill so the owner's next status refresh can attribute the death.
|
|
58
|
-
function writeReceipt(reason, extra) {
|
|
59
|
-
if (!receiptPath) return;
|
|
37
|
+
function writeReceipt(target, reason, extra) {
|
|
38
|
+
if (!target.receiptPath) return;
|
|
60
39
|
try {
|
|
61
|
-
require('node:fs').writeFileSync(receiptPath, JSON.stringify({
|
|
40
|
+
require('node:fs').writeFileSync(target.receiptPath, JSON.stringify({
|
|
62
41
|
reason,
|
|
63
42
|
at: new Date().toISOString(),
|
|
64
43
|
guardianPid: process.pid,
|
|
65
|
-
childPid,
|
|
44
|
+
childPid: target.childPid,
|
|
66
45
|
...extra,
|
|
67
46
|
}));
|
|
68
47
|
} catch {}
|
|
@@ -72,57 +51,186 @@ function alive(pid) {
|
|
|
72
51
|
try { process.kill(pid, 0); return true; }
|
|
73
52
|
catch (error) { return error && error.code === 'EPERM'; }
|
|
74
53
|
}
|
|
75
|
-
function killTarget(force) {
|
|
54
|
+
function killTarget(target, force) {
|
|
76
55
|
if (platform === 'win32') {
|
|
77
56
|
const systemRoot = process.env.SystemRoot || process.env.WINDIR || 'C:\\\\Windows';
|
|
78
57
|
const taskkill = systemRoot + '\\\\System32\\\\taskkill.exe';
|
|
79
58
|
if (force) {
|
|
80
|
-
try { spawnSync(taskkill, ['/PID', String(childPid), '/T', '/F'], { stdio: 'ignore', windowsHide: true }); } catch {}
|
|
59
|
+
try { spawnSync(taskkill, ['/PID', String(target.childPid), '/T', '/F'], { stdio: 'ignore', windowsHide: true }); } catch {}
|
|
81
60
|
} else {
|
|
82
|
-
try { process.kill(childPid, 'SIGTERM'); } catch {}
|
|
83
|
-
try { spawnSync(taskkill, ['/PID', String(childPid), '/T'], { stdio: 'ignore', windowsHide: true }); } catch {}
|
|
61
|
+
try { process.kill(target.childPid, 'SIGTERM'); } catch {}
|
|
62
|
+
try { spawnSync(taskkill, ['/PID', String(target.childPid), '/T'], { stdio: 'ignore', windowsHide: true }); } catch {}
|
|
84
63
|
}
|
|
85
64
|
return;
|
|
86
65
|
}
|
|
87
66
|
const signal = force ? 'SIGKILL' : 'SIGTERM';
|
|
88
|
-
try { process.kill(-childGroupPid, signal); return; } catch {}
|
|
89
|
-
try { process.kill(childPid, signal); } catch {}
|
|
67
|
+
try { process.kill(-target.childGroupPid, signal); return; } catch {}
|
|
68
|
+
try { process.kill(target.childPid, signal); } catch {}
|
|
69
|
+
}
|
|
70
|
+
function accept(message) {
|
|
71
|
+
if (!message || typeof message !== 'object') return;
|
|
72
|
+
const id = String(message.id || '');
|
|
73
|
+
if (!id) return;
|
|
74
|
+
if (message.type === 'remove') {
|
|
75
|
+
targets.delete(id);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (message.type !== 'add') return;
|
|
79
|
+
const childPid = Number(message.childPid);
|
|
80
|
+
const parentPid = Number(message.parentPid);
|
|
81
|
+
if (!Number.isInteger(childPid) || childPid <= 0
|
|
82
|
+
|| !Number.isInteger(parentPid) || parentPid <= 0) return;
|
|
83
|
+
targets.set(id, {
|
|
84
|
+
id,
|
|
85
|
+
parentPid,
|
|
86
|
+
childPid,
|
|
87
|
+
childGroupPid: Number(message.childGroupPid) || childPid,
|
|
88
|
+
pollMs: Math.max(100, Number(message.pollMs) || 750),
|
|
89
|
+
orphanGraceMs: Math.max(100, Number(message.orphanGraceMs) || 3000),
|
|
90
|
+
forceGraceMs: Math.max(100, Number(message.forceGraceMs) || 3000),
|
|
91
|
+
receiptPath: typeof message.receiptPath === 'string' ? message.receiptPath : null,
|
|
92
|
+
orphanedAt: 0,
|
|
93
|
+
lastPolledAt: 0,
|
|
94
|
+
killing: false,
|
|
95
|
+
});
|
|
96
|
+
everHadTarget = true;
|
|
97
|
+
emptySince = 0;
|
|
90
98
|
}
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
process.stdin.setEncoding('utf8');
|
|
100
|
+
process.stdin.on('data', (chunk) => {
|
|
101
|
+
input += chunk;
|
|
102
|
+
let newline;
|
|
103
|
+
while ((newline = input.indexOf('\\n')) >= 0) {
|
|
104
|
+
const line = input.slice(0, newline);
|
|
105
|
+
input = input.slice(newline + 1);
|
|
106
|
+
try { accept(JSON.parse(line)); } catch {}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
93
109
|
const timer = setInterval(() => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
killTarget(true);
|
|
106
|
-
process.exit(0);
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
for (const [id, target] of targets) {
|
|
112
|
+
if (now - target.lastPolledAt < target.pollMs) continue;
|
|
113
|
+
target.lastPolledAt = now;
|
|
114
|
+
if (!alive(target.childPid)) {
|
|
115
|
+
targets.delete(id);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (alive(target.parentPid)) {
|
|
119
|
+
target.orphanedAt = 0;
|
|
120
|
+
continue;
|
|
107
121
|
}
|
|
122
|
+
if (!target.orphanedAt) target.orphanedAt = now;
|
|
123
|
+
if (target.killing || now - target.orphanedAt < target.orphanGraceMs) continue;
|
|
124
|
+
target.killing = true;
|
|
125
|
+
writeReceipt(target, 'parent-exit', {});
|
|
126
|
+
killTarget(target, false);
|
|
127
|
+
setTimeout(() => {
|
|
128
|
+
if (alive(target.childPid)) killTarget(target, true);
|
|
129
|
+
targets.delete(id);
|
|
130
|
+
}, target.forceGraceMs).unref?.();
|
|
108
131
|
}
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
132
|
+
// Pay for one Node supervisor only while it owns at least one live child.
|
|
133
|
+
// A short empty grace absorbs command-to-command reuse without keeping the
|
|
134
|
+
// broker resident for the whole daemon lifetime.
|
|
135
|
+
if (targets.size > 0) emptySince = 0;
|
|
136
|
+
else if (everHadTarget) {
|
|
137
|
+
if (!emptySince) emptySince = now;
|
|
138
|
+
else if (now - emptySince >= 1000) process.exit(0);
|
|
115
139
|
}
|
|
116
|
-
|
|
117
|
-
if (killing || Date.now() - orphanedAt < orphanGraceMs) return;
|
|
118
|
-
killing = true;
|
|
119
|
-
writeReceipt('parent-exit', {});
|
|
120
|
-
killTarget(false);
|
|
121
|
-
setTimeout(() => { if (alive(childPid)) killTarget(true); process.exit(0); }, forceGraceMs).unref?.();
|
|
122
|
-
}, pollMs);
|
|
140
|
+
}, 100);
|
|
123
141
|
`;
|
|
124
142
|
}
|
|
125
143
|
|
|
144
|
+
let sharedBroker = null;
|
|
145
|
+
let brokerRestartTimer = null;
|
|
146
|
+
let brokerTargetSweepTimer = null;
|
|
147
|
+
const brokerTargets = new Map();
|
|
148
|
+
|
|
149
|
+
function sendBrokerMessage(message) {
|
|
150
|
+
try {
|
|
151
|
+
if (!sharedBroker?.stdin?.writable) return false;
|
|
152
|
+
sharedBroker.stdin.write(`${JSON.stringify(message)}\n`);
|
|
153
|
+
return true;
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function targetPidAlive(pid) {
|
|
160
|
+
try {
|
|
161
|
+
process.kill(pid, 0);
|
|
162
|
+
return true;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return error?.code === 'EPERM';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function stopBrokerTargetSweepIfIdle() {
|
|
169
|
+
if (brokerTargets.size > 0 || !brokerTargetSweepTimer) return;
|
|
170
|
+
clearInterval(brokerTargetSweepTimer);
|
|
171
|
+
brokerTargetSweepTimer = null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function removeBrokerTarget(id, { notify = true } = {}) {
|
|
175
|
+
const target = brokerTargets.get(id);
|
|
176
|
+
if (!target) return false;
|
|
177
|
+
brokerTargets.delete(id);
|
|
178
|
+
try { target.onRemoved?.(); } catch {}
|
|
179
|
+
if (notify) sendBrokerMessage({ type: 'remove', id });
|
|
180
|
+
stopBrokerTargetSweepIfIdle();
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function ensureBrokerTargetSweep() {
|
|
185
|
+
if (brokerTargetSweepTimer) return;
|
|
186
|
+
brokerTargetSweepTimer = setInterval(() => {
|
|
187
|
+
for (const [id, target] of brokerTargets) {
|
|
188
|
+
if (!targetPidAlive(target.childPid)) removeBrokerTarget(id);
|
|
189
|
+
}
|
|
190
|
+
stopBrokerTargetSweepIfIdle();
|
|
191
|
+
}, 250);
|
|
192
|
+
brokerTargetSweepTimer.unref?.();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function ensureSharedBroker() {
|
|
196
|
+
if (sharedBroker && sharedBroker.exitCode == null && sharedBroker.signalCode == null) {
|
|
197
|
+
return sharedBroker;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const broker = spawn(process.execPath, [
|
|
201
|
+
'--no-warnings',
|
|
202
|
+
'--eval',
|
|
203
|
+
guardianBrokerScript(),
|
|
204
|
+
], {
|
|
205
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
206
|
+
env: childGuardianSpawnEnv(),
|
|
207
|
+
...detachedSpawnOpts,
|
|
208
|
+
});
|
|
209
|
+
sharedBroker = broker;
|
|
210
|
+
broker.unref?.();
|
|
211
|
+
broker.stdin?.unref?.();
|
|
212
|
+
broker.stdin?.on?.('error', () => {});
|
|
213
|
+
broker.once('exit', () => {
|
|
214
|
+
if (sharedBroker !== broker) return;
|
|
215
|
+
sharedBroker = null;
|
|
216
|
+
if (brokerTargets.size > 0 && !brokerRestartTimer) {
|
|
217
|
+
brokerRestartTimer = setTimeout(() => {
|
|
218
|
+
brokerRestartTimer = null;
|
|
219
|
+
const replacement = ensureSharedBroker();
|
|
220
|
+
if (!replacement) return;
|
|
221
|
+
for (const target of brokerTargets.values()) sendBrokerMessage(target);
|
|
222
|
+
}, 100);
|
|
223
|
+
brokerRestartTimer.unref?.();
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
for (const target of brokerTargets.values()) sendBrokerMessage(target);
|
|
227
|
+
return broker;
|
|
228
|
+
} catch {
|
|
229
|
+
sharedBroker = null;
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
126
234
|
export function startChildGuardian({
|
|
127
235
|
parentPid = process.pid,
|
|
128
236
|
childPid,
|
|
@@ -132,53 +240,41 @@ export function startChildGuardian({
|
|
|
132
240
|
graceMs = 3000,
|
|
133
241
|
orphanGraceMs = graceMs,
|
|
134
242
|
forceGraceMs = graceMs,
|
|
135
|
-
protectHostMemory = false,
|
|
136
|
-
minFreeMemoryMb = protectHostMemory ? childGuardianMemoryFloorMb() : 0,
|
|
137
243
|
receiptPath = null,
|
|
138
244
|
} = {}) {
|
|
139
245
|
const parent = positiveInt(parentPid);
|
|
140
246
|
const child = positiveInt(childPid);
|
|
141
247
|
if (!parent || !child || parent === child) return null;
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
env: childGuardianSpawnEnv(),
|
|
162
|
-
...detachedSpawnOpts,
|
|
163
|
-
});
|
|
164
|
-
guardian.unref?.();
|
|
165
|
-
let stopped = false;
|
|
166
|
-
return {
|
|
167
|
-
pid: guardian.pid || null,
|
|
168
|
-
label,
|
|
169
|
-
childPid: child,
|
|
170
|
-
stop() {
|
|
171
|
-
if (stopped) return false;
|
|
172
|
-
stopped = true;
|
|
173
|
-
try {
|
|
174
|
-
guardian.kill();
|
|
175
|
-
return true;
|
|
176
|
-
} catch {
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
},
|
|
180
|
-
};
|
|
181
|
-
} catch {
|
|
248
|
+
const id = randomUUID();
|
|
249
|
+
let stopped = false;
|
|
250
|
+
const target = {
|
|
251
|
+
type: 'add',
|
|
252
|
+
id,
|
|
253
|
+
parentPid: parent,
|
|
254
|
+
childPid: child,
|
|
255
|
+
childGroupPid: positiveInt(childGroupPid) || child,
|
|
256
|
+
pollMs: Math.max(100, Math.floor(Number(pollMs) || 750)),
|
|
257
|
+
orphanGraceMs: Math.max(100, Math.floor(Number(orphanGraceMs) || Number(graceMs) || 3000)),
|
|
258
|
+
forceGraceMs: Math.max(100, Math.floor(Number(forceGraceMs) || Number(graceMs) || 3000)),
|
|
259
|
+
receiptPath: typeof receiptPath === 'string' && receiptPath ? receiptPath : null,
|
|
260
|
+
onRemoved: () => { stopped = true; },
|
|
261
|
+
};
|
|
262
|
+
brokerTargets.set(id, target);
|
|
263
|
+
ensureBrokerTargetSweep();
|
|
264
|
+
const broker = ensureSharedBroker();
|
|
265
|
+
if (!broker) {
|
|
266
|
+
removeBrokerTarget(id, { notify: false });
|
|
182
267
|
return null;
|
|
183
268
|
}
|
|
269
|
+
sendBrokerMessage(target);
|
|
270
|
+
return {
|
|
271
|
+
pid: broker.pid || null,
|
|
272
|
+
label,
|
|
273
|
+
childPid: child,
|
|
274
|
+
stop() {
|
|
275
|
+
if (stopped) return false;
|
|
276
|
+
stopped = true;
|
|
277
|
+
return removeBrokerTarget(id);
|
|
278
|
+
},
|
|
279
|
+
};
|
|
184
280
|
}
|