mixdog 0.9.151 → 0.9.152
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 +110 -68
- package/package.json +3 -2
- package/scripts/tool-stress.mjs +0 -2
- package/src/defaults/skills/setup/SKILL.md +18 -27
- package/src/headless-exec.mjs +3 -10
- package/src/headless-exec.test.mjs +2 -7
- package/src/runtime/agent/orchestrator/config.mjs +6 -66
- package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
- package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
- package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
- package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +4 -7
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +235 -0
- package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +24 -1
- package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +161 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +31 -18
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +6 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +556 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +457 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +105 -186
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/browser-bridge/client.mjs +103 -0
- package/src/runtime/browser-bridge/tool-defs.mjs +42 -0
- package/src/runtime/computer-bridge/client.mjs +101 -0
- package/src/runtime/computer-bridge/tool-defs.mjs +53 -0
- package/src/runtime/media/store.mjs +18 -8
- package/src/runtime/media/store.test.mjs +14 -0
- package/src/runtime/memory/index.mjs +1 -1
- package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
- package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
- package/src/runtime/shared/child-spawn-gate.mjs +17 -0
- package/src/runtime/shared/child-spawn-remote.mjs +69 -7
- package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
- package/src/runtime/shared/pristine-execution-contract.json +2 -1
- package/src/runtime/shared/provider-api-key.mjs +1 -0
- package/src/runtime/shared/session-runtime-health.mjs +91 -0
- package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
- package/src/runtime/shared/skill-document.mjs +86 -0
- package/src/runtime/shared/skill-document.test.mjs +81 -0
- package/src/runtime/shared/tool-surface.mjs +18 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
- package/src/runtime/shared/turn-snapshot.mjs +32 -9
- package/src/runtime/shared/user-cwd.mjs +33 -14
- package/src/runtime/shared/user-cwd.test.mjs +43 -0
- package/src/session-runtime/cwd-plugins.mjs +26 -26
- package/src/session-runtime/cwd-plugins.test.mjs +75 -0
- package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
- package/src/session-runtime/global-extensions.mjs +26 -0
- package/src/session-runtime/global-extensions.test.mjs +28 -0
- package/src/session-runtime/goal-runtime.mjs +721 -0
- package/src/session-runtime/goal-runtime.test.mjs +108 -0
- package/src/session-runtime/lifecycle-api.mjs +18 -4
- package/src/session-runtime/lifecycle-api.test.mjs +21 -0
- package/src/session-runtime/mcp-glue.mjs +49 -47
- package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
- package/src/session-runtime/plugin-mcp.mjs +96 -22
- package/src/session-runtime/prewarm.mjs +1 -10
- package/src/session-runtime/resource-api-global.test.mjs +69 -0
- package/src/session-runtime/resource-api.mjs +173 -58
- package/src/session-runtime/runtime-core.mjs +148 -20
- package/src/session-runtime/runtime-tunables.mjs +6 -8
- package/src/session-runtime/session-turn-api.mjs +22 -11
- package/src/session-runtime/skills-api.mjs +79 -25
- package/src/session-runtime/skills-api.test.mjs +99 -0
- package/src/session-runtime/tool-catalog-schema.mjs +0 -5
- package/src/session-runtime/tool-catalog.mjs +4 -13
- package/src/session-runtime/tool-policy-surface.test.mjs +33 -11
- package/src/session-runtime/tool-surface.mjs +12 -0
- package/src/standalone/agent-dispatch-broker.mjs +5 -9
- package/src/standalone/agent-tool/notify.mjs +6 -8
- package/src/standalone/agent-tool/notify.test.mjs +37 -0
- package/src/standalone/daemon.mjs +31 -15
- package/src/standalone/hook-bus/config.mjs +1 -0
- package/src/standalone/plugin-admin.mjs +17 -0
- package/src/standalone/plugin-admin.test.mjs +7 -0
- package/src/standalone/provider-admin.mjs +1 -0
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
- package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
- package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
- package/src/standalone/session-runtime-host-factory.mjs +17 -0
- package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
- package/src/standalone/session-runtime-host-health.test.mjs +5 -0
- package/src/standalone/session-runtime-host.mjs +696 -58
- package/src/standalone/session-runtime-inline-host.mjs +351 -0
- package/src/standalone/session-runtime-inline-host.test.mjs +83 -0
- package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
- package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
- package/src/standalone/session-runtime-shard-host.test.mjs +569 -0
- package/src/standalone/session-runtime-shard-router.mjs +149 -0
- package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
- package/src/standalone/session-runtime-worker.mjs +476 -18
- package/src/standalone/session-service.mjs +81 -6
- package/src/tui/app/app-view.jsx +1 -0
- package/src/tui/app/core-memory-picker.mjs +2 -0
- package/src/tui/app/extension-pickers.mjs +1 -1
- package/src/tui/app/maintenance-pickers.mjs +3 -3
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/app/model-picker.mjs +14 -4
- package/src/tui/app/panel-handoff.test.mjs +311 -0
- package/src/tui/app/project-picker.mjs +1 -0
- package/src/tui/app/route-pickers.mjs +21 -6
- package/src/tui/app/settings-picker.mjs +26 -3
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +73 -22
- package/src/tui/app/theme-effort-pickers.mjs +7 -2
- package/src/tui/components/Picker.jsx +10 -1
- package/src/tui/dist/index.mjs +163 -44
- package/src/tui/session/context-state.mjs +1 -0
- package/src/tui/session/goal-continuation.mjs +110 -0
- package/src/tui/session/goal-continuation.test.mjs +84 -0
- package/src/tui/session/queue-helpers.mjs +5 -2
- package/src/tui/session/session-action-surface.test.mjs +108 -0
- package/src/tui/session/session-api-ext.mjs +39 -8
- package/src/tui/session/session-api.mjs +58 -0
- package/src/tui/session/session-flow.mjs +6 -0
- package/src/tui/session/turn.mjs +1 -0
- package/src/tui/session-local.mjs +15 -1
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
// Resident native search: forwards rg-style windowed line requests to ONE
|
|
2
|
-
// long-lived
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// long-lived engine that embeds the ripgrep matcher/searcher/printer crates
|
|
3
|
+
// and is the only local-search backend. Unsupported requests and server
|
|
4
|
+
// failures are explicit errors at the caller.
|
|
5
|
+
//
|
|
6
|
+
// The engine is reached through one crash-isolated
|
|
7
|
+
// `mixdog-graph --serve-search` child process.
|
|
8
|
+
//
|
|
5
9
|
// MIXDOG_SEARCH_SERVER=0 disables; MIXDOG_SEARCH_SERVER_BIN overrides the
|
|
6
|
-
//
|
|
7
|
-
import { spawn } from 'node:child_process';
|
|
10
|
+
// executable.
|
|
8
11
|
import { existsSync } from 'node:fs';
|
|
9
|
-
import {
|
|
10
|
-
|
|
12
|
+
import {
|
|
13
|
+
bindChildLifecycle,
|
|
14
|
+
createNativeSearchTransport,
|
|
15
|
+
} from './native-search-transport.mjs';
|
|
11
16
|
import { reportRuntimeWorkerUnhealthy } from '../../../../shared/session-runtime-health.mjs';
|
|
12
17
|
import { invalidateBuiltinResultCache } from './cache-layers.mjs';
|
|
13
18
|
import { getPluginData } from '../../config.mjs';
|
|
14
19
|
import { ensureGraphBinary } from '../graph-binary-fetcher.mjs';
|
|
15
|
-
import { fuzzyRank, prepareFuzzyItems } from './fuzzy-match.mjs';
|
|
16
20
|
|
|
17
21
|
const RESTART_BACKOFF_MS = 30_000;
|
|
18
22
|
// Default request budget. Deliberately BELOW the read-only I/O watchdog in
|
|
@@ -28,7 +32,7 @@ const PROCESS_FAILURES_BEFORE_BACKOFF = 2;
|
|
|
28
32
|
const SEARCH_TIMEOUT_RECYCLE_WINDOW_MS = 30_000;
|
|
29
33
|
const SEARCH_TIMEOUT_BURST_MS = 1_000;
|
|
30
34
|
|
|
31
|
-
let _server = null; // {
|
|
35
|
+
let _server = null; // { transport, exited, pending: Map, sequence }
|
|
32
36
|
let _binaryPath = undefined; // undefined = unresolved, null = unavailable
|
|
33
37
|
let _lastFailureAt = 0;
|
|
34
38
|
let _lastFailure = null;
|
|
@@ -149,11 +153,7 @@ function unavailableError() {
|
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
function _setServerReferenced(server, referenced) {
|
|
152
|
-
|
|
153
|
-
try { server?.child?.[method]?.(); } catch {}
|
|
154
|
-
try { server?.child?.stdin?.[method]?.(); } catch {}
|
|
155
|
-
try { server?.child?.stdout?.[method]?.(); } catch {}
|
|
156
|
-
try { server?.child?.stderr?.[method]?.(); } catch {}
|
|
156
|
+
try { server?.transport?.[referenced ? 'ref' : 'unref']?.(); } catch {}
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
function _resolveBinary() {
|
|
@@ -201,18 +201,18 @@ function _teardown(error, { countFailure = true, detail = '' } = {}) {
|
|
|
201
201
|
for (const timer of server.cancelWatchdogs?.values?.() || []) clearTimeout(timer);
|
|
202
202
|
server.cancelWatchdogs?.clear?.();
|
|
203
203
|
try { server.resolveReady?.(false); } catch {}
|
|
204
|
-
try { server.
|
|
204
|
+
try { server.transport.kill(); } catch {}
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
export async function shutdownNativeSearchServer(reason = 'process-exit', timeoutMs = 1_000) {
|
|
208
208
|
const server = _server;
|
|
209
209
|
if (!server) return true;
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
try {
|
|
210
|
+
const transport = server.transport;
|
|
211
|
+
// The exit promise is armed at attach time rather than here: the transport
|
|
212
|
+
// routes each event to exactly one handler, so a second listener registered
|
|
213
|
+
// now would displace the teardown wiring.
|
|
214
|
+
const exited = server.exited;
|
|
215
|
+
try { transport.end(); } catch {}
|
|
216
216
|
_teardown(new Error(`native search server shutdown (${reason})`), { countFailure: false });
|
|
217
217
|
_warmPromise = null;
|
|
218
218
|
let timer;
|
|
@@ -224,20 +224,12 @@ export async function shutdownNativeSearchServer(reason = 'process-exit', timeou
|
|
|
224
224
|
]);
|
|
225
225
|
if (timer) clearTimeout(timer);
|
|
226
226
|
if (!stopped) {
|
|
227
|
-
try {
|
|
227
|
+
try { transport.kill('SIGKILL'); } catch {}
|
|
228
228
|
}
|
|
229
229
|
return stopped;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
export
|
|
233
|
-
if (!child?.on) return;
|
|
234
|
-
child.on('error', onError);
|
|
235
|
-
child.on('exit', onExit);
|
|
236
|
-
// ChildProcess stdin emits write failures asynchronously. A sync try/catch
|
|
237
|
-
// around stdin.write cannot catch EPIPE; without this listener the session
|
|
238
|
-
// runtime worker itself terminates and every active/queued turn is recovered.
|
|
239
|
-
child.stdin?.on?.('error', onError);
|
|
240
|
-
}
|
|
232
|
+
export { bindChildLifecycle as _bindNativeSearchServerLifecycle };
|
|
241
233
|
|
|
242
234
|
function _ensureServer() {
|
|
243
235
|
if (_server) return _server;
|
|
@@ -245,22 +237,26 @@ function _ensureServer() {
|
|
|
245
237
|
_consecutiveProcessFailures >= PROCESS_FAILURES_BEFORE_BACKOFF
|
|
246
238
|
&& Date.now() - _lastFailureAt < RESTART_BACKOFF_MS
|
|
247
239
|
) return null;
|
|
248
|
-
|
|
249
|
-
if (!binary) return null;
|
|
250
|
-
let child;
|
|
240
|
+
let transport;
|
|
251
241
|
try {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
242
|
+
transport = createNativeSearchTransport({
|
|
243
|
+
binaryPath: _resolveBinary(),
|
|
244
|
+
cwd: process.cwd(),
|
|
255
245
|
});
|
|
256
246
|
} catch (error) {
|
|
257
247
|
noteProcessFailure(processFailure(error, null, 'spawn failed'));
|
|
258
248
|
return null;
|
|
259
249
|
}
|
|
250
|
+
if (!transport) return null;
|
|
260
251
|
let resolveReady;
|
|
261
252
|
const ready = new Promise((resolve) => { resolveReady = resolve; });
|
|
253
|
+
// Armed here, not at shutdown: the transport routes each event to exactly
|
|
254
|
+
// one handler, so a listener added later would displace the teardown wiring.
|
|
255
|
+
let markExited;
|
|
256
|
+
const exited = new Promise((resolve) => { markExited = resolve; });
|
|
262
257
|
const server = {
|
|
263
|
-
|
|
258
|
+
transport,
|
|
259
|
+
exited,
|
|
264
260
|
pending: new Map(),
|
|
265
261
|
cancelWatchdogs: new Map(),
|
|
266
262
|
sequence: 0,
|
|
@@ -270,11 +266,10 @@ function _ensureServer() {
|
|
|
270
266
|
readyWaiters: 0,
|
|
271
267
|
resolveReady,
|
|
272
268
|
};
|
|
273
|
-
|
|
269
|
+
transport.on('stderr', (chunk) => {
|
|
274
270
|
server.stderrTail = `${server.stderrTail}${String(chunk || '')}`.slice(-8192);
|
|
275
271
|
});
|
|
276
|
-
|
|
277
|
-
lines.on('line', (line) => {
|
|
272
|
+
transport.on('line', (line) => {
|
|
278
273
|
let message;
|
|
279
274
|
try { message = JSON.parse(line); } catch { return; }
|
|
280
275
|
if (message?.ready === true) {
|
|
@@ -297,24 +292,27 @@ function _ensureServer() {
|
|
|
297
292
|
clearProcessFailures();
|
|
298
293
|
pending.resolve(message);
|
|
299
294
|
});
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
295
|
+
transport.on('error', (error) => {
|
|
296
|
+
markExited(true);
|
|
297
|
+
if (_server === server) _teardown(error);
|
|
298
|
+
});
|
|
299
|
+
transport.on('exit', (code, signal) => {
|
|
300
|
+
markExited(true);
|
|
301
|
+
if (_server === server) {
|
|
302
|
+
_teardown(null, {
|
|
303
|
+
// A clean code-0 exit is the server's own idle self-shutdown
|
|
304
|
+
// (MIXDOG_SEARCH_SERVER_IDLE_MS), not a fault. Counting it would let
|
|
305
|
+
// two quiet stretches trip the restart backoff and refuse the next
|
|
306
|
+
// real search for 30s.
|
|
307
|
+
countFailure: code !== 0,
|
|
308
|
+
detail: `exit code=${code ?? 'null'} signal=${signal ?? 'null'}`,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
314
311
|
});
|
|
315
|
-
//
|
|
316
|
-
// handles
|
|
317
|
-
//
|
|
312
|
+
// An attached engine must never be the reason a one-shot CLI or test stays
|
|
313
|
+
// alive — a child's pipe handles and the addon's keep-alive handle both do
|
|
314
|
+
// that. Keep the resident server idle-unreferenced, then reference it only
|
|
315
|
+
// while a request is awaiting a response.
|
|
318
316
|
_setServerReferenced(server, false);
|
|
319
317
|
_server = server;
|
|
320
318
|
return server;
|
|
@@ -384,7 +382,7 @@ function armNativeCancellationWatchdog(server, request) {
|
|
|
384
382
|
if (_server === server) {
|
|
385
383
|
_teardown(error, { countFailure: false, detail: 'cancellation grace expired' });
|
|
386
384
|
} else {
|
|
387
|
-
try { server.
|
|
385
|
+
try { server.transport.kill(); } catch {}
|
|
388
386
|
}
|
|
389
387
|
}, graceMs);
|
|
390
388
|
timer.unref?.();
|
|
@@ -464,7 +462,7 @@ async function requestNative(server, request, execOptions, deadlineMs) {
|
|
|
464
462
|
let onAbort = null;
|
|
465
463
|
let unsubscribeAbort = null;
|
|
466
464
|
const cancelServerWork = () => {
|
|
467
|
-
try { server.
|
|
465
|
+
try { server.transport.write(`${JSON.stringify({ cancel: request.id })}\n`); } catch {}
|
|
468
466
|
armNativeCancellationWatchdog(server, request);
|
|
469
467
|
};
|
|
470
468
|
const settle = (value, cancel = false, error = null) => {
|
|
@@ -529,7 +527,7 @@ async function requestNative(server, request, execOptions, deadlineMs) {
|
|
|
529
527
|
}
|
|
530
528
|
unsubscribeAbort = subscribeAbortSignal(execOptions.signal, onAbort);
|
|
531
529
|
try {
|
|
532
|
-
server.
|
|
530
|
+
server.transport.write(`${JSON.stringify(request)}\n`);
|
|
533
531
|
} catch (error) {
|
|
534
532
|
if (_server === server) _teardown(error);
|
|
535
533
|
else {
|
|
@@ -711,6 +709,10 @@ export async function tryServeFuzzySearch(args, execOptions = {}) {
|
|
|
711
709
|
limit: Math.max(1, Math.min(1_000, Math.floor(Number(args?.limit) || 25))),
|
|
712
710
|
hidden: args?.hidden !== false,
|
|
713
711
|
includeNoise: args?.includeNoise === true,
|
|
712
|
+
// A fuzzy deadline returns a truthful partial window. Keep the one
|
|
713
|
+
// query-independent inventory walk alive so a retry can use its complete
|
|
714
|
+
// cache instead of starting a second whole-tree enumeration.
|
|
715
|
+
keepInventory: true,
|
|
714
716
|
...(Number.isFinite(Number(args?.maxDepth)) && Number(args.maxDepth) > 0
|
|
715
717
|
? { maxDepth: Math.floor(Number(args.maxDepth)) }
|
|
716
718
|
: {}),
|
|
@@ -727,18 +729,8 @@ export async function tryServeFuzzySearch(args, execOptions = {}) {
|
|
|
727
729
|
throw codedError('NATIVE_SEARCH_ERROR', String(response.error));
|
|
728
730
|
}
|
|
729
731
|
if (!Array.isArray(response?.matches)) throw unavailableError();
|
|
730
|
-
// Parity with the JS scorer's noise floor: the native matcher scores any
|
|
731
|
-
// subsequence, so a query with no real hit returns scattered junk paths
|
|
732
|
-
// (hash-named artifacts and the like). Re-rank the returned window through
|
|
733
|
-
// fuzzyRank, which keeps contiguous/basename hits unconditionally and
|
|
734
|
-
// drops sub-floor subsequence-only matches instead of surfacing noise.
|
|
735
|
-
const query = String(args?.query || '');
|
|
736
|
-
const filtered = query
|
|
737
|
-
? fuzzyRank(query, prepareFuzzyItems(response.matches.map(String)))
|
|
738
|
-
.map((entry) => entry.item.path)
|
|
739
|
-
: response.matches.map(String);
|
|
740
732
|
return {
|
|
741
|
-
matches:
|
|
733
|
+
matches: response.matches.map(String),
|
|
742
734
|
hasMore: response.hasMore === true,
|
|
743
735
|
totalMatches: Math.max(0, Number(response.totalMatches) || 0),
|
|
744
736
|
totalSeen: Math.max(0, Number(response.totalSeen) || 0),
|
|
@@ -17,11 +17,8 @@ async function serve(argsList, execOptions, opts) {
|
|
|
17
17
|
recordLocalSearchBackend('native', performance.now() - startedAt, 'hit');
|
|
18
18
|
return result;
|
|
19
19
|
} catch (error) {
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// the fallback as a backend error made healthy calls read as silent
|
|
23
|
-
// failures in run telemetry (9 of them in one measured run, all of
|
|
24
|
-
// them ordinary fallbacks for paths outside the served inventory).
|
|
20
|
+
// Unsupported request shapes are classified separately in telemetry,
|
|
21
|
+
// but still throw: there is no alternate local-search backend.
|
|
25
22
|
const code = String(error?.code || '');
|
|
26
23
|
const outcome = code === 'NATIVE_SEARCH_UNAVAILABLE' || code === 'NATIVE_SEARCH_UNSUPPORTED'
|
|
27
24
|
? 'miss'
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Crash-isolated transport for the resident native search engine.
|
|
2
|
+
//
|
|
3
|
+
// Search runs as one long-lived `mixdog-graph --serve-search` child. Keeping
|
|
4
|
+
// the Rust engine outside the daemon is intentional: a stuck native scan can
|
|
5
|
+
// be killed and restarted without blocking every session on the daemon loop.
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { createInterface } from 'node:readline';
|
|
8
|
+
import { hiddenSpawnOpts } from '../../../../shared/spawn-flags.mjs';
|
|
9
|
+
|
|
10
|
+
/** Minimal event fan-out: one transport feeds exactly one client. */
|
|
11
|
+
function createEmitter() {
|
|
12
|
+
const handlers = { line: null, stderr: null, error: null, exit: null };
|
|
13
|
+
return {
|
|
14
|
+
handlers,
|
|
15
|
+
emit(name, ...args) {
|
|
16
|
+
const handler = handlers[name];
|
|
17
|
+
if (typeof handler !== 'function') return;
|
|
18
|
+
try { handler(...args); } catch { /* a listener fault must not kill the transport */ }
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createChildTransport(binaryPath, cwd) {
|
|
24
|
+
const child = spawn(binaryPath, [cwd, '--serve-search'], {
|
|
25
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
26
|
+
...hiddenSpawnOpts,
|
|
27
|
+
});
|
|
28
|
+
const { handlers, emit } = createEmitter();
|
|
29
|
+
child.stderr?.on?.('data', (chunk) => emit('stderr', chunk));
|
|
30
|
+
const lines = createInterface({ input: child.stdout });
|
|
31
|
+
lines.on('line', (line) => emit('line', line));
|
|
32
|
+
bindChildLifecycle(child, {
|
|
33
|
+
onError: (error) => emit('error', error),
|
|
34
|
+
onExit: (code, signal) => emit('exit', code, signal),
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
kind: 'child',
|
|
38
|
+
child,
|
|
39
|
+
write(text) { child.stdin.write(text); },
|
|
40
|
+
end() { try { child.stdin?.end?.(); } catch { /* already closed */ } },
|
|
41
|
+
kill(signal) { try { child.kill(signal); } catch { /* already gone */ } },
|
|
42
|
+
ref() {
|
|
43
|
+
for (const handle of [child, child.stdin, child.stdout, child.stderr]) {
|
|
44
|
+
try { handle?.ref?.(); } catch { /* detached handle */ }
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
unref() {
|
|
48
|
+
for (const handle of [child, child.stdin, child.stdout, child.stderr]) {
|
|
49
|
+
try { handle?.unref?.(); } catch { /* detached handle */ }
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
on(name, handler) { handlers[name] = handler; },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* ChildProcess stdin emits write failures asynchronously. A sync try/catch
|
|
58
|
+
* around stdin.write cannot catch EPIPE; without this listener the session
|
|
59
|
+
* runtime worker itself terminates and every active/queued turn is recovered.
|
|
60
|
+
*/
|
|
61
|
+
export function bindChildLifecycle(child, { onError, onExit } = {}) {
|
|
62
|
+
if (!child?.on) return;
|
|
63
|
+
child.on('error', onError);
|
|
64
|
+
child.on('exit', onExit);
|
|
65
|
+
child.stdin?.on?.('error', onError);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Attach to the engine. Returns null until the verified binary is available;
|
|
70
|
+
* throws only on an unexpected spawn failure, which the caller counts.
|
|
71
|
+
*/
|
|
72
|
+
export function createNativeSearchTransport({ binaryPath, cwd }) {
|
|
73
|
+
if (!binaryPath) return null;
|
|
74
|
+
return createChildTransport(binaryPath, cwd);
|
|
75
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
bindChildLifecycle,
|
|
6
|
+
createNativeSearchTransport,
|
|
7
|
+
} from './native-search-transport.mjs';
|
|
8
|
+
|
|
9
|
+
test('search transport stays unavailable until the verified child binary exists', () => {
|
|
10
|
+
assert.equal(createNativeSearchTransport({ binaryPath: null, cwd: process.cwd() }), null);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('child stdin failures are routed through the lifecycle error handler', () => {
|
|
14
|
+
const errors = [];
|
|
15
|
+
const listeners = {};
|
|
16
|
+
const child = {
|
|
17
|
+
on(name, handler) { listeners[name] = handler; },
|
|
18
|
+
stdin: { on(name, handler) { listeners[`stdin:${name}`] = handler; } },
|
|
19
|
+
};
|
|
20
|
+
bindChildLifecycle(child, { onError: (error) => errors.push(error) });
|
|
21
|
+
const expected = new Error('EPIPE');
|
|
22
|
+
listeners['stdin:error'](expected);
|
|
23
|
+
assert.deepEqual(errors, [expected]);
|
|
24
|
+
});
|
|
@@ -419,6 +419,18 @@ export function _spawnCodeGraphWorker(
|
|
|
419
419
|
_onSignalAbort = null;
|
|
420
420
|
}
|
|
421
421
|
if (_releaseSlot) { try { _releaseSlot(); } catch {} _releaseSlot = null; }
|
|
422
|
+
// The graph has already arrived, so the worker has nothing left to run.
|
|
423
|
+
// It cannot exit on its own either: reading its piped stdout/stderr
|
|
424
|
+
// below keeps the underlying MessagePort ref'd for the worker's whole
|
|
425
|
+
// lifetime (the same trap save-worker.mjs documents). Measured on a
|
|
426
|
+
// desktop session worker: 10 builds left 26 live threads and ~450 MB
|
|
427
|
+
// resident with no build in flight. The timeout and abort paths already
|
|
428
|
+
// terminate; this is the success path they were missing.
|
|
429
|
+
if (_worker) {
|
|
430
|
+
const finished = _worker;
|
|
431
|
+
_worker = null;
|
|
432
|
+
try { void finished.terminate(); } catch { /* already down */ }
|
|
433
|
+
}
|
|
422
434
|
if (val instanceof Error) reject(val);
|
|
423
435
|
else resolve(val);
|
|
424
436
|
};
|
|
@@ -34,7 +34,10 @@ export const CODE_GRAPH_MEMORY_MAX_ENTRIES = Math.max(
|
|
|
34
34
|
1,
|
|
35
35
|
Math.floor(Number(process.env.MIXDOG_CODE_GRAPH_MEMORY_MAX_ENTRIES) || 6),
|
|
36
36
|
);
|
|
37
|
-
|
|
37
|
+
// Total retained graph memory, including nodes, symbols, indexes and source
|
|
38
|
+
// caches. The previous source-cache-only accounting left the much larger base
|
|
39
|
+
// graph unbounded across the six-entry LRU.
|
|
40
|
+
export const CODE_GRAPH_MEMORY_MAX_BYTES = Math.max(
|
|
38
41
|
1 * 1024 * 1024,
|
|
39
42
|
Math.floor((Number(process.env.MIXDOG_CODE_GRAPH_MEMORY_MAX_MB) || 48) * 1024 * 1024),
|
|
40
43
|
);
|
|
@@ -31,7 +31,8 @@ import {
|
|
|
31
31
|
} from './project-root.mjs';
|
|
32
32
|
import { buildCodeGraphAsync, prewarmCodeGraph, prewarmCodeGraphSymbols } from './build.mjs';
|
|
33
33
|
import { _fileInfoFromRustRecord, _runGraphFiles } from './graph-binary.mjs';
|
|
34
|
-
import { _attachGraphRuntimeCaches } from './graph-model.mjs';
|
|
34
|
+
import { _attachGraphRuntimeCaches, _estimateGraphRetainedBytes } from './graph-model.mjs';
|
|
35
|
+
import { _pruneCodeGraphMemoryCache } from './memory-cache.mjs';
|
|
35
36
|
import {
|
|
36
37
|
_isFilesystemRootPath,
|
|
37
38
|
collectTrustedCodeGraphRoots,
|
|
@@ -93,6 +94,23 @@ function _outlineLanguageForPath(file) {
|
|
|
93
94
|
const _exactFileGraphInflight = new Map();
|
|
94
95
|
const _exactFileGraphCache = new Map();
|
|
95
96
|
const EXACT_FILE_GRAPH_CACHE_MAX = 64;
|
|
97
|
+
const EXACT_FILE_GRAPH_CACHE_MAX_BYTES = 8 * 1024 * 1024;
|
|
98
|
+
|
|
99
|
+
function _pruneExactFileGraphCache() {
|
|
100
|
+
const rows = [..._exactFileGraphCache.entries()].map(([key, entry]) => ({
|
|
101
|
+
key,
|
|
102
|
+
retainedBytes: _estimateGraphRetainedBytes(entry?.graph),
|
|
103
|
+
}));
|
|
104
|
+
let totalRetainedBytes = rows.reduce((sum, row) => sum + row.retainedBytes, 0);
|
|
105
|
+
for (const row of rows) {
|
|
106
|
+
if (
|
|
107
|
+
_exactFileGraphCache.size <= EXACT_FILE_GRAPH_CACHE_MAX
|
|
108
|
+
&& totalRetainedBytes <= EXACT_FILE_GRAPH_CACHE_MAX_BYTES
|
|
109
|
+
) break;
|
|
110
|
+
_exactFileGraphCache.delete(row.key);
|
|
111
|
+
totalRetainedBytes -= row.retainedBytes;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
96
114
|
|
|
97
115
|
async function _buildExactFileGraph(cwd, abs, signal = null) {
|
|
98
116
|
const root = pathResolve(cwd);
|
|
@@ -144,9 +162,7 @@ async function _buildExactFileGraph(cwd, abs, signal = null) {
|
|
|
144
162
|
});
|
|
145
163
|
_exactFileGraphCache.delete(key);
|
|
146
164
|
_exactFileGraphCache.set(key, { sourceHash, graph });
|
|
147
|
-
|
|
148
|
-
_exactFileGraphCache.delete(_exactFileGraphCache.keys().next().value);
|
|
149
|
-
}
|
|
165
|
+
_pruneExactFileGraphCache();
|
|
150
166
|
return graph;
|
|
151
167
|
})();
|
|
152
168
|
const inflight = { sourceHash, promise: pending };
|
|
@@ -1244,7 +1260,10 @@ async function executeCodeGraphToolRaw(name, args, cwd, signal = null, options =
|
|
|
1244
1260
|
}
|
|
1245
1261
|
default: throw new Error(`Unknown code-graph tool: ${name}`);
|
|
1246
1262
|
}
|
|
1247
|
-
})()
|
|
1263
|
+
})().finally(() => {
|
|
1264
|
+
_pruneCodeGraphMemoryCache();
|
|
1265
|
+
_pruneExactFileGraphCache();
|
|
1266
|
+
});
|
|
1248
1267
|
if (!signal) return _work;
|
|
1249
1268
|
let onAbort = null;
|
|
1250
1269
|
const abortP = new Promise((_, reject) => {
|
|
@@ -1,10 +1,54 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import test from 'node:test';
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
3
4
|
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
5
|
import { tmpdir } from 'node:os';
|
|
5
6
|
import { join, resolve } from 'node:path';
|
|
6
7
|
|
|
7
8
|
import { _resolveBoundedSentinelFreeAggregateRootForTest } from './dispatch.mjs';
|
|
9
|
+
import { _spawnCodeGraphWorker } from './build.mjs';
|
|
10
|
+
|
|
11
|
+
test('successful code-graph build terminates its worker and releases the spawn slot', async () => {
|
|
12
|
+
let worker = null;
|
|
13
|
+
let releases = 0;
|
|
14
|
+
class FakeWorker extends EventEmitter {
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
this.stdout = new EventEmitter();
|
|
18
|
+
this.stderr = new EventEmitter();
|
|
19
|
+
this.terminations = 0;
|
|
20
|
+
}
|
|
21
|
+
terminate() {
|
|
22
|
+
this.terminations += 1;
|
|
23
|
+
return Promise.resolve(0);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const pending = _spawnCodeGraphWorker(
|
|
28
|
+
process.cwd(),
|
|
29
|
+
process.cwd(),
|
|
30
|
+
0,
|
|
31
|
+
null,
|
|
32
|
+
() => { releases += 1; },
|
|
33
|
+
null,
|
|
34
|
+
null,
|
|
35
|
+
{
|
|
36
|
+
createWorker() {
|
|
37
|
+
worker = new FakeWorker();
|
|
38
|
+
return worker;
|
|
39
|
+
},
|
|
40
|
+
getGeneration: () => 0,
|
|
41
|
+
setMemoryCache: () => {},
|
|
42
|
+
setDiskCache: () => {},
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
46
|
+
worker.emit('message', { ok: true, graph: { files: [] }, signature: 'sig' });
|
|
47
|
+
|
|
48
|
+
assert.deepEqual(await pending, { files: [] });
|
|
49
|
+
assert.equal(worker.terminations, 1);
|
|
50
|
+
assert.equal(releases, 1);
|
|
51
|
+
});
|
|
8
52
|
|
|
9
53
|
test('sentinel-free aggregate anchors adopt only their explicit bounded cwd', () => {
|
|
10
54
|
const root = mkdtempSync(join(tmpdir(), 'mixdog-codegraph-bounded-'));
|
|
@@ -216,13 +216,23 @@ async function _runGraphBinaryJsonl(absRoot, extraArgs, stdinLines = null, signa
|
|
|
216
216
|
}
|
|
217
217
|
const out = [];
|
|
218
218
|
const buf = Buffer.concat(chunks).toString('utf8');
|
|
219
|
+
let lineNumber = 0;
|
|
219
220
|
for (const line of buf.split('\n')) {
|
|
221
|
+
lineNumber += 1;
|
|
220
222
|
const trimmed = line.trim();
|
|
221
223
|
if (!trimmed) continue;
|
|
222
224
|
try {
|
|
223
225
|
const rec = JSON.parse(trimmed);
|
|
224
|
-
if (rec
|
|
225
|
-
|
|
226
|
+
if (!rec || typeof rec.rel !== 'string') {
|
|
227
|
+
throw new Error('record is missing string rel');
|
|
228
|
+
}
|
|
229
|
+
out.push(rec);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
reject(new Error(
|
|
232
|
+
`[code-graph] mixdog-graph emitted invalid JSONL at line ${lineNumber}: ${error?.message || error}`,
|
|
233
|
+
));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
226
236
|
}
|
|
227
237
|
resolve(out);
|
|
228
238
|
});
|
|
@@ -128,27 +128,41 @@ export function _attachGraphRuntimeCaches(graph) {
|
|
|
128
128
|
return graph;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
export function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
131
|
+
export function _estimateGraphRetainedBytes(graph) {
|
|
132
|
+
const seen = new WeakSet();
|
|
133
|
+
const estimate = (value) => {
|
|
134
|
+
if (value == null) return 0;
|
|
135
|
+
const type = typeof value;
|
|
136
|
+
if (type === 'string') return 16 + (value.length * 2);
|
|
137
|
+
if (type === 'number' || type === 'bigint') return 8;
|
|
138
|
+
if (type === 'boolean') return 4;
|
|
139
|
+
if (type !== 'object') return 0;
|
|
140
|
+
if (seen.has(value)) return 0;
|
|
141
|
+
seen.add(value);
|
|
142
|
+
if (ArrayBuffer.isView(value)) return 32 + value.byteLength;
|
|
143
|
+
if (value instanceof ArrayBuffer) return 24 + value.byteLength;
|
|
144
|
+
if (value instanceof Map) {
|
|
145
|
+
let bytes = 48 + (value.size * 24);
|
|
146
|
+
for (const [key, item] of value) bytes += estimate(key) + estimate(item);
|
|
147
|
+
return bytes;
|
|
148
|
+
}
|
|
149
|
+
if (value instanceof Set) {
|
|
150
|
+
let bytes = 48 + (value.size * 16);
|
|
151
|
+
for (const item of value) bytes += estimate(item);
|
|
152
|
+
return bytes;
|
|
153
|
+
}
|
|
154
|
+
if (Array.isArray(value)) {
|
|
155
|
+
let bytes = 24 + (value.length * 8);
|
|
156
|
+
for (const item of value) bytes += estimate(item);
|
|
157
|
+
return bytes;
|
|
158
|
+
}
|
|
159
|
+
let bytes = 48;
|
|
160
|
+
for (const [key, item] of Object.entries(value)) {
|
|
161
|
+
bytes += 8 + estimate(key) + estimate(item);
|
|
162
|
+
}
|
|
163
|
+
return bytes;
|
|
164
|
+
};
|
|
165
|
+
return estimate(graph);
|
|
152
166
|
}
|
|
153
167
|
|
|
154
168
|
export function _clearGraphRuntimeCaches(graph) {
|