mixdog 0.8.1 → 0.9.1
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/README.md +47 -23
- package/package.json +34 -28
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +205 -33
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1222 -90
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -29
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +242 -98
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2211 -446
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +108 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
- package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +149 -51
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +0 -1
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
- package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -6
- package/src/runtime/channels/lib/tool-format.mjs +0 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +519 -118
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
- package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
- package/src/runtime/memory/lib/session-ingest.mjs +196 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +104 -473
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +44 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +139 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -95
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +1 -78
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4800 -2140
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +14 -4
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +428 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +597 -362
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +499 -0
- package/src/tui/components/tool-output-format.test.mjs +312 -0
- package/src/tui/dist/index.mjs +9192 -2399
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +671 -69
- package/src/tui/markdown/format-token.test.mjs +312 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +756 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +2 -13
- package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/ink.js +54 -8
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -88,10 +88,6 @@ export function loadUsageState() {
|
|
|
88
88
|
return state
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
export function saveUsageState(state) {
|
|
92
|
-
scheduleUsageFlush(state)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
91
|
export function updateProviderState(state, provider, patch) {
|
|
96
92
|
let normalizedPatch = { ...patch }
|
|
97
93
|
const remaining =
|
|
@@ -125,7 +121,7 @@ export function noteProviderSuccess(state, provider, extra = {}) {
|
|
|
125
121
|
})
|
|
126
122
|
}
|
|
127
123
|
|
|
128
|
-
|
|
124
|
+
const PROVIDER_ERROR_KIND = {
|
|
129
125
|
AUTH: 'auth',
|
|
130
126
|
QUOTA: 'quota',
|
|
131
127
|
PAYMENT: 'payment',
|
|
@@ -151,16 +147,6 @@ export function classifyProviderError(error) {
|
|
|
151
147
|
return PROVIDER_ERROR_KIND.UNKNOWN
|
|
152
148
|
}
|
|
153
149
|
|
|
154
|
-
/** Structured HTTP error for search backends (enables cooldown via classifyProviderError). */
|
|
155
|
-
export function providerHttpError(provider, status, detail = '') {
|
|
156
|
-
const code = Number(status)
|
|
157
|
-
const snippet = detail ? `: ${String(detail).slice(0, 200)}` : ''
|
|
158
|
-
const err = new Error(`[search:${provider}] HTTP ${code}${snippet}`)
|
|
159
|
-
err.status = code
|
|
160
|
-
err.provider = provider
|
|
161
|
-
return err
|
|
162
|
-
}
|
|
163
|
-
|
|
164
150
|
const PROVIDER_DISABLE_TTL_MS = {
|
|
165
151
|
auth: 24 * 3600 * 1000,
|
|
166
152
|
quota: 24 * 3600 * 1000,
|
|
@@ -3,12 +3,26 @@ import dns from 'dns'
|
|
|
3
3
|
import net from 'net'
|
|
4
4
|
import { Agent, fetch as undiciFetch } from 'undici'
|
|
5
5
|
|
|
6
|
-
import { JSDOM } from 'jsdom'
|
|
7
|
-
import puppeteer from 'puppeteer-core'
|
|
8
6
|
import { Readability } from '@mozilla/readability'
|
|
9
7
|
import { isWSL } from '../../shared/wsl.mjs'
|
|
10
8
|
import { startChildGuardian } from '../../shared/child-guardian.mjs'
|
|
11
9
|
|
|
10
|
+
// Lazy heavy deps: importing jsdom (~400ms) and puppeteer-core (~130ms) at
|
|
11
|
+
// module load added ~540ms to the first web search even when the request never
|
|
12
|
+
// scraped HTML. Load them on first actual use and cache the resolved binding so
|
|
13
|
+
// repeat calls pay nothing. The search runtime itself is already dynamically
|
|
14
|
+
// imported, so this keeps that first-use cost proportional to what the request
|
|
15
|
+
// truly needs (a plain fetch path touches neither).
|
|
16
|
+
let _JSDOM = null
|
|
17
|
+
async function loadJSDOM() {
|
|
18
|
+
if (!_JSDOM) ({ JSDOM: _JSDOM } = await import('jsdom'))
|
|
19
|
+
return _JSDOM
|
|
20
|
+
}
|
|
21
|
+
let _puppeteer = null
|
|
22
|
+
async function loadPuppeteer() {
|
|
23
|
+
if (!_puppeteer) _puppeteer = (await import('puppeteer-core')).default
|
|
24
|
+
return _puppeteer
|
|
25
|
+
}
|
|
12
26
|
|
|
13
27
|
const PKG_VERSION = (() => { try { return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version } catch { return '0.0.1' } })()
|
|
14
28
|
import {
|
|
@@ -491,7 +505,8 @@ function classifyJavascriptRenderingPlaceholder(page) {
|
|
|
491
505
|
return null
|
|
492
506
|
}
|
|
493
507
|
|
|
494
|
-
function extractReadableArticle(url, html) {
|
|
508
|
+
async function extractReadableArticle(url, html) {
|
|
509
|
+
const JSDOM = await loadJSDOM()
|
|
495
510
|
const dom = new JSDOM(html, { url })
|
|
496
511
|
try {
|
|
497
512
|
const doc = dom.window.document
|
|
@@ -701,11 +716,11 @@ async function fetchPinnedForPausedRequest(url, { signal, method = 'GET', header
|
|
|
701
716
|
currentUrl = new URL(location, currentUrl).toString()
|
|
702
717
|
continue
|
|
703
718
|
}
|
|
704
|
-
const
|
|
719
|
+
const respBody = await readBodyBytesWithCap(response, MAX_BODY_BYTES)
|
|
705
720
|
return {
|
|
706
721
|
status: response.status,
|
|
707
722
|
responseHeaders: headersToCdpPairs(response.headers),
|
|
708
|
-
body,
|
|
723
|
+
body: respBody,
|
|
709
724
|
}
|
|
710
725
|
}
|
|
711
726
|
}
|
|
@@ -812,7 +827,7 @@ async function scrapeWithReadability(url, timeoutMs, signal) {
|
|
|
812
827
|
currentUrl = target
|
|
813
828
|
html = await fetchHtml(currentUrl, timeoutMs, signal)
|
|
814
829
|
}
|
|
815
|
-
return extractReadableArticle(currentUrl, html)
|
|
830
|
+
return await extractReadableArticle(currentUrl, html)
|
|
816
831
|
}
|
|
817
832
|
|
|
818
833
|
function resolveBrowserLaunchOptions() {
|
|
@@ -888,23 +903,26 @@ async function _getPoolBrowser() {
|
|
|
888
903
|
}
|
|
889
904
|
if (_poolBrowser) return _poolBrowser
|
|
890
905
|
if (!_poolLaunching) {
|
|
891
|
-
_poolLaunching =
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
906
|
+
_poolLaunching = loadPuppeteer()
|
|
907
|
+
.then((puppeteer) => puppeteer.launch({
|
|
908
|
+
headless: true,
|
|
909
|
+
...resolveBrowserLaunchOptions(),
|
|
910
|
+
args: buildPuppeteerLaunchArgs(),
|
|
911
|
+
}))
|
|
912
|
+
.then((browser) => {
|
|
913
|
+
_poolBrowser = browser
|
|
914
|
+
try {
|
|
915
|
+
const proc = browser.process?.()
|
|
916
|
+
startChildGuardian({ childPid: proc?.pid, label: 'puppeteer-browser' })
|
|
917
|
+
} catch {}
|
|
918
|
+
browser.on('disconnected', () => {
|
|
919
|
+
if (_poolBrowser === browser) _poolBrowser = null
|
|
920
|
+
})
|
|
921
|
+
return browser
|
|
922
|
+
})
|
|
923
|
+
.finally(() => {
|
|
924
|
+
_poolLaunching = null
|
|
903
925
|
})
|
|
904
|
-
return browser
|
|
905
|
-
}).finally(() => {
|
|
906
|
-
_poolLaunching = null
|
|
907
|
-
})
|
|
908
926
|
}
|
|
909
927
|
return _poolLaunching
|
|
910
928
|
}
|
|
@@ -1033,7 +1051,7 @@ async function scrapeWithPuppeteer(url, timeoutMs, signal) {
|
|
|
1033
1051
|
}
|
|
1034
1052
|
try {
|
|
1035
1053
|
return {
|
|
1036
|
-
...extractReadableArticle(finalUrl, html),
|
|
1054
|
+
...(await extractReadableArticle(finalUrl, html)),
|
|
1037
1055
|
extractor: 'puppeteer',
|
|
1038
1056
|
}
|
|
1039
1057
|
} catch {
|
|
@@ -1088,7 +1106,8 @@ function filterLinks(rawLinks, baseUrl, { limit = 50, sameDomainOnly = true, sea
|
|
|
1088
1106
|
return items
|
|
1089
1107
|
}
|
|
1090
1108
|
|
|
1091
|
-
function extractLinksFromHtml(baseUrl, html, options) {
|
|
1109
|
+
async function extractLinksFromHtml(baseUrl, html, options) {
|
|
1110
|
+
const JSDOM = await loadJSDOM()
|
|
1092
1111
|
const dom = new JSDOM(html, { url: baseUrl })
|
|
1093
1112
|
try {
|
|
1094
1113
|
const links = Array.from(dom.window.document.querySelectorAll('a[href]')).map(link => ({
|
|
@@ -1103,7 +1122,7 @@ function extractLinksFromHtml(baseUrl, html, options) {
|
|
|
1103
1122
|
|
|
1104
1123
|
async function mapWithHttp(url, options, timeoutMs, signal) {
|
|
1105
1124
|
const html = await fetchHtml(url, timeoutMs, signal)
|
|
1106
|
-
return extractLinksFromHtml(url, html, options)
|
|
1125
|
+
return await extractLinksFromHtml(url, html, options)
|
|
1107
1126
|
}
|
|
1108
1127
|
|
|
1109
1128
|
async function mapWithPuppeteer(url, options, timeoutMs, signal) {
|
|
@@ -1,44 +1,37 @@
|
|
|
1
1
|
import {
|
|
2
|
-
TOOL_ASYNC_EXECUTION_CONTRACT,
|
|
3
2
|
TOOL_SYNC_EXECUTION_CONTRACT,
|
|
4
|
-
|
|
5
|
-
} from '../shared/background-tasks.mjs';
|
|
3
|
+
} from '../shared/tool-execution-contract.mjs';
|
|
6
4
|
|
|
7
5
|
export const TOOL_DEFS = [
|
|
8
6
|
{
|
|
9
7
|
name: 'search',
|
|
10
8
|
title: 'Mixdog Web Search',
|
|
11
|
-
description: `Web search
|
|
9
|
+
description: `Web/docs/current-info search. Not repo-local; use explore/code_graph/grep. ${TOOL_SYNC_EXECUTION_CONTRACT} Cached; query supports array fan-out.`,
|
|
12
10
|
inputSchema: {
|
|
13
11
|
type: 'object',
|
|
14
12
|
properties: {
|
|
15
|
-
query: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: '
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
firstResponseTimeoutMs: { type: 'number', minimum: 0, description: 'Abort only when the Web Researcher produces no first stream/tool activity within this many ms. Default 120s. 0 disables this watchdog.' },
|
|
20
|
-
idleTimeoutMs: { type: 'number', minimum: 0, description: 'Stale watchdog after first Web Researcher stream/tool activity. Default 30m. 0 disables stale abort.' },
|
|
21
|
-
site: { type: 'string', description: 'Optional site/domain filter.' },
|
|
22
|
-
type: { type: 'string', enum: ['web', 'news', 'images'], description: 'Search type. Default web.' },
|
|
23
|
-
maxResults: { type: 'number', minimum: 1, maximum: 20, description: 'Maximum results to return, 1-20.' },
|
|
13
|
+
query: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Query or array for fan-out.' },
|
|
14
|
+
site: { type: 'string', description: 'Site/domain filter.' },
|
|
15
|
+
type: { type: 'string', enum: ['web', 'news', 'images'], description: 'Default web.' },
|
|
16
|
+
maxResults: { type: 'number', minimum: 1, maximum: 20, description: 'Result count, 1-20.' },
|
|
24
17
|
locale: {
|
|
25
|
-
description: 'Optional locale hint
|
|
18
|
+
description: 'Optional locale hint.',
|
|
26
19
|
anyOf: [
|
|
27
20
|
{ type: 'string' },
|
|
28
21
|
{
|
|
29
22
|
type: 'object',
|
|
30
23
|
properties: {
|
|
31
|
-
country: { type: 'string', description: 'Country
|
|
32
|
-
language: { type: 'string', description: '
|
|
33
|
-
region: { type: 'string', description: 'Region
|
|
34
|
-
city: { type: 'string', description: 'City
|
|
35
|
-
timezone: { type: 'string', description: '
|
|
24
|
+
country: { type: 'string', description: 'Country.' },
|
|
25
|
+
language: { type: 'string', description: 'Language.' },
|
|
26
|
+
region: { type: 'string', description: 'Region.' },
|
|
27
|
+
city: { type: 'string', description: 'City.' },
|
|
28
|
+
timezone: { type: 'string', description: 'Timezone.' },
|
|
36
29
|
},
|
|
37
30
|
additionalProperties: false,
|
|
38
31
|
},
|
|
39
32
|
],
|
|
40
33
|
},
|
|
41
|
-
contextSize: { type: 'string', enum: ['low', 'medium', 'high'], description: '
|
|
34
|
+
contextSize: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Default low.' },
|
|
42
35
|
},
|
|
43
36
|
required: [],
|
|
44
37
|
additionalProperties: false,
|
|
@@ -48,12 +41,12 @@ export const TOOL_DEFS = [
|
|
|
48
41
|
{
|
|
49
42
|
name: 'web_fetch',
|
|
50
43
|
title: 'Mixdog Web Fetch',
|
|
51
|
-
description: 'Fetch
|
|
44
|
+
description: 'Use after search. Fetch page/docs body from URL.',
|
|
52
45
|
inputSchema: {
|
|
53
46
|
type: 'object',
|
|
54
47
|
properties: {
|
|
55
|
-
url: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'URL
|
|
56
|
-
startIndex: { type: 'number', minimum: 0, description: 'Character offset
|
|
48
|
+
url: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'URL or array of URLs.' },
|
|
49
|
+
startIndex: { type: 'number', minimum: 0, description: 'Character offset.' },
|
|
57
50
|
maxLength: { type: 'number', minimum: 0, description: 'Maximum characters to return.' },
|
|
58
51
|
},
|
|
59
52
|
required: ['url'],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AbortController helpers — ported from
|
|
2
|
+
* AbortController helpers — ported from a reference agent CLI pattern.
|
|
3
3
|
*
|
|
4
4
|
* `createAbortController()` raises the signal's max listener cap so long-running
|
|
5
5
|
* sessions with many per-iteration handlers don't trip Node's default warning.
|
|
@@ -121,9 +121,10 @@ export function withFileLockSync(lockPath, fn, opts = {}) {
|
|
|
121
121
|
// For secret-bearing critical sections, the lock file sits beside
|
|
122
122
|
// the secret in the same (shared-home) directory; clamp it owner-only
|
|
123
123
|
// too. Fail-closed: an unenforceable ACL aborts before fn() runs.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
try {
|
|
125
|
+
if (opts.secret === true) _enforceOwnerOnlyAclWin32(lockPath);
|
|
126
|
+
return fn();
|
|
127
|
+
} finally {
|
|
127
128
|
try { closeSync(fd); } catch {}
|
|
128
129
|
// Only unlink if we still own the lock. If our hold exceeded
|
|
129
130
|
// staleMs and another process already stole + replaced the
|
|
@@ -19,6 +19,14 @@ const TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled']);
|
|
|
19
19
|
const tasks = new Map();
|
|
20
20
|
let seq = 0;
|
|
21
21
|
|
|
22
|
+
// Owner-injected fallback used by notifyToolCompletion: when no notifyFn is
|
|
23
|
+
// available (or it declines), enqueue the completion onto the caller session so
|
|
24
|
+
// the owner is never left waiting on a canonical completion that never arrives.
|
|
25
|
+
let _enqueueFallback = null;
|
|
26
|
+
export function setBackgroundTaskEnqueueFallback(fn) {
|
|
27
|
+
_enqueueFallback = typeof fn === 'function' ? fn : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
function clean(value) {
|
|
23
31
|
return String(value ?? '').trim();
|
|
24
32
|
}
|
|
@@ -41,7 +49,7 @@ export function resolveExecutionMode(args = {}, defaultMode = 'sync') {
|
|
|
41
49
|
|
|
42
50
|
export function executionModeSchemaDescription(defaultMode = 'sync') {
|
|
43
51
|
const defaultText = defaultMode === 'async' ? 'Default async.' : 'Default sync.';
|
|
44
|
-
return `
|
|
52
|
+
return `sync = inline result; async = task_id + completion notification. ${defaultText}`;
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
export function taskIdFromArgs(args = {}) {
|
|
@@ -75,6 +83,45 @@ function normalizeTaskScope(options = {}) {
|
|
|
75
83
|
};
|
|
76
84
|
}
|
|
77
85
|
|
|
86
|
+
const RENDER_META_OMIT_KEYS = new Set([
|
|
87
|
+
// Internal watchdog/control knobs are intentionally not part of the public
|
|
88
|
+
// task notification surface. They may appear on legacy/in-flight task meta
|
|
89
|
+
// from older callers, but rendering them teaches the model/user to set them.
|
|
90
|
+
'firstResponseTimeoutMs',
|
|
91
|
+
'idleTimeoutMs',
|
|
92
|
+
'spawnPrepTimeoutMs',
|
|
93
|
+
'watchdogPolicy',
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
function isInternalTaskMetaKey(key) {
|
|
97
|
+
const name = clean(key);
|
|
98
|
+
if (!name) return true;
|
|
99
|
+
if (RENDER_META_OMIT_KEYS.has(name)) return true;
|
|
100
|
+
if (/timeoutms$/i.test(name)) return true;
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Drop internal control knobs from meta before persisting on a task row. */
|
|
105
|
+
export function sanitizeTaskMeta(meta = {}) {
|
|
106
|
+
if (!meta || typeof meta !== 'object') return {};
|
|
107
|
+
const out = {};
|
|
108
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
109
|
+
if (isInternalTaskMetaKey(key)) continue;
|
|
110
|
+
out[key] = value;
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function publicTaskMeta(meta = {}) {
|
|
116
|
+
const out = {};
|
|
117
|
+
for (const [key, value] of Object.entries(meta || {})) {
|
|
118
|
+
if (value == null || value === '') continue;
|
|
119
|
+
if (isInternalTaskMetaKey(key)) continue;
|
|
120
|
+
out[key] = value;
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
78
125
|
function hasScopeCriteria(scope) {
|
|
79
126
|
return Boolean(scope?.callerSessionId || scope?.routingSessionId || scope?.clientHostPid);
|
|
80
127
|
}
|
|
@@ -153,7 +200,7 @@ export function registerBackgroundTask({
|
|
|
153
200
|
ownerSessionId: notifyContext.callerSessionId || notifyContext.routingSessionId || null,
|
|
154
201
|
routingSessionId: notifyContext.routingSessionId || notifyContext.callerSessionId || null,
|
|
155
202
|
clientHostPid: notifyContext.clientHostPid || null,
|
|
156
|
-
meta: meta && typeof meta === 'object' ? meta : {},
|
|
203
|
+
meta: sanitizeTaskMeta(meta && typeof meta === 'object' ? meta : {}),
|
|
157
204
|
resultType: clean(resultType) || null,
|
|
158
205
|
renderResult: typeof renderResult === 'function' ? renderResult : null,
|
|
159
206
|
cancel: typeof cancel === 'function' ? cancel : null,
|
|
@@ -167,7 +214,7 @@ export function startBackgroundTask(options = {}) {
|
|
|
167
214
|
task.promise = Promise.resolve()
|
|
168
215
|
.then(() => options.run?.())
|
|
169
216
|
.then((result) => {
|
|
170
|
-
completeBackgroundTask(task.taskId, { status: 'completed', result });
|
|
217
|
+
completeBackgroundTask(task.taskId, { status: 'completed', result, terminalReason: 'run-resolved' });
|
|
171
218
|
return result;
|
|
172
219
|
})
|
|
173
220
|
.catch((error) => {
|
|
@@ -175,6 +222,7 @@ export function startBackgroundTask(options = {}) {
|
|
|
175
222
|
completeBackgroundTask(task.taskId, {
|
|
176
223
|
status: 'failed',
|
|
177
224
|
error,
|
|
225
|
+
terminalReason: 'run-rejected',
|
|
178
226
|
});
|
|
179
227
|
return null;
|
|
180
228
|
});
|
|
@@ -241,7 +289,14 @@ function resultTextForTask(task) {
|
|
|
241
289
|
if (task.resultText) return task.resultText;
|
|
242
290
|
if (task.result !== undefined) {
|
|
243
291
|
if (typeof task.renderResult === 'function') {
|
|
244
|
-
try {
|
|
292
|
+
try {
|
|
293
|
+
const rendered = String(task.renderResult(task.result, task) || '');
|
|
294
|
+
if (rendered) return rendered;
|
|
295
|
+
} catch (err) {
|
|
296
|
+
// Don't silently swallow a renderer throw: fall through to the JSON
|
|
297
|
+
// fallback below so the completion notification still carries a body.
|
|
298
|
+
try { process.stderr.write(`[background-${task.surface}] renderResult failed: ${err?.message || err}\n`); } catch {}
|
|
299
|
+
}
|
|
245
300
|
}
|
|
246
301
|
if (typeof task.result === 'string') return task.result;
|
|
247
302
|
try { return JSON.stringify(task.result, null, 2); } catch {}
|
|
@@ -257,6 +312,7 @@ export function completeBackgroundTask(taskId, {
|
|
|
257
312
|
resultType,
|
|
258
313
|
instruction,
|
|
259
314
|
notify = true,
|
|
315
|
+
terminalReason = null,
|
|
260
316
|
} = {}) {
|
|
261
317
|
const task = getBackgroundTask(taskId);
|
|
262
318
|
if (!task) return null;
|
|
@@ -265,17 +321,30 @@ export function completeBackgroundTask(taskId, {
|
|
|
265
321
|
task.status = normalizeStatus(status);
|
|
266
322
|
task.finishedAtMs = now;
|
|
267
323
|
task.finishedAt = new Date(now).toISOString();
|
|
324
|
+
if (terminalReason) task.terminalReason = terminalReason;
|
|
268
325
|
if (result !== undefined) task.result = result;
|
|
269
326
|
if (resultText != null) task.resultText = compactText(resultText);
|
|
270
327
|
if (error != null) task.error = presentErrorText(error, { surface: task.surface });
|
|
271
328
|
if (resultType) task.resultType = resultType;
|
|
272
|
-
if (notify)
|
|
329
|
+
if (notify) {
|
|
330
|
+
task.meta = sanitizeTaskMeta(task.meta);
|
|
331
|
+
notifyTaskCompletion(task, instruction);
|
|
332
|
+
}
|
|
273
333
|
return task;
|
|
274
334
|
}
|
|
275
335
|
|
|
276
336
|
export function notifyTaskCompletion(task, instruction) {
|
|
277
|
-
if (!task
|
|
337
|
+
if (!task) return false;
|
|
278
338
|
if (!TERMINAL_STATUSES.has(task.status)) return false;
|
|
339
|
+
// `notified` tracks whether *any* completion notification was sent (e.g. an
|
|
340
|
+
// early header-only preview from the agent surface). A later call that can
|
|
341
|
+
// finally include the result body must still fire once, otherwise the owner
|
|
342
|
+
// only ever sees the bodyless preview. Gate on `notifiedWithBody` so the
|
|
343
|
+
// body-carrying notification is delivered exactly once.
|
|
344
|
+
const body = resultTextForTask(task);
|
|
345
|
+
const hasBody = Boolean(body);
|
|
346
|
+
if (task.notifiedWithBody === true) return false;
|
|
347
|
+
if (task.notified === true && !hasBody) return false;
|
|
279
348
|
const text = renderBackgroundTask(task, { includeResult: true });
|
|
280
349
|
const sent = notifyToolCompletion({
|
|
281
350
|
surface: task.surface,
|
|
@@ -285,12 +354,43 @@ export function notifyTaskCompletion(task, instruction) {
|
|
|
285
354
|
resultType: task.resultType || `${task.surface}_task_result`,
|
|
286
355
|
instruction,
|
|
287
356
|
context: task.notifyContext,
|
|
357
|
+
enqueueFallback: _enqueueFallback || undefined,
|
|
288
358
|
logPrefix: `background-${task.surface}`,
|
|
289
359
|
});
|
|
290
|
-
if (sent)
|
|
360
|
+
if (sent) {
|
|
361
|
+
task.notified = true;
|
|
362
|
+
if (hasBody) task.notifiedWithBody = true;
|
|
363
|
+
}
|
|
291
364
|
return sent;
|
|
292
365
|
}
|
|
293
366
|
|
|
367
|
+
// Safety net for the "task stuck in running" failure mode: when an upstream
|
|
368
|
+
// owner (e.g. the agent surface) knows a job has reached a terminal state but
|
|
369
|
+
// the normal run-resolved path did not mark the task — typically because a
|
|
370
|
+
// post-result step (session save) hung or threw before the task promise could
|
|
371
|
+
// settle — this forces the task to a terminal state and fires the completion
|
|
372
|
+
// notification. Idempotent: a no-op once the task is already terminal.
|
|
373
|
+
export function reconcileBackgroundTask(taskId, {
|
|
374
|
+
status = 'completed',
|
|
375
|
+
result,
|
|
376
|
+
resultText,
|
|
377
|
+
error,
|
|
378
|
+
instruction,
|
|
379
|
+
terminalReason = 'reconciled',
|
|
380
|
+
} = {}) {
|
|
381
|
+
const task = getBackgroundTask(taskId);
|
|
382
|
+
if (!task) return null;
|
|
383
|
+
if (TERMINAL_STATUSES.has(task.status)) return task;
|
|
384
|
+
return completeBackgroundTask(task.taskId, {
|
|
385
|
+
status,
|
|
386
|
+
result,
|
|
387
|
+
resultText,
|
|
388
|
+
error,
|
|
389
|
+
instruction,
|
|
390
|
+
terminalReason,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
294
394
|
export function notifyBackgroundTaskProgress(taskOrId, {
|
|
295
395
|
text,
|
|
296
396
|
resultText,
|
|
@@ -317,6 +417,7 @@ export function notifyBackgroundTaskProgress(taskOrId, {
|
|
|
317
417
|
resultType: resultType || `${task.surface}_task_progress`,
|
|
318
418
|
instruction,
|
|
319
419
|
context: task.notifyContext,
|
|
420
|
+
enqueueFallback: _enqueueFallback || undefined,
|
|
320
421
|
logPrefix: `background-${task.surface}`,
|
|
321
422
|
});
|
|
322
423
|
if (sent && once) task.progressNotifiedKeys.add(progressKey);
|
|
@@ -334,13 +435,14 @@ export function taskSummary(task) {
|
|
|
334
435
|
startedAt: task.startedAt,
|
|
335
436
|
finishedAt: task.finishedAt,
|
|
336
437
|
error: task.error,
|
|
337
|
-
...task.meta,
|
|
438
|
+
...publicTaskMeta(task.meta),
|
|
338
439
|
};
|
|
339
440
|
}
|
|
340
441
|
|
|
341
442
|
export function renderBackgroundTask(taskOrId, { includeResult = false } = {}) {
|
|
342
443
|
const task = typeof taskOrId === 'string' ? getBackgroundTask(taskOrId) : taskOrId;
|
|
343
444
|
if (!task) return 'Error: background task not found';
|
|
445
|
+
const visibleMeta = publicTaskMeta(sanitizeTaskMeta(task.meta));
|
|
344
446
|
const lines = [
|
|
345
447
|
'background task',
|
|
346
448
|
`task_id: ${task.taskId}`,
|
|
@@ -352,8 +454,7 @@ export function renderBackgroundTask(taskOrId, { includeResult = false } = {}) {
|
|
|
352
454
|
task.finishedAt ? `finished: ${task.finishedAt}` : null,
|
|
353
455
|
task.error ? `error: ${task.error}` : null,
|
|
354
456
|
];
|
|
355
|
-
for (const [key, value] of Object.entries(
|
|
356
|
-
if (value == null || value === '') continue;
|
|
457
|
+
for (const [key, value] of Object.entries(visibleMeta)) {
|
|
357
458
|
lines.push(`${key}: ${value}`);
|
|
358
459
|
}
|
|
359
460
|
if (task.status === 'running') {
|
|
@@ -361,7 +462,17 @@ export function renderBackgroundTask(taskOrId, { includeResult = false } = {}) {
|
|
|
361
462
|
}
|
|
362
463
|
if (includeResult) {
|
|
363
464
|
const body = resultTextForTask(task);
|
|
364
|
-
if (body)
|
|
465
|
+
if (body) {
|
|
466
|
+
lines.push('', body);
|
|
467
|
+
} else if (TERMINAL_STATUSES.has(task.status) && task.error) {
|
|
468
|
+
const errorText = String(task.error || '').trim();
|
|
469
|
+
lines.push('', /^error\s*:/i.test(errorText) ? errorText : `Error: ${errorText}`);
|
|
470
|
+
} else if (TERMINAL_STATUSES.has(task.status) && task.status === 'completed') {
|
|
471
|
+
// Terminal-completed task with no extractable body: surface a placeholder
|
|
472
|
+
// instead of silently omitting the result so the owner isn't left with a
|
|
473
|
+
// header-only card that looks truncated.
|
|
474
|
+
lines.push('', '(no result body — use read for full output)');
|
|
475
|
+
}
|
|
365
476
|
}
|
|
366
477
|
return lines.filter((line) => line !== null).join('\n');
|
|
367
478
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ── Module-global child-spawn semaphore ──────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Single-daemon premise: tool execution is in-process, so a module-level
|
|
6
|
+
// singleton semaphore bounds the number of concurrent child processes (rg,
|
|
7
|
+
// mixdog-graph, …) across ALL agents/workers in this daemon. The goal is to
|
|
8
|
+
// absorb the over-saturation tail — when N agents fire grep/code_graph at
|
|
9
|
+
// once each child no longer fans out across every core, and the rg 20s
|
|
10
|
+
// deadline stops tripping en masse — WITHOUT serializing: up to the cap run
|
|
11
|
+
// concurrently, only the overflow queues and drains as slots free.
|
|
12
|
+
//
|
|
13
|
+
// IMPORTANT: this is a resource-control knob and is deliberately NOT exposed
|
|
14
|
+
// on any tool JSON schema / tool parameter surface. The only tuning surface
|
|
15
|
+
// is the internal constant + the MIXDOG_CHILD_SPAWN_MAX_INFLIGHT env override.
|
|
16
|
+
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
|
|
19
|
+
function _defaultMaxInflight() {
|
|
20
|
+
const override = Number(process.env.MIXDOG_CHILD_SPAWN_MAX_INFLIGHT);
|
|
21
|
+
if (Number.isFinite(override) && override >= 1) return Math.floor(override);
|
|
22
|
+
let cpus = 0;
|
|
23
|
+
try { cpus = os.cpus()?.length || 0; } catch { cpus = 0; }
|
|
24
|
+
return Math.max(4, cpus || 4);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MAX_INFLIGHT = _defaultMaxInflight();
|
|
28
|
+
|
|
29
|
+
// Warn (once-throttled, stderr only) when a waiter sat in the queue longer
|
|
30
|
+
// than this — a coarse signal that the cap is undersized for the load. Kept
|
|
31
|
+
// intentionally quiet so a busy daemon does not spam stderr.
|
|
32
|
+
const SLOW_WAIT_MS = Math.max(
|
|
33
|
+
1000,
|
|
34
|
+
Number(process.env.MIXDOG_CHILD_SPAWN_SLOW_MS) || 10000,
|
|
35
|
+
);
|
|
36
|
+
const SLOW_WARN_THROTTLE_MS = 30000;
|
|
37
|
+
|
|
38
|
+
let _inflight = 0;
|
|
39
|
+
/** @type {Array<{ resolve: () => void, reject: (e: any) => void, signal: AbortSignal | null, onAbort: (() => void) | null, enqueuedAt: number }>} */
|
|
40
|
+
const _queue = [];
|
|
41
|
+
let _lastSlowWarnAt = 0;
|
|
42
|
+
|
|
43
|
+
function _maybeWarnSlow(waitedMs) {
|
|
44
|
+
if (waitedMs < SLOW_WAIT_MS) return;
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
if (now - _lastSlowWarnAt < SLOW_WARN_THROTTLE_MS) return;
|
|
47
|
+
_lastSlowWarnAt = now;
|
|
48
|
+
try {
|
|
49
|
+
process.stderr.write(
|
|
50
|
+
`[child-spawn-gate] queue wait ${waitedMs}ms (inflight cap=${MAX_INFLIGHT}, queued=${_queue.length}); `
|
|
51
|
+
+ 'raise MIXDOG_CHILD_SPAWN_MAX_INFLIGHT if this persists\n',
|
|
52
|
+
);
|
|
53
|
+
} catch { /* ignore */ }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function _drain() {
|
|
57
|
+
while (_inflight < MAX_INFLIGHT && _queue.length > 0) {
|
|
58
|
+
const waiter = _queue.shift();
|
|
59
|
+
if (waiter.onAbort && waiter.signal) {
|
|
60
|
+
try { waiter.signal.removeEventListener('abort', waiter.onAbort); } catch { /* ignore */ }
|
|
61
|
+
}
|
|
62
|
+
_inflight++;
|
|
63
|
+
_maybeWarnSlow(Date.now() - waiter.enqueuedAt);
|
|
64
|
+
waiter.resolve();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Acquire one child-spawn slot. Resolves immediately when below the cap,
|
|
70
|
+
* otherwise queues until a slot frees. The returned function releases the
|
|
71
|
+
* slot and is idempotent (safe to call from multiple settle paths — only the
|
|
72
|
+
* first call counts). Supports an optional AbortSignal: aborting while still
|
|
73
|
+
* queued rejects with the signal reason and removes the waiter (no leak); a
|
|
74
|
+
* post-acquire abort is a no-op here — the caller owns teardown and must still
|
|
75
|
+
* call release().
|
|
76
|
+
*
|
|
77
|
+
* @param {AbortSignal | null} [signal]
|
|
78
|
+
* @returns {Promise<() => void>}
|
|
79
|
+
*/
|
|
80
|
+
export function acquire(signal = null) {
|
|
81
|
+
if (signal && signal.aborted) {
|
|
82
|
+
return Promise.reject(signal.reason ?? _abortError());
|
|
83
|
+
}
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
let released = false;
|
|
86
|
+
const release = () => {
|
|
87
|
+
if (released) return;
|
|
88
|
+
released = true;
|
|
89
|
+
_inflight = Math.max(0, _inflight - 1);
|
|
90
|
+
_drain();
|
|
91
|
+
};
|
|
92
|
+
if (_inflight < MAX_INFLIGHT && _queue.length === 0) {
|
|
93
|
+
_inflight++;
|
|
94
|
+
resolve(release);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const waiter = {
|
|
98
|
+
enqueuedAt: Date.now(),
|
|
99
|
+
signal: signal || null,
|
|
100
|
+
onAbort: null,
|
|
101
|
+
resolve: () => resolve(release),
|
|
102
|
+
reject,
|
|
103
|
+
};
|
|
104
|
+
if (signal) {
|
|
105
|
+
waiter.onAbort = () => {
|
|
106
|
+
const idx = _queue.indexOf(waiter);
|
|
107
|
+
if (idx !== -1) _queue.splice(idx, 1);
|
|
108
|
+
try { signal.removeEventListener('abort', waiter.onAbort); } catch { /* ignore */ }
|
|
109
|
+
reject(signal.reason ?? _abortError());
|
|
110
|
+
};
|
|
111
|
+
try { signal.addEventListener('abort', waiter.onAbort, { once: true }); } catch { /* ignore */ }
|
|
112
|
+
}
|
|
113
|
+
_queue.push(waiter);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function _abortError() {
|
|
118
|
+
const e = new Error('child-spawn-gate: aborted while queued');
|
|
119
|
+
e.code = 'ABORT_ERR';
|
|
120
|
+
return e;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Run `fn` while holding one child-spawn slot. Release is guaranteed in a
|
|
125
|
+
* finally so a throw/return from `fn` cannot leak a slot or deadlock the gate.
|
|
126
|
+
*
|
|
127
|
+
* @template T
|
|
128
|
+
* @param {(args: { signal: AbortSignal | null }) => Promise<T> | T} fn
|
|
129
|
+
* @param {AbortSignal | null} [signal]
|
|
130
|
+
* @returns {Promise<T>}
|
|
131
|
+
*/
|
|
132
|
+
export async function withGate(fn, signal = null) {
|
|
133
|
+
const release = await acquire(signal);
|
|
134
|
+
try {
|
|
135
|
+
return await fn({ signal: signal || null });
|
|
136
|
+
} finally {
|
|
137
|
+
release();
|
|
138
|
+
}
|
|
139
|
+
}
|