mixdog 0.9.127 → 0.9.129
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/app.mjs +2 -0
- package/src/headless-command.mjs +1 -1
- package/src/headless-exec.mjs +14 -0
- 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/agent-loop.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/compact/handoff.mjs +278 -25
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +26 -9
- 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 +25 -43
- 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 +255 -83
- 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/runtime-capabilities.mjs +8 -1
- 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/env-scrub.mjs +27 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +6 -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.129",
|
|
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');
|
package/src/app.mjs
CHANGED
package/src/headless-command.mjs
CHANGED
|
@@ -4,7 +4,7 @@ const FLAG_OPTIONS = new Set([
|
|
|
4
4
|
'--web-search', '--memory', '--json',
|
|
5
5
|
]);
|
|
6
6
|
const EXEC_UNSUPPORTED_FLAGS = new Set([
|
|
7
|
-
'--readonly', '--remote', '--onboarding',
|
|
7
|
+
'--readonly', '--remote', '--onboarding',
|
|
8
8
|
]);
|
|
9
9
|
const HEADLESS_WORKFLOW_ERROR = 'option --workflow is not supported for mixdog exec';
|
|
10
10
|
|
package/src/headless-exec.mjs
CHANGED
|
@@ -90,6 +90,8 @@ function createJsonLifecycle({
|
|
|
90
90
|
effort,
|
|
91
91
|
fast,
|
|
92
92
|
cwd,
|
|
93
|
+
webSearch = false,
|
|
94
|
+
memory = false,
|
|
93
95
|
}) {
|
|
94
96
|
let threadId = `exec_${randomUUID().replace(/-/g, '')}`;
|
|
95
97
|
const turnId = 'turn_1';
|
|
@@ -143,6 +145,8 @@ function createJsonLifecycle({
|
|
|
143
145
|
tool_mode: 'full',
|
|
144
146
|
approval_mode: 'implicit',
|
|
145
147
|
delegation: false,
|
|
148
|
+
web_search: webSearch === true,
|
|
149
|
+
memory: memory === true,
|
|
146
150
|
},
|
|
147
151
|
}, turnStartedAt);
|
|
148
152
|
emit({
|
|
@@ -540,6 +544,8 @@ export async function runHeadlessExec({
|
|
|
540
544
|
model,
|
|
541
545
|
effort,
|
|
542
546
|
fast,
|
|
547
|
+
webSearch = false,
|
|
548
|
+
memory = false,
|
|
543
549
|
json = false,
|
|
544
550
|
cwd = process.cwd(),
|
|
545
551
|
write = (text) => stdout.write(text),
|
|
@@ -571,6 +577,8 @@ export async function runHeadlessExec({
|
|
|
571
577
|
effort,
|
|
572
578
|
fast,
|
|
573
579
|
cwd,
|
|
580
|
+
webSearch,
|
|
581
|
+
memory,
|
|
574
582
|
}) : null;
|
|
575
583
|
let boundary = null;
|
|
576
584
|
let runtime = null;
|
|
@@ -602,6 +610,12 @@ export async function runHeadlessExec({
|
|
|
602
610
|
const createRuntime = runtimeFactory || (
|
|
603
611
|
await import('./mixdog-session-runtime.mjs')
|
|
604
612
|
).createMixdogSessionRuntime;
|
|
613
|
+
// Headless defaults: web research and memory tools stay OFF unless the
|
|
614
|
+
// caller opts in via --web-search / --memory. Delegation is already
|
|
615
|
+
// disallowed below, completing the solo surface. The per-process
|
|
616
|
+
// MIXDOG_FEATURE_* overrides are the runtime's canonical switches.
|
|
617
|
+
process.env.MIXDOG_FEATURE_WEB_SEARCH = webSearch === true ? '1' : '0';
|
|
618
|
+
process.env.MIXDOG_FEATURE_MEMORY = memory === true ? '1' : '0';
|
|
605
619
|
runtime = await createRuntime({
|
|
606
620
|
provider,
|
|
607
621
|
model,
|
|
@@ -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()]);
|
|
@@ -1009,6 +1009,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1009
1009
|
// server-side cache prefix stable.
|
|
1010
1010
|
const _assistantTurnMsg = attachAssistantTranscriptMetadata({
|
|
1011
1011
|
role: 'assistant',
|
|
1012
|
+
createdAt: Date.now(),
|
|
1012
1013
|
// Sub-agent tool-call turns carry only mid-turn preamble in
|
|
1013
1014
|
// response.content (the real result rides the later final-answer
|
|
1014
1015
|
// turn). Blank it so it never accumulates as input tokens.
|