mixdog 1.2.4 → 1.2.6
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 +1 -1
- package/scripts/lib/gate-scheduler.mjs +2 -2
- package/scripts/lib/run-node-tests.mjs +5 -14
- package/scripts/mobile-release.mjs +5 -4
- package/scripts/provider-pingpong-live.mjs +9 -9
- package/src/agents/advisor/AGENT.md +4 -4
- package/src/agents/advisor/agent.json +1 -1
- package/src/agents/reviewer/AGENT.md +2 -3
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +11 -0
- package/src/defaults/skills/browser-use/SKILL.md +2 -1
- package/src/defaults/skills/computer-use/SKILL.md +3 -1
- package/src/defaults/skills/visualize/SKILL.md +3 -2
- package/src/lib/rules-builder.cjs +1 -1
- package/src/output-styles/common.md +22 -5
- package/src/output-styles/detailed.md +7 -5
- package/src/output-styles/minimal.md +7 -3
- package/src/output-styles/simple.md +2 -3
- package/src/rules/agent/AGENT.md +3 -1
- package/src/rules/lead/LEAD.md +10 -7
- package/src/rules/shared/10-tool-workflow.md +2 -1
- package/src/rules/shared/60-verification.md +4 -5
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -1
- package/src/runtime/agent/orchestrator/config.mjs +6 -0
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-agents.mjs +10 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +53 -60
- package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
- package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +40 -19
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +3 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-start.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +1 -2
- package/src/runtime/agent/orchestrator/providers/catalog-deadline.mjs +163 -0
- package/src/runtime/agent/orchestrator/providers/catalog-failures.mjs +34 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +38 -14
- package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +100 -26
- package/src/runtime/agent/orchestrator/providers/lib/send-callbacks.mjs +24 -0
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +49 -4
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +0 -7
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +11 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +45 -16
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-connect.mjs +196 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +74 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +14 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-login.mjs +194 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-tokens.mjs +353 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +293 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +33 -100
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +23 -14
- package/src/runtime/agent/orchestrator/providers/registry.mjs +41 -27
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +2 -3
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +3 -8
- package/src/runtime/agent/orchestrator/session/agent-pool-progress.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +74 -13
- package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +102 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +8 -2
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +16 -0
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +39 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +9 -19
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +13 -5
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +10 -7
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +62 -22
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +23 -1
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +81 -34
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/request-fork.mjs +47 -0
- package/src/runtime/agent/orchestrator/session/side-question-store.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/side-question.mjs +5 -43
- package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +73 -6
- package/src/runtime/agent/orchestrator/session/store/load-session.mjs +40 -0
- package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/store/session-paths.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-header.mjs +36 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +255 -32
- package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +306 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +70 -21
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +46 -30
- package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +144 -45
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +32 -13
- package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +364 -38
- package/src/runtime/agent/orchestrator/session/store.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +43 -12
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +23 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -6
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +13 -7
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/benign-exit.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +40 -7
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +37 -34
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/inline-hoist.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +0 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +29 -27
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/find-files-tool.mjs +28 -11
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +203 -101
- package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +90 -41
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-delivery.mjs +199 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +165 -39
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +25 -13
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +7 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +19 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-line-collector.mjs +5 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +25 -7
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-render.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +12 -5
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +61 -22
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +32 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +193 -19
- package/src/runtime/agent/orchestrator/tools/builtin/shell-direct-exe.mjs +57 -7
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +119 -32
- package/src/runtime/agent/orchestrator/tools/builtin/shell-lossless-compact.mjs +26 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +13 -28
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +90 -11
- package/src/runtime/agent/orchestrator/tools/builtin/wait-budgets.mjs +23 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +54 -10
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +22 -28
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +11 -7
- package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +126 -9
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +15 -6
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +40 -2
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promotion-results.mjs +13 -3
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +35 -4
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +7 -2
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +39 -23
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
- package/src/runtime/attachments/office-extract.mjs +11 -9
- package/src/runtime/memory/lib/embedding-tokenizer.mjs +32 -18
- package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -1
- package/src/runtime/memory/lib/tool-call-handler.mjs +7 -5
- package/src/runtime/office/authoring/pptx-script-normalize.mjs +11 -8
- package/src/runtime/office/core/tabular.mjs +1 -1
- package/src/runtime/office/design/library/design-template-inspect.mjs +7 -4
- package/src/runtime/office/portable/docx-formatting.mjs +1 -1
- package/src/runtime/office/portable/docx-runs.mjs +1 -1
- package/src/runtime/office/portable/docx-tracked-edits.mjs +2 -2
- package/src/runtime/office/portable/portable-docx-edits.mjs +11 -10
- package/src/runtime/office/portable/portable-docx-operations.mjs +21 -17
- package/src/runtime/office/portable/portable-docx-parts.mjs +15 -9
- package/src/runtime/office/portable/portable-docx-styles.mjs +4 -1
- package/src/runtime/office/portable/portable-docx-xml.mjs +9 -9
- package/src/runtime/office/portable/portable-opc.mjs +27 -13
- package/src/runtime/office/portable/portable-pivot.mjs +3 -3
- package/src/runtime/office/portable/portable-pptx-chart.mjs +3 -3
- package/src/runtime/office/portable/portable-pptx-charts.mjs +24 -18
- package/src/runtime/office/portable/portable-pptx-core.mjs +5 -5
- package/src/runtime/office/portable/portable-pptx-deck.mjs +8 -4
- package/src/runtime/office/portable/portable-pptx-package.mjs +7 -4
- package/src/runtime/office/portable/portable-pptx-shapes.mjs +18 -9
- package/src/runtime/office/portable/portable-sheet-parts.mjs +9 -3
- package/src/runtime/office/portable/portable-sheet-xml.mjs +5 -5
- package/src/runtime/office/portable/portable-xlsx-charts.mjs +4 -1
- package/src/runtime/office/portable/portable-xlsx-drawings.mjs +10 -6
- package/src/runtime/office/portable/portable-xlsx-operations.mjs +8 -5
- package/src/runtime/office/portable/portable-xlsx-pivot-ops.mjs +1 -1
- package/src/runtime/office/portable/portable-xlsx-reference-shift.mjs +11 -7
- package/src/runtime/office/portable/portable-xlsx-sheet-edits.mjs +6 -3
- package/src/runtime/office/portable/portable-xml.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +22 -13
- package/src/runtime/shared/background-tasks.mjs +60 -20
- package/src/runtime/shared/child-spawn-gate.mjs +26 -16
- package/src/runtime/shared/config.mjs +0 -10
- package/src/runtime/shared/file-lock.mjs +7 -1
- package/src/runtime/shared/file-permissions-worker.mjs +38 -0
- package/src/runtime/shared/file-permissions.d.mts +19 -4
- package/src/runtime/shared/file-permissions.mjs +310 -4
- package/src/runtime/shared/git-runner-worker.mjs +6 -78
- package/src/runtime/shared/llm/http-agent.mjs +7 -3
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +10 -0
- package/src/runtime/shared/llm/usage-accounting.mjs +2 -0
- package/src/runtime/shared/llm/usage-ledger-import.mjs +32 -3
- package/src/runtime/shared/llm/usage-ledger-rollup.mjs +12 -5
- package/src/runtime/shared/llm/usage-ledger-worker.mjs +1 -1
- package/src/runtime/shared/llm/usage-ledger.mjs +20 -5
- package/src/runtime/shared/memory-snapshot.mjs +5 -1
- package/src/runtime/shared/notify-trace.mjs +8 -1
- package/src/runtime/shared/pristine-execution-contract.json +5 -1
- package/src/runtime/shared/process-runner-worker.mjs +7 -0
- package/src/runtime/shared/process-runner.mjs +38 -0
- package/src/runtime/shared/provider-accounts.mjs +2 -1
- package/src/runtime/shared/provider-api-key.mjs +0 -1
- package/src/runtime/shared/read-row-numbers.mjs +1 -1
- package/src/runtime/shared/resource-admission.mjs +60 -18
- package/src/runtime/shared/run-child.mjs +84 -0
- package/src/runtime/shared/session-activity-signal.mjs +23 -0
- package/src/runtime/shared/shell-output-summary.mjs +23 -0
- package/src/runtime/shared/singleton-owner.mjs +126 -42
- package/src/runtime/shared/tool-output-limit.mjs +4 -2
- package/src/runtime/shared/tool-output-marker.mjs +58 -0
- package/src/runtime/shared/user-data-guard.mjs +28 -9
- package/src/runtime/shared/worktrees/activity-gate.mjs +33 -8
- package/src/runtime/shared/worktrees/git-head.mjs +20 -2
- package/src/runtime/shared/worktrees/lock.mjs +3 -2
- package/src/runtime/terminal-bridge/tool-defs.mjs +1 -1
- package/src/runtime/tidy/runners/biome.mjs +3 -1
- package/src/runtime/tidy/shadow.mjs +2 -1
- package/src/runtime/web-search/index.mjs +5 -7
- package/src/runtime/web-search/lib/formatter.mjs +42 -6
- package/src/runtime/web-search/tool-defs.mjs +6 -1
- package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +4 -3
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -4
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +3 -3
- package/src/session-runtime/lifecycle/shared.mjs +19 -2
- package/src/session-runtime/lifecycle/teardown/session-close.mjs +2 -0
- package/src/session-runtime/lifecycle/teardown.mjs +5 -2
- package/src/session-runtime/lifecycle-api.mjs +3 -1
- package/src/session-runtime/provider-auth/login.mjs +10 -6
- package/src/session-runtime/provider-auth/setup.mjs +19 -2
- package/src/session-runtime/provider-catalog-cache.mjs +101 -25
- package/src/session-runtime/provider-models/catalog-load.mjs +14 -3
- package/src/session-runtime/provider-models/row-hydration.mjs +4 -0
- package/src/session-runtime/provider-models.mjs +76 -10
- package/src/session-runtime/provider-readiness.mjs +4 -0
- package/src/session-runtime/services/agent-tool/close-flow.mjs +2 -1
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +10 -2
- package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +13 -4
- package/src/session-runtime/services/agent-tool/notify.mjs +15 -4
- package/src/session-runtime/services/agent-tool/restart-notify.mjs +134 -0
- package/src/session-runtime/services/agent-tool/send-flow.mjs +24 -1
- package/src/session-runtime/services/agent-tool/spawn-flow/deferred-spawn.mjs +2 -1
- package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +5 -1
- package/src/session-runtime/services/agent-tool/tag-registry/tag-lookup.mjs +8 -3
- package/src/session-runtime/services/agent-tool/tag-registry.mjs +3 -0
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +4 -0
- package/src/session-runtime/services/agent-tool/worker-index.mjs +44 -10
- package/src/session-runtime/services/agent-tool.mjs +20 -0
- package/src/session-runtime/services/provider-admin.mjs +92 -20
- package/src/session-runtime/services/usage-dashboard-model.mjs +2 -2
- package/src/session-runtime/services/usage-dashboard.mjs +27 -69
- package/src/session-runtime/turn/session-ops.mjs +2 -2
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
- package/src/session-runtime/usage-stats-api.mjs +6 -1
- package/src/standalone/channel-transport/http-routes.mjs +10 -9
- package/src/standalone/daemon-desktop-runtime.mjs +20 -7
- package/src/standalone/daemon-owner.mjs +117 -25
- package/src/standalone/daemon.mjs +43 -13
- package/src/standalone/desktop-service-registry.mjs +32 -5
- package/src/standalone/session-call-cache.mjs +6 -0
- package/src/standalone/session-protocol.mjs +1 -1
- package/src/standalone/session-service/retention/busy.mjs +7 -2
- package/src/standalone/session-transport/routes.mjs +7 -6
- package/src/tui/app/model-options.mjs +39 -2
- package/src/tui/app/model-picker.mjs +16 -2
- package/src/tui/app/provider-setup-picker.mjs +1 -2
- package/src/tui/dist/index.mjs +436 -123
- package/src/tui/session/notice-text.mjs +0 -1
- package/src/tui/session/session-api/intake/submission.mjs +7 -13
- package/src/tui/session/session-api/integrations.mjs +1 -5
- package/src/tui/session/session-flow/drain.mjs +8 -0
- package/src/tui/session/session-flow/steering.mjs +46 -0
- package/src/tui/session/session-flow.mjs +2 -0
- package/src/tui/session/turn-stream.mjs +33 -0
- package/src/tui/session/turn.mjs +30 -3
- package/src/ui/statusline.mjs +1 -6
- package/src/ui/streaming-markdown-heal.mjs +94 -25
- package/src/vendor/statusline/bin/statusline-route.mjs +0 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +0 -1
- package/src/workflows/default/WORKFLOW.md +9 -13
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Opt-in (MIXDOG_NOTIFY_TRACE=1) completion-notification trace: off by
|
|
2
|
+
// default, so a normal install writes no line per delivery decision (it kept
|
|
3
|
+
// a multi-MB log growing in every data dir). The display path for
|
|
2
4
|
// background completions is event-time only and has repeatedly failed silently
|
|
3
5
|
// in the field (2026-08-17: bench shell output never rendered while the model
|
|
4
6
|
// queue twin worked every time). One append-only line per decision point keeps
|
|
@@ -36,7 +38,12 @@ function tracePath() {
|
|
|
36
38
|
return join(traceDir, 'notify-trace.log');
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
function notifyTraceEnabled() {
|
|
42
|
+
return process.env.MIXDOG_NOTIFY_TRACE === '1';
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
export function notifyTrace(stage, fields = {}) {
|
|
46
|
+
if (!notifyTraceEnabled()) return;
|
|
40
47
|
try {
|
|
41
48
|
const parts = [`[${new Date().toISOString()}]`, `pid=${process.pid}`, stage];
|
|
42
49
|
for (const [key, value] of Object.entries(fields)) {
|
|
@@ -88,6 +88,11 @@
|
|
|
88
88
|
"credentialFile": "antigravity-oauth.json",
|
|
89
89
|
"credentialPathEnv": "ANTIGRAVITY_OAUTH_CREDENTIALS_PATH",
|
|
90
90
|
"modelCatalogFile": "antigravity-oauth-models.json"
|
|
91
|
+
},
|
|
92
|
+
"opencode-go-oauth": {
|
|
93
|
+
"credentialFile": "opencode-go-oauth.json",
|
|
94
|
+
"credentialPathEnv": "OPENCODE_GO_OAUTH_CREDENTIALS_PATH",
|
|
95
|
+
"modelCatalogFile": "opencode-go-oauth-models.json"
|
|
91
96
|
}
|
|
92
97
|
},
|
|
93
98
|
"apiKeyProviders": {
|
|
@@ -96,7 +101,6 @@
|
|
|
96
101
|
"gemini": "GEMINI_API_KEY",
|
|
97
102
|
"deepseek": "DEEPSEEK_API_KEY",
|
|
98
103
|
"xai": "XAI_API_KEY",
|
|
99
|
-
"opencode-go": "OPENCODE_API_KEY",
|
|
100
104
|
"openrouter": "OPENROUTER_API_KEY"
|
|
101
105
|
},
|
|
102
106
|
"personalStateCounters": [
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Worker-thread body for process-runner.mjs: helper children (process-table
|
|
2
|
+
// enumeration, tree kills) are spawned on this thread, never on the host
|
|
3
|
+
// event loop (run-child.mjs).
|
|
4
|
+
import { runChild } from './run-child.mjs';
|
|
5
|
+
import { serveWorkerRequests } from './worker-requests.mjs';
|
|
6
|
+
|
|
7
|
+
serveWorkerRequests(runChild);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Run short helper processes from one shared worker thread, so their
|
|
2
|
+
// synchronous native process creation never stalls the host event loop. A
|
|
3
|
+
// daemon profile under heavy agent load measured single PowerShell spawns
|
|
4
|
+
// blocking the daemon's loop for up to 7 s each. Separate from the git
|
|
5
|
+
// runner's worker: a slow helper start never queues a git child behind it.
|
|
6
|
+
import { SHARE_ENV } from 'node:worker_threads';
|
|
7
|
+
import { runChild } from './run-child.mjs';
|
|
8
|
+
import { createWorkerRequestClient } from './worker-requests.mjs';
|
|
9
|
+
|
|
10
|
+
// Created on first use. A bundled copy of this module (the desktop service's
|
|
11
|
+
// CommonJS bundle) has no module URL and no worker file beside it: there the
|
|
12
|
+
// child is started on the calling thread, exactly as before.
|
|
13
|
+
let requestProcess = null;
|
|
14
|
+
function processClient() {
|
|
15
|
+
if (requestProcess !== null) return requestProcess;
|
|
16
|
+
try {
|
|
17
|
+
// SHARE_ENV: children inherit the live process environment, exactly as a
|
|
18
|
+
// spawn from this thread would.
|
|
19
|
+
requestProcess = createWorkerRequestClient(new URL('./process-runner-worker.mjs', import.meta.url), {
|
|
20
|
+
env: SHARE_ENV,
|
|
21
|
+
});
|
|
22
|
+
} catch {
|
|
23
|
+
requestProcess = false;
|
|
24
|
+
}
|
|
25
|
+
return requestProcess;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolves to { code, stdout, stderr } (a non-zero exit is the caller's to
|
|
30
|
+
* judge). Rejects with code ETIMEDOUT after timeoutMs (the child is killed;
|
|
31
|
+
* 0 sets no deadline),
|
|
32
|
+
* EMAXBUFFER once stdout+stderr exceed maxBytes, or the spawn error itself.
|
|
33
|
+
*/
|
|
34
|
+
export function runProcessOffThread(command, args, { cwd, input = '', env = null, timeoutMs, maxBytes }) {
|
|
35
|
+
const request = processClient();
|
|
36
|
+
const payload = { command, args, cwd, input, env, timeoutMs, maxBytes };
|
|
37
|
+
return request ? request(payload) : runChild(payload);
|
|
38
|
+
}
|
|
@@ -13,6 +13,7 @@ export const ACCOUNT_PROVIDERS = Object.freeze([
|
|
|
13
13
|
'mimo-oauth',
|
|
14
14
|
'cursor-oauth',
|
|
15
15
|
'antigravity-oauth',
|
|
16
|
+
'opencode-go-oauth',
|
|
16
17
|
]);
|
|
17
18
|
const MAX_ACCOUNTS = 20;
|
|
18
19
|
// `dataDir` defaults to this process's data dir; an isolated runtime passes
|
|
@@ -79,7 +80,7 @@ function normalizeLabels(pool) {
|
|
|
79
80
|
// only; `limit` keeps the selected account until it reaches a limit and then
|
|
80
81
|
// moves to the account with the most headroom; `headroom` starts every new
|
|
81
82
|
// session on the account with the most headroom and keeps it for that session.
|
|
82
|
-
|
|
83
|
+
const ACCOUNT_POLICIES = Object.freeze(['manual', 'limit', 'headroom']);
|
|
83
84
|
|
|
84
85
|
// `auto` predates `policy` and is still written beside it, so a build that
|
|
85
86
|
// knows only `auto` reads the same switching on/off. Whenever the two
|
|
@@ -88,7 +88,7 @@ export function readSourceWindow(text) {
|
|
|
88
88
|
rows.push({ number, content: row[2] });
|
|
89
89
|
continue;
|
|
90
90
|
}
|
|
91
|
-
const metadata = /^\[lines (\d+)-(\d+) of (\d+)(?:;
|
|
91
|
+
const metadata = /^\[lines (\d+)-(\d+) of (\d+)(?:; offset:\d+ for more)?\]$/.exec(line);
|
|
92
92
|
if (!metadata || footer) return null;
|
|
93
93
|
footer = metadata;
|
|
94
94
|
totalLines = Number(metadata[3]);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { freemem, totalmem } from 'node:os';
|
|
1
|
+
import { availableParallelism, freemem, totalmem } from 'node:os';
|
|
2
2
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
3
|
import { requestMemoryPressureSnapshot } from './memory-snapshot.mjs';
|
|
4
4
|
import { nonNegativeInt, positiveInt } from './numbers.mjs';
|
|
@@ -6,12 +6,20 @@ import { nonNegativeInt, positiveInt } from './numbers.mjs';
|
|
|
6
6
|
const MB = 1024 * 1024;
|
|
7
7
|
|
|
8
8
|
// The unified daemon queues bursts instead of starting an unbounded number of
|
|
9
|
-
// agent/provider
|
|
10
|
-
//
|
|
9
|
+
// agent/provider tasks in one process. Shell commands start immediately: their
|
|
10
|
+
// only bound is maxShells (64) live foreground commands, a runaway guard
|
|
11
|
+
// rather than a CPU budget; the OS shares the cores. Environment overrides remain available; memory thresholds
|
|
12
|
+
// are diagnostic only.
|
|
13
|
+
// Agent leases are held only while an agent runs tools (they yield during
|
|
14
|
+
// provider I/O) and their tool work is mostly child-process I/O, so two per
|
|
15
|
+
// core keeps the host busy: 30-agent stress on 20 cores halved wall time at
|
|
16
|
+
// 40 versus 8 with no event-loop or memory cost. 8 is the floor for small
|
|
17
|
+
// hosts. maxHighLoad now bounds agents only and defaults to the same.
|
|
18
|
+
const AGENT_LIMIT = Math.max(8, 2 * availableParallelism());
|
|
11
19
|
const RESOURCE_ADMISSION_DEFAULTS = Object.freeze({
|
|
12
|
-
maxAgents:
|
|
13
|
-
maxShells:
|
|
14
|
-
maxHighLoad:
|
|
20
|
+
maxAgents: AGENT_LIMIT,
|
|
21
|
+
maxShells: 64,
|
|
22
|
+
maxHighLoad: AGENT_LIMIT,
|
|
15
23
|
maxQueue: 1024,
|
|
16
24
|
minFreeMemoryMb: 0,
|
|
17
25
|
maxRssMb: 0,
|
|
@@ -79,12 +87,15 @@ class ResourceAdmissionQueueFullError extends ResourcePressureError {
|
|
|
79
87
|
* shells. Running work is never revoked: pressure only rejects a new request or
|
|
80
88
|
* keeps a concurrency-blocked request in the bounded FIFO queue.
|
|
81
89
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
90
|
+
* Shell leases are their own pool of maxShells: they are not part of the
|
|
91
|
+
* maxHighLoad budget, do not suspend the calling agent's lease, and never wait
|
|
92
|
+
* behind queued agent work, so a command starts at once unless maxShells
|
|
93
|
+
* commands are already running. A shell lease covers only the foreground part
|
|
94
|
+
* of a command: promotion to a background job releases it
|
|
95
|
+
* (lib/shell-run-background/promote-job.mjs), so background jobs are not
|
|
96
|
+
* capped at all: they are mostly idle servers/watchers at below-normal OS
|
|
97
|
+
* priority whose memory lives in their own processes. Session/runtime close
|
|
98
|
+
* stops them (shell-jobs shutdownShellJobs).
|
|
88
99
|
*/
|
|
89
100
|
export class ResourceAdmissionController {
|
|
90
101
|
constructor({ limits = {}, metrics = defaultMetrics, now = Date.now, env = process.env } = {}) {
|
|
@@ -98,6 +109,20 @@ export class ResourceAdmissionController {
|
|
|
98
109
|
// Live leases for saturation diagnostics only (labels/ages in snapshot()).
|
|
99
110
|
this.activeLeases = new Set();
|
|
100
111
|
this.context = new AsyncLocalStorage();
|
|
112
|
+
// Admission waits by pool: first acquisition (agent/shell) and the
|
|
113
|
+
// reacquisition after a yielded provider call (restore). Shows whether
|
|
114
|
+
// the agent cap actually delays work.
|
|
115
|
+
this.waitStats = {};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_recordWait(pool, waitedMs) {
|
|
119
|
+
this.waitStats[pool] ??= { admitted: 0, waited: 0, totalWaitMs: 0, maxWaitMs: 0 };
|
|
120
|
+
const stats = this.waitStats[pool];
|
|
121
|
+
stats.admitted += 1;
|
|
122
|
+
if (waitedMs <= 0) return;
|
|
123
|
+
stats.waited += 1;
|
|
124
|
+
stats.totalWaitMs += waitedMs;
|
|
125
|
+
stats.maxWaitMs = Math.max(stats.maxWaitMs, waitedMs);
|
|
101
126
|
}
|
|
102
127
|
|
|
103
128
|
_recordMemoryPressure(kind) {
|
|
@@ -129,9 +154,14 @@ export class ResourceAdmissionController {
|
|
|
129
154
|
}
|
|
130
155
|
|
|
131
156
|
_canStart(kind) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
157
|
+
if (kind === 'shell') return this.active.shell < this.limits.maxShells;
|
|
158
|
+
return this.active.agent < this.limits.maxHighLoad && this.active.agent < this.limits.maxAgents;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Shell and agent waiters never delay each other's new requests.
|
|
162
|
+
_queuedInPool(lane) {
|
|
163
|
+
const shell = lane === 'shell';
|
|
164
|
+
return this.queue.some((item) => (item.kind === 'shell') === shell);
|
|
135
165
|
}
|
|
136
166
|
|
|
137
167
|
_adjustOwnerCount(kind, ownerKey, delta) {
|
|
@@ -240,6 +270,7 @@ export class ResourceAdmissionController {
|
|
|
240
270
|
},
|
|
241
271
|
};
|
|
242
272
|
this.activeLeases.add(lease);
|
|
273
|
+
this._recordWait(kind, lease.queuedMs);
|
|
243
274
|
return lease;
|
|
244
275
|
}
|
|
245
276
|
|
|
@@ -294,8 +325,9 @@ export class ResourceAdmissionController {
|
|
|
294
325
|
this._recordMemoryPressure(lane);
|
|
295
326
|
const ambientParent = this.context.getStore();
|
|
296
327
|
const detachedDependency = dependency === 'detached' && ambientParent?.controller === this;
|
|
297
|
-
|
|
298
|
-
|
|
328
|
+
// A shell runs beside its agent instead of taking the agent's slot.
|
|
329
|
+
const parent = detachedDependency || lane === 'shell' ? null : this._suspendParent(ambientParent);
|
|
330
|
+
if (detachedDependency && (!this._canStart(lane) || this._queuedInPool(lane))) {
|
|
299
331
|
const error = new ResourcePressureError(
|
|
300
332
|
`detached nested ${lane} work has no admission capacity; retry after running work completes`,
|
|
301
333
|
{ kind: lane }
|
|
@@ -303,7 +335,7 @@ export class ResourceAdmissionController {
|
|
|
303
335
|
error.code = 'ERESOURCEDEPENDENCY';
|
|
304
336
|
return Promise.reject(error);
|
|
305
337
|
}
|
|
306
|
-
if (this._canStart(lane) && this.
|
|
338
|
+
if (this._canStart(lane) && !this._queuedInPool(lane)) {
|
|
307
339
|
const lease = this._lease(lane, null, parent, owner, taskPriority);
|
|
308
340
|
lease.label = label;
|
|
309
341
|
lease.signal = signal;
|
|
@@ -439,6 +471,7 @@ export class ResourceAdmissionController {
|
|
|
439
471
|
if (item.restore) {
|
|
440
472
|
item.parent.restorePending = null;
|
|
441
473
|
item.parent.counted = true;
|
|
474
|
+
this._recordWait(`${item.kind}Restore`, Math.max(0, this.now() - item.queuedAt));
|
|
442
475
|
this.active[item.kind] += 1;
|
|
443
476
|
this._adjustOwnerCount(item.kind, item.ownerKey, 1);
|
|
444
477
|
item.resolve();
|
|
@@ -461,6 +494,15 @@ export class ResourceAdmissionController {
|
|
|
461
494
|
shell: this.activeByOwner.shell.size,
|
|
462
495
|
},
|
|
463
496
|
queued: this.queue.length,
|
|
497
|
+
waitStats: Object.fromEntries(
|
|
498
|
+
Object.entries(this.waitStats).map(([pool, stats]) => [
|
|
499
|
+
pool,
|
|
500
|
+
{
|
|
501
|
+
...stats,
|
|
502
|
+
averageQueuedMs: stats.waited > 0 ? Math.round(stats.totalWaitMs / stats.waited) : 0,
|
|
503
|
+
},
|
|
504
|
+
])
|
|
505
|
+
),
|
|
464
506
|
limits: {
|
|
465
507
|
...this.limits,
|
|
466
508
|
maxAgents: wireLimit(this.limits.maxAgents),
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// One child process run to completion, its output collected — the body of
|
|
2
|
+
// the off-thread runners (git-runner-worker.mjs, process-runner-worker.mjs).
|
|
3
|
+
// Starting a child process is a synchronous native call (CreateProcess on
|
|
4
|
+
// Windows, amplified by on-access scanning and seconds long under load), so
|
|
5
|
+
// those workers spawn here instead of on the host event loop.
|
|
6
|
+
// Resolves to { code, stdout, stderr }; rejects with code ETIMEDOUT after
|
|
7
|
+
// timeoutMs (the child is killed), EMAXBUFFER once stdout+stderr exceed
|
|
8
|
+
// maxBytes, or the spawn error itself.
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
|
|
11
|
+
export function runChild({ command, args, cwd, input, env, timeoutMs, maxBytes, stdoutEncoding = 'utf8' }) {
|
|
12
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
13
|
+
const hasInput = (typeof input === 'string' || ArrayBuffer.isView(input)) && input.length > 0;
|
|
14
|
+
const child = spawn(command, args, {
|
|
15
|
+
cwd,
|
|
16
|
+
...(env ? { env } : {}),
|
|
17
|
+
windowsHide: true,
|
|
18
|
+
stdio: [hasInput ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
19
|
+
});
|
|
20
|
+
const stdout = [];
|
|
21
|
+
const stderr = [];
|
|
22
|
+
let outputBytes = 0;
|
|
23
|
+
let stdinError = null;
|
|
24
|
+
let settled = false;
|
|
25
|
+
const finish = (error, result) => {
|
|
26
|
+
if (settled) return;
|
|
27
|
+
settled = true;
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
if (error) rejectPromise(error);
|
|
30
|
+
else resolvePromise(result);
|
|
31
|
+
};
|
|
32
|
+
// timeoutMs 0: no deadline (the caller's own work bounds the child).
|
|
33
|
+
const timer =
|
|
34
|
+
timeoutMs > 0
|
|
35
|
+
? setTimeout(() => {
|
|
36
|
+
const error = new Error(`${command} ${args.join(' ')} timed out after ${timeoutMs}ms`);
|
|
37
|
+
error.code = 'ETIMEDOUT';
|
|
38
|
+
try {
|
|
39
|
+
child.kill();
|
|
40
|
+
} catch {}
|
|
41
|
+
finish(error);
|
|
42
|
+
}, timeoutMs)
|
|
43
|
+
: null;
|
|
44
|
+
const collect = (chunks, chunk) => {
|
|
45
|
+
const next = Buffer.from(chunk);
|
|
46
|
+
outputBytes += next.length;
|
|
47
|
+
if (outputBytes > maxBytes) {
|
|
48
|
+
const error = new Error(`${command} ${args.join(' ')} exceeded ${maxBytes} output bytes`);
|
|
49
|
+
error.code = 'EMAXBUFFER';
|
|
50
|
+
try {
|
|
51
|
+
child.kill();
|
|
52
|
+
} catch {}
|
|
53
|
+
finish(error);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
chunks.push(next);
|
|
57
|
+
};
|
|
58
|
+
child.stdout.on('data', (chunk) => collect(stdout, chunk));
|
|
59
|
+
child.stderr.on('data', (chunk) => collect(stderr, chunk));
|
|
60
|
+
child.once('error', (error) => finish(error));
|
|
61
|
+
child.once('close', (code) => {
|
|
62
|
+
const result = {
|
|
63
|
+
code: Number(code ?? 1),
|
|
64
|
+
// 'base64' keeps non-UTF-8 bytes (a replayable patch) intact across the thread hop.
|
|
65
|
+
stdout: Buffer.concat(stdout).toString(stdoutEncoding === 'base64' ? 'base64' : 'utf8'),
|
|
66
|
+
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
67
|
+
};
|
|
68
|
+
// A failed stdin write only matters when the child otherwise reported success.
|
|
69
|
+
if (stdinError && result.code === 0) {
|
|
70
|
+
finish(stdinError);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
finish(null, result);
|
|
74
|
+
});
|
|
75
|
+
if (child.stdin) {
|
|
76
|
+
// A short-lived child can close before stdin is flushed. Keep that
|
|
77
|
+
// transport race inside this promise instead of an unhandled EPIPE.
|
|
78
|
+
child.stdin.on('error', (error) => {
|
|
79
|
+
stdinError = error;
|
|
80
|
+
});
|
|
81
|
+
child.stdin.end(input);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// session-activity-signal.mjs
|
|
2
|
+
// Process-local "a session's activity state may have changed" pulse: a turn
|
|
3
|
+
// started or settled, a task wait began or ended, a background task was
|
|
4
|
+
// handed off or finished. Status surfaces that project activity from the
|
|
5
|
+
// runtime map (the Agents pool) listen and re-read at once instead of waiting
|
|
6
|
+
// for their next store event. Import-free leaf.
|
|
7
|
+
const listeners = new Set();
|
|
8
|
+
|
|
9
|
+
export function onSessionActivityChange(listener) {
|
|
10
|
+
if (typeof listener !== 'function') return () => {};
|
|
11
|
+
listeners.add(listener);
|
|
12
|
+
return () => listeners.delete(listener);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function emitSessionActivityChange(sessionId = '') {
|
|
16
|
+
for (const listener of [...listeners]) {
|
|
17
|
+
try {
|
|
18
|
+
listener(String(sessionId || ''));
|
|
19
|
+
} catch {
|
|
20
|
+
/* a listener's failure never affects the transition */
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Formatting shared by every surface that hands shell output to an agent.
|
|
2
|
+
// Program output is untrusted text: anything shaped like a harness/system tag
|
|
3
|
+
// is escaped so it can never forge a notice, a result boundary or a reminder.
|
|
4
|
+
// (Capping lives in tools/builtin/lib/shell-delivery.mjs, per-job cursors in
|
|
5
|
+
// tools/builtin/lib/shell-task-output.mjs.)
|
|
6
|
+
|
|
7
|
+
// A tag-looking token: `<name`, `</name`, where the name is a known
|
|
8
|
+
// harness/system/transcript word or any hyphenated/underscored/namespaced
|
|
9
|
+
// name (task-notification, system-reminder, tool_result, ns:tag ...). Plain
|
|
10
|
+
// HTML-ish words like <div> or a comparison `a<b` stay readable.
|
|
11
|
+
const HARNESS_TAG =
|
|
12
|
+
/<(\/?)(?=(?:[a-z][\w]*[-_:][\w:-]*|system|result|results|summary|status|error|user|assistant|human|tool|function|functions|instructions|context|notification|reminder|output|invoke|parameter|thinking)\b)/gi;
|
|
13
|
+
|
|
14
|
+
export function escapeHarnessTags(text) {
|
|
15
|
+
return String(text ?? '').replace(HARNESS_TAG, '<$1');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatDuration(ms) {
|
|
19
|
+
if (!(ms >= 0)) return '';
|
|
20
|
+
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
|
|
21
|
+
const minutes = Math.floor(ms / 60_000);
|
|
22
|
+
return `${minutes}m ${Math.round((ms % 60_000) / 1000)}s`;
|
|
23
|
+
}
|
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
import { mkdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { mkdirSync, renameSync, rmdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { readFile, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
|
|
2
3
|
import { dirname } from 'node:path';
|
|
4
|
+
import { withFileLock, withFileLockSync } from './file-lock.mjs';
|
|
3
5
|
import { readJsonSafe } from './json-file.mjs';
|
|
4
6
|
import { isPidAlive } from './pid-liveness.mjs';
|
|
5
|
-
|
|
7
|
+
|
|
8
|
+
// Claim-lock protocol shared by the sync and async paths: `<path>.claim.lock`
|
|
9
|
+
// is a file-lock.mjs lock, published atomically with its holder's pid and
|
|
10
|
+
// token. A holder whose pid is alive is waited out however long it holds the
|
|
11
|
+
// lock — a holder stalled mid-refresh still owns it: reclaiming it on age let
|
|
12
|
+
// a successor claim the record and the stalled refresh then overwrite it. Its
|
|
13
|
+
// lock is reclaimed only once that pid is dead, and only the holder's own
|
|
14
|
+
// acquisition (pid + token) is ever released. A lock naming no holder — the
|
|
15
|
+
// bare directory versions before this one took the lock with — is reclaimed
|
|
16
|
+
// once older than CLAIM_LOCK_STALE_MS, as those versions did.
|
|
17
|
+
const CLAIM_LOCK_TIMEOUT_MS = 500;
|
|
18
|
+
const CLAIM_LOCK_STALE_MS = 5000;
|
|
19
|
+
const CLAIM_LOCK_OPTS = Object.freeze({ timeoutMs: CLAIM_LOCK_TIMEOUT_MS, staleMs: CLAIM_LOCK_STALE_MS });
|
|
20
|
+
const BUSY = Symbol('claim lock busy');
|
|
6
21
|
|
|
7
22
|
function writeJsonAtomic(path, value) {
|
|
8
23
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -11,28 +26,82 @@ function writeJsonAtomic(path, value) {
|
|
|
11
26
|
renameSync(tmp, path);
|
|
12
27
|
}
|
|
13
28
|
|
|
14
|
-
|
|
29
|
+
// Held by another live process or this one's async holder ('busy'). Any other
|
|
30
|
+
// failure (an unwritable data directory) is thrown, as before.
|
|
31
|
+
function claimLockHeld(error) {
|
|
32
|
+
return error?.code === 'ELOCKCONTENDED' || (error?.code === 'ELOCKTIMEOUT' && error.cause?.code === 'EEXIST');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function legacyLockStale(info) {
|
|
36
|
+
return info.isDirectory() && Date.now() - info.mtimeMs > CLAIM_LOCK_STALE_MS;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// An older version's stale bare lock directory is removed with rmdir alone,
|
|
40
|
+
// never recursive or forced: between the stat and the removal a contender may
|
|
41
|
+
// have replaced it with a live owner-named lock file. rmdir refuses a file
|
|
42
|
+
// (ENOENT on Windows, ENOTDIR elsewhere) and a non-empty directory
|
|
43
|
+
// (ENOTEMPTY); a missing path (ENOENT) is already gone. Any refusal (EPERM
|
|
44
|
+
// included) leaves the path untouched to the shared acquire loop below, which
|
|
45
|
+
// waits a live holder out and reclaims only a dead one's lock.
|
|
46
|
+
|
|
47
|
+
// `fn()` under the claim lock, or BUSY. file-lock creates a missing data
|
|
48
|
+
// directory (a first boot, e.g. a daemon an ssh bootstrap starts on a fresh
|
|
49
|
+
// server).
|
|
50
|
+
function withClaimLockSync(path, fn) {
|
|
15
51
|
const lockPath = `${path}.claim.lock`;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
52
|
+
try {
|
|
53
|
+
if (legacyLockStale(statSync(lockPath))) rmdirSync(lockPath);
|
|
54
|
+
} catch {}
|
|
55
|
+
let entered = false;
|
|
56
|
+
try {
|
|
57
|
+
return withFileLockSync(
|
|
58
|
+
lockPath,
|
|
59
|
+
() => {
|
|
60
|
+
entered = true;
|
|
61
|
+
return fn();
|
|
62
|
+
},
|
|
63
|
+
CLAIM_LOCK_OPTS
|
|
64
|
+
);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (!entered && claimLockHeld(error)) return BUSY;
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The same lock taken without blocking the event loop: every file operation
|
|
72
|
+
// and every retry wait is awaited (a synchronous refresh once held the
|
|
73
|
+
// daemon's loop for 20 s in one rmSync under disk contention).
|
|
74
|
+
async function withClaimLock(path, fn) {
|
|
75
|
+
const lockPath = `${path}.claim.lock`;
|
|
76
|
+
try {
|
|
77
|
+
if (legacyLockStale(await stat(lockPath))) await rmdir(lockPath);
|
|
78
|
+
} catch {}
|
|
79
|
+
let entered = false;
|
|
80
|
+
try {
|
|
81
|
+
return await withFileLock(
|
|
82
|
+
lockPath,
|
|
83
|
+
() => {
|
|
84
|
+
entered = true;
|
|
85
|
+
return fn();
|
|
86
|
+
},
|
|
87
|
+
CLAIM_LOCK_OPTS
|
|
88
|
+
);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (!entered && claimLockHeld(error)) return BUSY;
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function namesOtherPid(record, pid) {
|
|
96
|
+
return Boolean(record?.pid) && Number(record.pid) !== Number(pid);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function readJsonSafeAsync(path) {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
34
104
|
}
|
|
35
|
-
return null;
|
|
36
105
|
}
|
|
37
106
|
|
|
38
107
|
// A live pid alone does not prove the record's process still owns it: the OS
|
|
@@ -64,18 +133,15 @@ export function claimSingletonOwner(path, { kind = 'runtime', pid = process.pid,
|
|
|
64
133
|
return { owned: false, owner: current };
|
|
65
134
|
}
|
|
66
135
|
|
|
67
|
-
const
|
|
68
|
-
if (!lockPath) return { owned: false, owner: readJsonSafe(path) };
|
|
69
|
-
try {
|
|
136
|
+
const result = withClaimLockSync(path, () => {
|
|
70
137
|
const lockedCurrent = readJsonSafe(path);
|
|
71
138
|
if (livePeer(lockedCurrent)) {
|
|
72
139
|
return { owned: false, owner: lockedCurrent };
|
|
73
140
|
}
|
|
74
141
|
writeJsonAtomic(path, owner);
|
|
75
142
|
return { owned: true, owner };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
}
|
|
143
|
+
});
|
|
144
|
+
return result === BUSY ? { owned: false, owner: readJsonSafe(path) } : result;
|
|
79
145
|
}
|
|
80
146
|
|
|
81
147
|
export function releaseSingletonOwner(path, pid = process.pid) {
|
|
@@ -94,16 +160,37 @@ export function releaseSingletonOwner(path, pid = process.pid) {
|
|
|
94
160
|
// naming another pid is reported, never overwritten ('superseded'); a claim
|
|
95
161
|
// lock held past its wait leaves the record alone ('busy').
|
|
96
162
|
export function refreshSingletonOwner(path, owner) {
|
|
97
|
-
const
|
|
98
|
-
if (!lockPath) return { status: 'busy', owner: null };
|
|
99
|
-
try {
|
|
163
|
+
const result = withClaimLockSync(path, () => {
|
|
100
164
|
const current = readJsonSafe(path);
|
|
101
|
-
if (
|
|
165
|
+
if (namesOtherPid(current, owner.pid)) return { status: 'superseded', owner: current };
|
|
102
166
|
writeJsonAtomic(path, owner);
|
|
103
167
|
return { status: 'refreshed', owner };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
168
|
+
});
|
|
169
|
+
return result === BUSY ? { status: 'busy', owner: null } : result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** refreshSingletonOwner without blocking the event loop: the same claim
|
|
173
|
+
* lock (mutually exclusive with the sync claim/refresh/handoff), the same
|
|
174
|
+
* verdicts, every file operation and retry wait awaited. Its awaited write
|
|
175
|
+
* may stall, so the record is read again right before it is replaced: a
|
|
176
|
+
* record another pid wrote meanwhile is reported, never overwritten. */
|
|
177
|
+
export async function refreshSingletonOwnerAsync(path, owner) {
|
|
178
|
+
const result = await withClaimLock(path, async () => {
|
|
179
|
+
const current = await readJsonSafeAsync(path);
|
|
180
|
+
if (namesOtherPid(current, owner.pid)) return { status: 'superseded', owner: current };
|
|
181
|
+
let tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
182
|
+
try {
|
|
183
|
+
await writeFile(tmp, `${JSON.stringify(owner, null, 2)}\n`);
|
|
184
|
+
const latest = await readJsonSafeAsync(path);
|
|
185
|
+
if (namesOtherPid(latest, owner.pid)) return { status: 'superseded', owner: latest };
|
|
186
|
+
await rename(tmp, path);
|
|
187
|
+
tmp = null;
|
|
188
|
+
return { status: 'refreshed', owner };
|
|
189
|
+
} finally {
|
|
190
|
+
if (tmp) await rm(tmp, { force: true }).catch(() => {});
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
return result === BUSY ? { status: 'busy', owner: null } : result;
|
|
107
194
|
}
|
|
108
195
|
|
|
109
196
|
// Atomic ownership handoff: replace the owner record from fromPid to toOwner
|
|
@@ -113,11 +200,9 @@ export function refreshSingletonOwner(path, owner) {
|
|
|
113
200
|
// (someone else won), the handoff is refused (owned:false) and the caller
|
|
114
201
|
// should fall back to the winner rather than overwrite it.
|
|
115
202
|
export function handoffSingletonOwner(path, fromPid, toOwner = {}) {
|
|
116
|
-
const
|
|
117
|
-
if (!lockPath) return { owned: false, owner: readJsonSafe(path) };
|
|
118
|
-
try {
|
|
203
|
+
const result = withClaimLockSync(path, () => {
|
|
119
204
|
const current = readJsonSafe(path);
|
|
120
|
-
if (
|
|
205
|
+
if (namesOtherPid(current, fromPid)) {
|
|
121
206
|
return { owned: false, owner: current };
|
|
122
207
|
}
|
|
123
208
|
const owner = {
|
|
@@ -127,7 +212,6 @@ export function handoffSingletonOwner(path, fromPid, toOwner = {}) {
|
|
|
127
212
|
};
|
|
128
213
|
writeJsonAtomic(path, owner);
|
|
129
214
|
return { owned: true, owner };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
}
|
|
215
|
+
});
|
|
216
|
+
return result === BUSY ? { owned: false, owner: readJsonSafe(path) } : result;
|
|
133
217
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// context cost of any one tool call is bounded and consistent. Override with
|
|
4
4
|
// MIXDOG_TOOL_OUTPUT_MAX_BYTES. Line-based sub-caps (e.g. bash max lines) stay
|
|
5
5
|
// per-tool; this governs only the byte budget.
|
|
6
|
+
import { capMarker } from './tool-output-marker.mjs';
|
|
7
|
+
export { capMarker, formatCapSize, windowMarker } from './tool-output-marker.mjs';
|
|
6
8
|
// A positive integer from an env var, or `fallback` when unset or invalid.
|
|
7
9
|
export function positiveEnvInt(name, fallback) {
|
|
8
10
|
const v = parseInt(process.env[name], 10);
|
|
@@ -53,7 +55,7 @@ export function capLineOrientedToolOutput(result, maxBytes, footerForPrefix) {
|
|
|
53
55
|
while (lo <= hi) {
|
|
54
56
|
const mid = Math.floor((lo + hi) / 2);
|
|
55
57
|
const kept = lines.slice(0, mid);
|
|
56
|
-
const footer = String(footerForPrefix?.(kept, lines) ||
|
|
58
|
+
const footer = String(footerForPrefix?.(kept, lines) || capMarker({ limit: cap }));
|
|
57
59
|
const candidate = `${kept.join('\n')}${kept.length ? '\n' : ''}${footer}`;
|
|
58
60
|
if (Buffer.byteLength(candidate, 'utf8') <= cap) {
|
|
59
61
|
best = candidate;
|
|
@@ -63,5 +65,5 @@ export function capLineOrientedToolOutput(result, maxBytes, footerForPrefix) {
|
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
if (best) return best;
|
|
66
|
-
return _prefixByBytes(String(footerForPrefix?.([], lines) ||
|
|
68
|
+
return _prefixByBytes(String(footerForPrefix?.([], lines) || capMarker({ limit: cap })), cap);
|
|
67
69
|
}
|