mixdog 0.8.0 → 0.9.0
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 +33 -27
- 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 +344 -0
- 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 +1223 -95
- 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 -7
- package/src/lib/rules-builder.cjs +240 -96
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2325 -450
- 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 +118 -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 +377 -243
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -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 +221 -70
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
- 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 +31 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
- 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 +851 -674
- package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
- 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 +34 -7
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
- 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/snapshot-store.mjs +1 -1
- 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 +152 -24
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -2
- package/src/runtime/channels/lib/tool-format.mjs +0 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/memory/index.mjs +635 -107
- 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/memory-cycle1.mjs +38 -17
- package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
- 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 +15 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
- package/src/runtime/memory/lib/session-ingest.mjs +194 -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 +102 -466
- package/src/runtime/search/lib/web-tools.mjs +45 -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 +145 -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 -98
- 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 +2 -69
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4798 -2153
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +87 -29
- 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 +367 -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 +617 -368
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +365 -0
- package/src/tui/components/tool-output-format.test.mjs +220 -0
- package/src/tui/dist/index.mjs +8915 -2418
- 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 +511 -68
- package/src/tui/markdown/format-token.test.mjs +216 -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 +746 -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 +3 -16
- package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
- 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/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/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/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
|
@@ -3,12 +3,27 @@ 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
|
+
}
|
|
26
|
+
|
|
12
27
|
|
|
13
28
|
const PKG_VERSION = (() => { try { return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version } catch { return '0.0.1' } })()
|
|
14
29
|
import {
|
|
@@ -491,7 +506,8 @@ function classifyJavascriptRenderingPlaceholder(page) {
|
|
|
491
506
|
return null
|
|
492
507
|
}
|
|
493
508
|
|
|
494
|
-
function extractReadableArticle(url, html) {
|
|
509
|
+
async function extractReadableArticle(url, html) {
|
|
510
|
+
const JSDOM = await loadJSDOM()
|
|
495
511
|
const dom = new JSDOM(html, { url })
|
|
496
512
|
try {
|
|
497
513
|
const doc = dom.window.document
|
|
@@ -701,11 +717,11 @@ async function fetchPinnedForPausedRequest(url, { signal, method = 'GET', header
|
|
|
701
717
|
currentUrl = new URL(location, currentUrl).toString()
|
|
702
718
|
continue
|
|
703
719
|
}
|
|
704
|
-
const
|
|
720
|
+
const respBody = await readBodyBytesWithCap(response, MAX_BODY_BYTES)
|
|
705
721
|
return {
|
|
706
722
|
status: response.status,
|
|
707
723
|
responseHeaders: headersToCdpPairs(response.headers),
|
|
708
|
-
body,
|
|
724
|
+
body: respBody,
|
|
709
725
|
}
|
|
710
726
|
}
|
|
711
727
|
}
|
|
@@ -812,7 +828,7 @@ async function scrapeWithReadability(url, timeoutMs, signal) {
|
|
|
812
828
|
currentUrl = target
|
|
813
829
|
html = await fetchHtml(currentUrl, timeoutMs, signal)
|
|
814
830
|
}
|
|
815
|
-
return extractReadableArticle(currentUrl, html)
|
|
831
|
+
return await extractReadableArticle(currentUrl, html)
|
|
816
832
|
}
|
|
817
833
|
|
|
818
834
|
function resolveBrowserLaunchOptions() {
|
|
@@ -888,23 +904,26 @@ async function _getPoolBrowser() {
|
|
|
888
904
|
}
|
|
889
905
|
if (_poolBrowser) return _poolBrowser
|
|
890
906
|
if (!_poolLaunching) {
|
|
891
|
-
_poolLaunching =
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
907
|
+
_poolLaunching = loadPuppeteer()
|
|
908
|
+
.then((puppeteer) => puppeteer.launch({
|
|
909
|
+
headless: true,
|
|
910
|
+
...resolveBrowserLaunchOptions(),
|
|
911
|
+
args: buildPuppeteerLaunchArgs(),
|
|
912
|
+
}))
|
|
913
|
+
.then((browser) => {
|
|
914
|
+
_poolBrowser = browser
|
|
915
|
+
try {
|
|
916
|
+
const proc = browser.process?.()
|
|
917
|
+
startChildGuardian({ childPid: proc?.pid, label: 'puppeteer-browser' })
|
|
918
|
+
} catch {}
|
|
919
|
+
browser.on('disconnected', () => {
|
|
920
|
+
if (_poolBrowser === browser) _poolBrowser = null
|
|
921
|
+
})
|
|
922
|
+
return browser
|
|
923
|
+
})
|
|
924
|
+
.finally(() => {
|
|
925
|
+
_poolLaunching = null
|
|
903
926
|
})
|
|
904
|
-
return browser
|
|
905
|
-
}).finally(() => {
|
|
906
|
-
_poolLaunching = null
|
|
907
|
-
})
|
|
908
927
|
}
|
|
909
928
|
return _poolLaunching
|
|
910
929
|
}
|
|
@@ -1033,7 +1052,7 @@ async function scrapeWithPuppeteer(url, timeoutMs, signal) {
|
|
|
1033
1052
|
}
|
|
1034
1053
|
try {
|
|
1035
1054
|
return {
|
|
1036
|
-
...extractReadableArticle(finalUrl, html),
|
|
1055
|
+
...(await extractReadableArticle(finalUrl, html)),
|
|
1037
1056
|
extractor: 'puppeteer',
|
|
1038
1057
|
}
|
|
1039
1058
|
} catch {
|
|
@@ -1088,7 +1107,8 @@ function filterLinks(rawLinks, baseUrl, { limit = 50, sameDomainOnly = true, sea
|
|
|
1088
1107
|
return items
|
|
1089
1108
|
}
|
|
1090
1109
|
|
|
1091
|
-
function extractLinksFromHtml(baseUrl, html, options) {
|
|
1110
|
+
async function extractLinksFromHtml(baseUrl, html, options) {
|
|
1111
|
+
const JSDOM = await loadJSDOM()
|
|
1092
1112
|
const dom = new JSDOM(html, { url: baseUrl })
|
|
1093
1113
|
try {
|
|
1094
1114
|
const links = Array.from(dom.window.document.querySelectorAll('a[href]')).map(link => ({
|
|
@@ -1103,7 +1123,7 @@ function extractLinksFromHtml(baseUrl, html, options) {
|
|
|
1103
1123
|
|
|
1104
1124
|
async function mapWithHttp(url, options, timeoutMs, signal) {
|
|
1105
1125
|
const html = await fetchHtml(url, timeoutMs, signal)
|
|
1106
|
-
return extractLinksFromHtml(url, html, options)
|
|
1126
|
+
return await extractLinksFromHtml(url, html, options)
|
|
1107
1127
|
}
|
|
1108
1128
|
|
|
1109
1129
|
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,145 @@
|
|
|
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
|
+
}
|
|
140
|
+
|
|
141
|
+
export function gateStats() {
|
|
142
|
+
return { inflight: _inflight, queued: _queue.length, maxInflight: MAX_INFLIGHT };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const CHILD_SPAWN_MAX_INFLIGHT = MAX_INFLIGHT;
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from './user-data-guard.mjs'
|
|
16
16
|
|
|
17
17
|
const _require = createRequire(import.meta.url)
|
|
18
|
-
const { getSecret: _getSecret, setSecret: _setSecret, deleteSecret: _deleteSecret } = _require('../../lib/keychain-cjs.cjs')
|
|
18
|
+
const { getSecret: _getSecret, setSecret: _setSecret, deleteSecret: _deleteSecret, hasSecret: _hasSecret } = _require('../../lib/keychain-cjs.cjs')
|
|
19
19
|
|
|
20
20
|
const DATA_DIR = resolvePluginData()
|
|
21
21
|
|
|
@@ -93,7 +93,7 @@ function readAll() {
|
|
|
93
93
|
// failed write), not a fresh install. Self-heal from the newest
|
|
94
94
|
// structurally-complete backup instead of silently collapsing to {} —
|
|
95
95
|
// which callers (readSection/getCapabilities) merge with in-memory
|
|
96
|
-
// defaults, dropping the user's presets/
|
|
96
|
+
// defaults, dropping the user's presets/agent routes/sections. In-memory only:
|
|
97
97
|
// readAll() runs OUTSIDE the config lock (unlike the RMW path), so we do
|
|
98
98
|
// not write here to avoid an unlocked-write race; the next writeSection
|
|
99
99
|
// re-persists the file under the lock.
|
|
@@ -220,7 +220,7 @@ export function updateSection(section, updater) {
|
|
|
220
220
|
// false (default), file tools are cwd-scoped — matches the setup UI's
|
|
221
221
|
// out-of-the-box "OFF" toggle so a fresh install is restrictive until the
|
|
222
222
|
// user explicitly opts in. This ONLY controls the main-agent path gate —
|
|
223
|
-
//
|
|
223
|
+
// agent role Edit/Write to HOME paths always go through Discord approval
|
|
224
224
|
// regardless (enforced in hooks/pre-tool-subagent.cjs).
|
|
225
225
|
const CAPABILITY_DEFAULTS = Object.freeze({ homeAccess: false })
|
|
226
226
|
|
|
@@ -387,7 +387,10 @@ export function deleteSecret(account) {
|
|
|
387
387
|
* browser.
|
|
388
388
|
*/
|
|
389
389
|
export function hasStoredSecret(account) {
|
|
390
|
-
try {
|
|
390
|
+
try {
|
|
391
|
+
if (typeof _hasSecret === 'function') return !!_hasSecret(account)
|
|
392
|
+
return !!_getSecret(account)
|
|
393
|
+
} catch { return false }
|
|
391
394
|
}
|
|
392
395
|
|
|
393
396
|
export { CONFIG_PATH }
|