mixdog 0.9.126 → 0.9.128
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/scripts/prepare-native-assets.mjs +78 -0
- package/scripts/tool-overhead-microbench.mjs +4 -4
- package/scripts/tool-stress.mjs +1 -1
- package/src/rules/shared/01-tool.md +5 -4
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +49 -40
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +54 -1
- package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +34 -104
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +23 -42
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +31 -88
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +88 -11
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +47 -128
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +259 -84
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +8 -2
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-mode-tool.mjs +35 -30
- package/src/runtime/agent/orchestrator/tools/builtin/read-open.mjs +23 -22
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +38 -31
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +15 -15
- package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +9 -9
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +22 -18
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +28 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +3 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +11 -0
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +3 -0
- package/src/runtime/shared/background-tasks.mjs +6 -19
- package/src/runtime/shared/native-tool-paths.mjs +21 -0
- package/src/runtime/shared/tool-card-model.mjs +7 -1
- package/src/session-runtime/prewarm.mjs +13 -0
- package/src/standalone/session-runtime-pool.mjs +12 -8
- package/src/tui/app/transcript-row-estimate.mjs +3 -9
- package/src/tui/dist/index.mjs +4240 -4242
- package/src/tui/session/agent-envelope.mjs +18 -1
- package/src/tui/session/session-api-ext.mjs +6 -1
- package/src/tui/session/session-api.mjs +7 -3
- package/src/tui/session/session-flow.mjs +21 -4
- package/src/tui/session/tui-steering-persist.mjs +28 -3
- package/src/tui/session-local.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/cwd-utils.mjs +0 -37
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.128",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone mixdog coding-agent CLI/TUI workspace.",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"vendor/"
|
|
44
44
|
],
|
|
45
45
|
"scripts": {
|
|
46
|
-
"postinstall": "node scripts/prune-embedding-runtime.mjs",
|
|
46
|
+
"postinstall": "node scripts/prune-embedding-runtime.mjs && node scripts/prepare-native-assets.mjs",
|
|
47
47
|
"prepack": "npm run build:tui",
|
|
48
48
|
"prepublishOnly": "node -e \"if(!process.env.CI){console.error('local npm publish is disabled — run npm run release:patch');process.exit(1)}\"",
|
|
49
49
|
"start": "node src/cli.mjs",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"test:shellhardening": "npm run build:spawn:test && node --test scripts/suite-shellhardening-test.mjs scripts/suite-shell-direct-exe-test.mjs",
|
|
62
62
|
"test:providers": "node --test scripts/provider-toolcall-test.mjs scripts/provider-contract-test.mjs scripts/provider-stream-outcome-test.mjs",
|
|
63
63
|
"test:embedding-runtime:warmup": "node scripts/verify-embedding-runtime.mjs --warmup",
|
|
64
|
-
"test:release-assets": "node --check scripts/verify-release-assets.mjs && node --check scripts/release-gate-test.mjs && node --test scripts/release-gate-test.mjs",
|
|
64
|
+
"test:release-assets": "node --check scripts/verify-release-assets.mjs && node --check scripts/release-gate-test.mjs && node --check scripts/prepare-native-assets.mjs && node --test scripts/release-gate-test.mjs scripts/prepare-native-assets-test.mjs",
|
|
65
65
|
"test:native-edit-wire": "node scripts/native-edit-wire-test.mjs",
|
|
66
66
|
"test:release-focused": "npm run test:release-critical && npm run test:compact && npm run test:shellhardening && npm run test:tool-contracts && npm run test:session && npm run test:session-transport",
|
|
67
67
|
"test:release-critical": "npm run test:release-assets && npm run smoke:patch && npm run test:providers",
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
chmod,
|
|
5
|
+
copyFile,
|
|
6
|
+
mkdir,
|
|
7
|
+
mkdtemp,
|
|
8
|
+
rename,
|
|
9
|
+
rm,
|
|
10
|
+
} from 'node:fs/promises';
|
|
11
|
+
import { dirname, join, resolve } from 'node:path';
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
NATIVE_TOOL_FILENAMES,
|
|
16
|
+
packageNativeToolsDir,
|
|
17
|
+
} from '../src/runtime/shared/native-tool-paths.mjs';
|
|
18
|
+
import { ensureGraphBinary } from '../src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs';
|
|
19
|
+
import { ensurePatchBinary } from '../src/runtime/agent/orchestrator/tools/patch-binary-fetcher.mjs';
|
|
20
|
+
import { ensureSpawnBinary } from '../src/runtime/agent/orchestrator/tools/spawn-binary-fetcher.mjs';
|
|
21
|
+
import { ensureTokenAddon } from '../src/runtime/agent/orchestrator/tools/token-addon-fetcher.mjs';
|
|
22
|
+
|
|
23
|
+
const DEFAULT_PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
24
|
+
|
|
25
|
+
export const REQUIRED_NATIVE_INSTALLERS = Object.freeze({
|
|
26
|
+
graph: ensureGraphBinary,
|
|
27
|
+
patch: ensurePatchBinary,
|
|
28
|
+
spawn: ensureSpawnBinary,
|
|
29
|
+
token: ensureTokenAddon,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export async function prepareRequiredNativeAssets({
|
|
33
|
+
packageRoot = DEFAULT_PACKAGE_ROOT,
|
|
34
|
+
installers = REQUIRED_NATIVE_INSTALLERS,
|
|
35
|
+
} = {}) {
|
|
36
|
+
const root = resolve(String(packageRoot || ''));
|
|
37
|
+
const target = packageNativeToolsDir(root);
|
|
38
|
+
const stagingRoot = await mkdtemp(join(root, '.native-tools-install-'));
|
|
39
|
+
const downloadDataDir = join(stagingRoot, 'downloads');
|
|
40
|
+
const stagedToolsDir = join(stagingRoot, 'native-tools');
|
|
41
|
+
try {
|
|
42
|
+
await mkdir(stagedToolsDir, { recursive: true });
|
|
43
|
+
const entries = await Promise.all(
|
|
44
|
+
Object.entries(NATIVE_TOOL_FILENAMES).map(async ([name, fileName]) => {
|
|
45
|
+
const install = installers[name];
|
|
46
|
+
if (typeof install !== 'function') {
|
|
47
|
+
throw new Error(`native asset installer is invalid: ${name}`);
|
|
48
|
+
}
|
|
49
|
+
const installedPath = await install(downloadDataDir);
|
|
50
|
+
if (typeof installedPath !== 'string' || installedPath.length === 0) {
|
|
51
|
+
throw new Error(`native asset installer returned no path: ${name}`);
|
|
52
|
+
}
|
|
53
|
+
const destination = join(stagedToolsDir, fileName);
|
|
54
|
+
await copyFile(installedPath, destination);
|
|
55
|
+
if (process.platform !== 'win32' && name !== 'token') {
|
|
56
|
+
await chmod(destination, 0o755);
|
|
57
|
+
}
|
|
58
|
+
return [name, join(target, fileName)];
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
await rm(target, { recursive: true, force: true });
|
|
62
|
+
await rename(stagedToolsDir, target);
|
|
63
|
+
return Object.fromEntries(entries);
|
|
64
|
+
} finally {
|
|
65
|
+
await rm(stagingRoot, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : '';
|
|
70
|
+
if (invokedPath === import.meta.url) {
|
|
71
|
+
try {
|
|
72
|
+
const prepared = await prepareRequiredNativeAssets();
|
|
73
|
+
console.log(`[native-assets] ready: ${Object.keys(prepared).join(', ')}`);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error(`[native-assets] install failed: ${error?.message || String(error)}`);
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Micro-bench: fixed per-call overhead of the shell tool path vs raw spawn.
|
|
2
|
-
// Usage: node scripts/tool-overhead-microbench.mjs [
|
|
2
|
+
// Usage: node scripts/tool-overhead-microbench.mjs [N]
|
|
3
3
|
// Prints per-call ms for executeBashTool('echo hi') and raw child spawn,
|
|
4
4
|
// so (tool - raw) isolates our tool-layer overhead (policy, wrappers, I/O).
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { executeBashTool } from '../src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs';
|
|
7
7
|
|
|
8
|
-
const shell = process.
|
|
9
|
-
const N = Number(process.argv[
|
|
8
|
+
const shell = process.platform === 'win32' ? 'powershell' : 'bash';
|
|
9
|
+
const N = Number(process.argv[2] || 15);
|
|
10
10
|
|
|
11
11
|
const stats = (arr) => {
|
|
12
12
|
const s = [...arr].sort((a, b) => a - b);
|
|
@@ -30,7 +30,7 @@ const rawOnce = () => new Promise((resolveDone, reject) => {
|
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
const toolOnce = async () => {
|
|
33
|
-
const out = await executeBashTool({ command: 'echo hi'
|
|
33
|
+
const out = await executeBashTool({ command: 'echo hi' }, process.cwd(), {});
|
|
34
34
|
return String(out);
|
|
35
35
|
};
|
|
36
36
|
|
package/scripts/tool-stress.mjs
CHANGED
|
@@ -119,7 +119,7 @@ try {
|
|
|
119
119
|
|
|
120
120
|
// ── Phase D: cancellation under load ─────────────────────────────────────
|
|
121
121
|
const bg = await timed('shell-async', /task_id/, () => executeBuiltinTool('shell', {
|
|
122
|
-
command: 'node -e "setTimeout(()=>{}, 30000)"',
|
|
122
|
+
command: 'node -e "setTimeout(()=>{}, 30000)"', run_in_background: true, timeout_ms: 60_000,
|
|
123
123
|
}, root, { sessionId: 'stress-cancel' }));
|
|
124
124
|
const bgId = (/task_id:\s*(\S+)/.exec(String(bg)) || [])[1];
|
|
125
125
|
if (!bgId) failures.push('async shell did not return task_id');
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
prior work→`recall` (history only, never current local state);
|
|
13
13
|
durable compact English memory→`memory`;
|
|
14
14
|
explicit Project change→`cwd`
|
|
15
|
-
(
|
|
15
|
+
(a shell-local `cd` never changes the Project);
|
|
16
16
|
explicit user-requested conversation reset→`session_manage`.
|
|
17
17
|
Use only named tools present in the current tool surface.
|
|
18
18
|
- Requirements define what must be true; evidence establishes what is true.
|
|
@@ -35,8 +35,9 @@
|
|
|
35
35
|
tool, and launch independent facets together — never split or duplicate a
|
|
36
36
|
facet across tools, mutate merely to widen retrieval, reserve known work, or
|
|
37
37
|
cap fanout.
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
Cover every independent facet of the round in one batch — one best-routed
|
|
39
|
+
call per facet; extra tools on the same facet add cost, not progress.
|
|
40
|
+
Returned output is fully mined
|
|
40
41
|
before the next round. `code_graph references` supplies the declaration and
|
|
41
42
|
scoped usages and ends that facet; values/locations end at the context grep returns; `read`
|
|
42
43
|
covers only what returned spans cannot, as an anchored offset/limit
|
|
@@ -59,7 +60,7 @@
|
|
|
59
60
|
execution, runtime/state operations, calculations, data transformation, file
|
|
60
61
|
generation, or formats unsupported by file tools. Do not use `shell` instead
|
|
61
62
|
of an available file tool for ordinary file-content inspection.
|
|
62
|
-
- Start shell commands
|
|
63
|
+
- Start shell commands in the foreground. Set `run_in_background:true` only when their result is not needed
|
|
63
64
|
immediately and completion notification is sufficient. Tracked sync/async
|
|
64
65
|
commands belong to the current run. Only when the request explicitly requires
|
|
65
66
|
a service to survive after the run exits, detach it at shell level (for
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Source: https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Overlay catalogs (LiteLLM + models.dev) are fetched once per process.
|
|
12
|
+
* Disk is a stale-ok fallback when the remote fetch fails. On fetch
|
|
13
|
+
* failure with no disk copy, providers keep whatever metadata their
|
|
14
|
+
* native endpoint exposed (usually nothing beyond the id).
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { existsSync, readFileSync } from 'fs';
|
|
@@ -25,14 +25,13 @@ import {
|
|
|
25
25
|
|
|
26
26
|
const CATALOG_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
|
|
27
27
|
const CATALOG_CACHE_FILE = 'litellm-catalog.json';
|
|
28
|
-
const CATALOG_TTL_MS = 24 * 60 * 60_000;
|
|
29
28
|
|
|
30
29
|
// Second auto pricing source: models.dev publishes per-PROVIDER model
|
|
31
30
|
// catalogs (cost in $/M) for 140+ providers — including ones LiteLLM does not
|
|
32
31
|
// track yet (e.g. opencode-go). Because it is keyed provider→model, a
|
|
33
32
|
// provider-scoped lookup is collision-free: deepseek-v4-pro under `deepseek`
|
|
34
|
-
// and under `opencode-go` resolve to their own distinct rates. Same
|
|
35
|
-
// + disk
|
|
33
|
+
// and under `opencode-go` resolve to their own distinct rates. Same
|
|
34
|
+
// process-lifetime fetch + disk fallback as the LiteLLM catalog above.
|
|
36
35
|
const MODELSDEV_URL = 'https://models.dev/api.json';
|
|
37
36
|
const MODELSDEV_CACHE_FILE = 'modelsdev-catalog.json';
|
|
38
37
|
|
|
@@ -193,28 +192,28 @@ const PRICING_OVERRIDES = {
|
|
|
193
192
|
|
|
194
193
|
let _memCache = null;
|
|
195
194
|
let _memCacheAt = 0;
|
|
195
|
+
// Disk warm must not count as "fetched this process" — otherwise a sync
|
|
196
|
+
// lookup before startup refresh would pin a stale overlay for the whole run.
|
|
197
|
+
let _catalogFetchedRemote = false;
|
|
196
198
|
// Single-flight: concurrent loadCatalog callers share the same in-flight
|
|
197
|
-
// Promise so a cold
|
|
199
|
+
// Promise so a cold process only triggers one remote fetch.
|
|
198
200
|
let _loadPromise = null;
|
|
199
201
|
|
|
200
202
|
function cachePath() {
|
|
201
203
|
return join(getPluginData(), CATALOG_CACHE_FILE);
|
|
202
204
|
}
|
|
203
205
|
|
|
204
|
-
|
|
205
|
-
// Disk cache first
|
|
206
|
+
function readDiskCatalog(filePath) {
|
|
206
207
|
try {
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
} catch { /* fall through */ }
|
|
217
|
-
// Remote fetch
|
|
208
|
+
if (!existsSync(filePath)) return null;
|
|
209
|
+
const raw = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
210
|
+
return raw?.data ? raw : null;
|
|
211
|
+
} catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function _loadCatalogImpl(fetchFn = fetch) {
|
|
218
217
|
try {
|
|
219
218
|
const res = await fetchFn(CATALOG_URL, { signal: AbortSignal.timeout(10_000) });
|
|
220
219
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
@@ -224,14 +223,23 @@ async function _loadCatalogImpl(fetchFn = fetch, force = false) {
|
|
|
224
223
|
} catch { /* cache is best-effort */ }
|
|
225
224
|
_memCache = data;
|
|
226
225
|
_memCacheAt = Date.now();
|
|
226
|
+
_catalogFetchedRemote = true;
|
|
227
227
|
return data;
|
|
228
228
|
} catch (err) {
|
|
229
229
|
process.stderr.write(`[model-catalog] fetch failed: ${err.message}\n`);
|
|
230
|
+
const raw = readDiskCatalog(cachePath());
|
|
231
|
+
if (raw?.data) {
|
|
232
|
+
_memCache = raw.data;
|
|
233
|
+
_memCacheAt = raw.fetchedAt || Date.now();
|
|
234
|
+
_catalogFetchedRemote = true;
|
|
235
|
+
return _memCache;
|
|
236
|
+
}
|
|
230
237
|
if (fetchFn !== fetch) {
|
|
231
238
|
_memCache = _memCache || {};
|
|
232
239
|
_memCacheAt = Date.now();
|
|
240
|
+
_catalogFetchedRemote = true;
|
|
233
241
|
}
|
|
234
|
-
return {};
|
|
242
|
+
return _memCache || {};
|
|
235
243
|
}
|
|
236
244
|
}
|
|
237
245
|
|
|
@@ -250,9 +258,9 @@ export async function loadCatalog({ fetchFn, force = false } = {}) {
|
|
|
250
258
|
if (typeof fetchFn === 'function' && fetchFn !== fetch) {
|
|
251
259
|
return _loadCatalogInjected(fetchFn);
|
|
252
260
|
}
|
|
253
|
-
if (!force &&
|
|
261
|
+
if (!force && _catalogFetchedRemote && _memCache) return _memCache;
|
|
254
262
|
if (_loadPromise) return _loadPromise;
|
|
255
|
-
_loadPromise = _loadCatalogImpl(fetchFn
|
|
263
|
+
_loadPromise = _loadCatalogImpl(fetchFn).finally(() => { _loadPromise = null; });
|
|
256
264
|
return _loadPromise;
|
|
257
265
|
}
|
|
258
266
|
|
|
@@ -271,21 +279,11 @@ function warmFromDiskSync() {
|
|
|
271
279
|
let _mdCache = null;
|
|
272
280
|
let _mdCacheAt = 0;
|
|
273
281
|
let _mdLoadPromise = null;
|
|
282
|
+
let _mdFetchedRemote = false;
|
|
274
283
|
function mdCachePath() {
|
|
275
284
|
return join(getPluginData(), MODELSDEV_CACHE_FILE);
|
|
276
285
|
}
|
|
277
|
-
async function _loadModelsDevImpl(fetchFn = fetch
|
|
278
|
-
try {
|
|
279
|
-
if (force) throw new Error('forced refresh');
|
|
280
|
-
if (existsSync(mdCachePath())) {
|
|
281
|
-
const raw = JSON.parse(readFileSync(mdCachePath(), 'utf-8'));
|
|
282
|
-
if (raw?.fetchedAt && (Date.now() - raw.fetchedAt) < CATALOG_TTL_MS && raw.data) {
|
|
283
|
-
_mdCache = raw.data;
|
|
284
|
-
_mdCacheAt = raw.fetchedAt;
|
|
285
|
-
return _mdCache;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
} catch { /* fall through to remote */ }
|
|
286
|
+
async function _loadModelsDevImpl(fetchFn = fetch) {
|
|
289
287
|
try {
|
|
290
288
|
const res = await fetchFn(MODELSDEV_URL, { signal: AbortSignal.timeout(10_000) });
|
|
291
289
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
@@ -295,12 +293,21 @@ async function _loadModelsDevImpl(fetchFn = fetch, force = false) {
|
|
|
295
293
|
} catch { /* cache is best-effort */ }
|
|
296
294
|
_mdCache = data;
|
|
297
295
|
_mdCacheAt = Date.now();
|
|
296
|
+
_mdFetchedRemote = true;
|
|
298
297
|
return data;
|
|
299
298
|
} catch (err) {
|
|
300
299
|
process.stderr.write(`[model-catalog] models.dev fetch failed: ${err.message}\n`);
|
|
300
|
+
const raw = readDiskCatalog(mdCachePath());
|
|
301
|
+
if (raw?.data) {
|
|
302
|
+
_mdCache = raw.data;
|
|
303
|
+
_mdCacheAt = raw.fetchedAt || Date.now();
|
|
304
|
+
_mdFetchedRemote = true;
|
|
305
|
+
return _mdCache;
|
|
306
|
+
}
|
|
301
307
|
if (fetchFn !== fetch) {
|
|
302
308
|
_mdCache = _mdCache || {};
|
|
303
309
|
_mdCacheAt = Date.now();
|
|
310
|
+
_mdFetchedRemote = true;
|
|
304
311
|
}
|
|
305
312
|
return _mdCache || {};
|
|
306
313
|
}
|
|
@@ -319,9 +326,9 @@ export async function loadModelsDevCatalog({ fetchFn, force = false } = {}) {
|
|
|
319
326
|
if (typeof fetchFn === 'function' && fetchFn !== fetch) {
|
|
320
327
|
return _loadModelsDevInjected(fetchFn);
|
|
321
328
|
}
|
|
322
|
-
if (!force &&
|
|
329
|
+
if (!force && _mdFetchedRemote && _mdCache) return _mdCache;
|
|
323
330
|
if (_mdLoadPromise) return _mdLoadPromise;
|
|
324
|
-
_mdLoadPromise = _loadModelsDevImpl(fetchFn
|
|
331
|
+
_mdLoadPromise = _loadModelsDevImpl(fetchFn).finally(() => { _mdLoadPromise = null; });
|
|
325
332
|
return _mdLoadPromise;
|
|
326
333
|
}
|
|
327
334
|
function warmModelsDevFromDiskSync() {
|
|
@@ -598,13 +605,15 @@ export async function enrichModels(models, { fetchFn, force = false } = {}) {
|
|
|
598
605
|
/**
|
|
599
606
|
* Force-refresh the catalog by ignoring cached data and re-fetching.
|
|
600
607
|
* Exposed so a user-initiated "refresh catalog" action in the UI can
|
|
601
|
-
* bypass the
|
|
608
|
+
* bypass the process-lifetime overlay cache.
|
|
602
609
|
*/
|
|
603
610
|
export async function refreshCatalog() {
|
|
604
611
|
_memCache = null;
|
|
605
612
|
_memCacheAt = 0;
|
|
613
|
+
_catalogFetchedRemote = false;
|
|
606
614
|
_mdCache = null;
|
|
607
615
|
_mdCacheAt = 0;
|
|
616
|
+
_mdFetchedRemote = false;
|
|
608
617
|
try {
|
|
609
618
|
if (existsSync(cachePath())) {
|
|
610
619
|
const fs = await import('fs');
|
|
@@ -626,7 +635,7 @@ export async function warmModelMetadataCatalogs() {
|
|
|
626
635
|
return litellm;
|
|
627
636
|
}
|
|
628
637
|
|
|
629
|
-
/** Fire-and-forget warm of both
|
|
638
|
+
/** Fire-and-forget warm of both overlay catalogs (remote once per process). */
|
|
630
639
|
export async function warmCatalogsInBackground() {
|
|
631
640
|
try {
|
|
632
641
|
await Promise.all([loadCatalog(), loadModelsDevCatalog()]);
|
|
@@ -23,6 +23,53 @@ import { preDispatchDenyForSession, routeWebFetchCall } from './pre-dispatch-den
|
|
|
23
23
|
import { runWithToolExecutionOwner } from '../../../../shared/tool-execution-owner.mjs';
|
|
24
24
|
import { runWithLocalSearchTelemetry } from '../../tools/builtin/local-search-telemetry.mjs';
|
|
25
25
|
|
|
26
|
+
const READ_ONLY_IO_TOOL_NAMES = new Set([
|
|
27
|
+
'read', 'head', 'tail', 'wc', 'summary', 'hex',
|
|
28
|
+
'grep', 'glob', 'find', 'find_files', 'list', 'tree',
|
|
29
|
+
]);
|
|
30
|
+
const READ_ONLY_IO_TIMEOUT_MS = 20_000;
|
|
31
|
+
|
|
32
|
+
function readOnlyIoTimeoutMs() {
|
|
33
|
+
const raw = String(process.env.MIXDOG_IO_TOOL_TIMEOUT_MS ?? '').trim();
|
|
34
|
+
if (raw === '0') return 0;
|
|
35
|
+
const configured = Number(raw);
|
|
36
|
+
return Number.isFinite(configured) && configured > 0
|
|
37
|
+
? Math.max(1, Math.floor(configured))
|
|
38
|
+
: READ_ONLY_IO_TIMEOUT_MS;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function runReadOnlyIoWithDeadline(name, parentSignal, run) {
|
|
42
|
+
if (!READ_ONLY_IO_TOOL_NAMES.has(name)) return await run(parentSignal || null);
|
|
43
|
+
const timeoutMs = readOnlyIoTimeoutMs();
|
|
44
|
+
if (timeoutMs <= 0) return await run(parentSignal || null);
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
let rejectAbort;
|
|
47
|
+
const aborted = new Promise((_, reject) => { rejectAbort = reject; });
|
|
48
|
+
const abortWith = (reason) => {
|
|
49
|
+
if (controller.signal.aborted) return;
|
|
50
|
+
const error = reason instanceof Error ? reason : new Error(String(reason || `tool "${name}" aborted`));
|
|
51
|
+
rejectAbort(error);
|
|
52
|
+
controller.abort(error);
|
|
53
|
+
};
|
|
54
|
+
const onParentAbort = () => abortWith(parentSignal?.reason);
|
|
55
|
+
if (parentSignal?.aborted) onParentAbort();
|
|
56
|
+
else parentSignal?.addEventListener?.('abort', onParentAbort, { once: true });
|
|
57
|
+
const timeoutError = new Error(`read-only I/O tool "${name}" timed out after ${timeoutMs}ms; work was cancelled`);
|
|
58
|
+
timeoutError.code = 'READ_ONLY_IO_TIMEOUT';
|
|
59
|
+
const timer = setTimeout(() => abortWith(timeoutError), timeoutMs);
|
|
60
|
+
try {
|
|
61
|
+
return await Promise.race([
|
|
62
|
+
Promise.resolve().then(() => run(controller.signal)),
|
|
63
|
+
aborted,
|
|
64
|
+
]);
|
|
65
|
+
} finally {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
parentSignal?.removeEventListener?.('abort', onParentAbort);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export { runReadOnlyIoWithDeadline as _runReadOnlyIoWithDeadlineForTest };
|
|
72
|
+
|
|
26
73
|
let codeGraphRuntimePromise = null;
|
|
27
74
|
async function executeCodeGraphToolLazy(name, args, cwd, signal = null, options = {}) {
|
|
28
75
|
codeGraphRuntimePromise ??= import('../../tools/code-graph.mjs');
|
|
@@ -166,7 +213,12 @@ async function executeToolOwned(name, args, cwd, callerSessionId, sessionRef, ex
|
|
|
166
213
|
: sessionRef?.afterToolHook;
|
|
167
214
|
const deferredPrep = prepareDeferredToolCallThrough(sessionRef, name, args);
|
|
168
215
|
if (deferredPrep?.deny) return deferredPrep.deny;
|
|
169
|
-
const __result = await (async () => {
|
|
216
|
+
const __result = await runReadOnlyIoWithDeadline(name, executeOpts.signal || null, async (deadlineSignal) => {
|
|
217
|
+
if (deadlineSignal !== executeOpts.signal) {
|
|
218
|
+
executeOpts = { ...executeOpts, signal: deadlineSignal };
|
|
219
|
+
completionToolOpts.signal = deadlineSignal;
|
|
220
|
+
}
|
|
221
|
+
return await (async () => {
|
|
170
222
|
if (name === 'Skill') {
|
|
171
223
|
return viewSkill(cwd, args?.name);
|
|
172
224
|
}
|
|
@@ -238,6 +290,7 @@ async function executeToolOwned(name, args, cwd, callerSessionId, sessionRef, ex
|
|
|
238
290
|
}
|
|
239
291
|
return formatUnknownBuiltinToolMessage(name, args, 'tool');
|
|
240
292
|
})();
|
|
293
|
+
});
|
|
241
294
|
if (typeof afterToolHook === 'function') {
|
|
242
295
|
try {
|
|
243
296
|
// Tool outcome metadata is runtime-internal. Hooks receive the same
|
|
@@ -13,6 +13,7 @@ import { Worker } from 'node:worker_threads';
|
|
|
13
13
|
import { getPluginData } from '../config.mjs';
|
|
14
14
|
import { ensureTokenAddon, findCachedTokenAddon } from '../tools/token-addon-fetcher.mjs';
|
|
15
15
|
import { safeIpcSend } from '../../../shared/safe-ipc-send.mjs';
|
|
16
|
+
import { packageNativeToolPath } from '../../../shared/native-tool-paths.mjs';
|
|
16
17
|
|
|
17
18
|
const PLUGIN_ROOT = process.env.MIXDOG_ROOT
|
|
18
19
|
|| pathResolve(pathDirname(fileURLToPath(import.meta.url)), '../../../../..');
|
|
@@ -51,6 +52,7 @@ function _resolveAddon() {
|
|
|
51
52
|
String(process.env.MIXDOG_TOKEN_NATIVE_ADDON || '').trim() || null,
|
|
52
53
|
String(process.env.MIXDOG_TOKEN_NATIVE_BIN || '').trim() || null,
|
|
53
54
|
LOCAL_ADDON,
|
|
55
|
+
packageNativeToolPath('token'),
|
|
54
56
|
].filter(Boolean);
|
|
55
57
|
_addonPath = candidates.find((candidate) => {
|
|
56
58
|
try { return existsSync(candidate); } catch { return false; }
|