mixdog 0.9.85 → 0.9.87
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 +7 -1
- package/package.json +4 -3
- package/src/defaults/agents.json +12 -0
- package/src/defaults/skills/setup/SKILL.md +44 -12
- package/src/help.mjs +40 -9
- package/src/lib/keychain-cjs.cjs +11 -1
- package/src/rules/agent/43-title-agent.md +22 -0
- package/src/rules/shared/01-tool.md +3 -3
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +4 -2
- package/src/runtime/agent/orchestrator/mcp/client.mjs +24 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +84 -3
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +125 -16
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +73 -45
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +15 -18
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +135 -9
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +31 -12
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +45 -6
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/token-bpe.mjs +42 -0
- package/src/runtime/agent/orchestrator/session/token-native.mjs +186 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +15 -5
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +286 -0
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +152 -18
- package/src/runtime/agent/orchestrator/tools/token-binary-fetcher.mjs +201 -0
- package/src/runtime/agent/orchestrator/tools/token-manifest.json +26 -0
- package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +3 -3
- package/src/runtime/media/adapters/gemini-video.mjs +1 -1
- package/src/runtime/media/renditions.mjs +1 -1
- package/src/runtime/media/store.mjs +1 -1
- package/src/runtime/memory/lib/memory-action-handlers.mjs +4 -1
- package/src/runtime/memory/tool-defs.mjs +19 -3
- package/src/runtime/shared/atomic-file.mjs +25 -14
- package/src/runtime/shared/automation-attachments.mjs +3 -3
- package/src/runtime/shared/child-guardian.mjs +15 -5
- package/src/runtime/shared/child-spawn-gate.mjs +6 -12
- package/src/runtime/shared/resource-admission.mjs +7 -3
- package/src/runtime/shared/turn-snapshot.mjs +0 -3
- package/src/session-runtime/lifecycle-api.mjs +49 -9
- package/src/session-runtime/prewarm.mjs +35 -0
- package/src/session-runtime/provider-auth-api.mjs +8 -0
- package/src/session-runtime/provider-usage.mjs +30 -5
- package/src/session-runtime/remote-control.mjs +8 -47
- package/src/session-runtime/remote-transcript.mjs +3 -7
- package/src/session-runtime/route-preparation.mjs +24 -0
- package/src/session-runtime/runtime-core.mjs +286 -548
- package/src/session-runtime/session-lifecycle.mjs +378 -0
- package/src/session-runtime/session-turn-api.mjs +9 -2
- package/src/session-runtime/workflow-agents-api.mjs +3 -0
- package/src/standalone/agent-shard/shard-child.mjs +300 -0
- package/src/standalone/agent-shard/shard-pool.mjs +443 -0
- package/src/standalone/agent-tool/job-views.mjs +329 -0
- package/src/standalone/agent-tool/spawn-flow.mjs +629 -0
- package/src/standalone/agent-tool/tag-registry.mjs +340 -0
- package/src/standalone/agent-tool.mjs +112 -941
- package/src/standalone/channel-daemon-transport.mjs +126 -185
- package/src/standalone/channel-daemon.mjs +9 -10
- package/src/standalone/usage-dashboard.mjs +23 -1
- package/src/tui/App.jsx +362 -2569
- package/src/tui/app/app-view.jsx +504 -0
- package/src/tui/app/channel-pickers.mjs +3 -8
- package/src/tui/app/create-app-pickers.mjs +313 -0
- package/src/tui/app/prompt-submit.mjs +501 -0
- package/src/tui/app/route-pickers.mjs +117 -152
- package/src/tui/app/settings-picker.mjs +90 -89
- package/src/tui/app/shell-layout.mjs +563 -0
- package/src/tui/app/slash-commands.mjs +7 -7
- package/src/tui/app/slash-dispatch.mjs +0 -45
- package/src/tui/app/usage-context-panels.mjs +288 -0
- package/src/tui/app/use-copy-selection.mjs +56 -0
- package/src/tui/app/use-global-key-input.mjs +151 -0
- package/src/tui/app/use-pasted-buffers.mjs +115 -0
- package/src/tui/app/use-prompt-draft-flow.mjs +167 -0
- package/src/tui/app/use-prompt-hint.mjs +58 -0
- package/src/tui/app/use-prompt-queue-history.mjs +100 -0
- package/src/tui/app/use-terminal-chrome.mjs +72 -0
- package/src/tui/app/use-transcript-activity.mjs +131 -0
- package/src/tui/app/use-welcome-prompt-hint.mjs +96 -0
- package/src/tui/components/StatusLine.jsx +1 -1
- package/src/tui/components/tool-execution/ResultBody.jsx +1 -1
- package/src/tui/dist/index.mjs +9857 -9159
- package/src/tui/engine/session-api-ext.mjs +47 -9
- package/src/tui/engine.mjs +7 -1
- package/src/tui/figures.mjs +0 -1
- package/src/ui/statusline-format.mjs +0 -1
- package/src/hooks/lib/permission-evaluator.cjs +0 -24
- package/src/lib/config-cjs.cjs +0 -61
- package/src/runtime/shared/launcher-control.mjs +0 -258
- package/src/runtime/shared/workspace-router.mjs +0 -259
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// token-binary-fetcher.mjs — fetches the prebuilt mixdog-token native binary
|
|
2
|
+
// from the GitHub release manifest. Token counting has an in-process WASM
|
|
3
|
+
// fallback, so callers treat fetch failures as a soft degrade (never a tool
|
|
4
|
+
// error). Caches under <dataDir>/token-bin/. Mirrors graph-binary-fetcher.mjs.
|
|
5
|
+
//
|
|
6
|
+
// Public API:
|
|
7
|
+
// ensureTokenBinary(dataDir) -> absolute path to the verified binary.
|
|
8
|
+
// Throws on no-asset / download / verify failure.
|
|
9
|
+
// findCachedTokenBinary(dataDir) -> path | null (sync, no network).
|
|
10
|
+
// Both accept an optional dependency object used by deterministic tests:
|
|
11
|
+
// { bundledManifest, download }.
|
|
12
|
+
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import {
|
|
15
|
+
chmodSync, createWriteStream, existsSync, mkdirSync,
|
|
16
|
+
readFileSync, readdirSync, renameSync, rmSync,
|
|
17
|
+
} from 'node:fs';
|
|
18
|
+
import { readFile } from 'node:fs/promises';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { pipeline } from 'node:stream/promises';
|
|
22
|
+
|
|
23
|
+
const BUNDLED_MANIFEST_PATH = fileURLToPath(new URL('./token-manifest.json', import.meta.url));
|
|
24
|
+
const MANIFEST_URL = 'https://raw.githubusercontent.com/tribgames/mixdog/main/src/runtime/agent/orchestrator/tools/token-manifest.json';
|
|
25
|
+
|
|
26
|
+
function binSuffix() {
|
|
27
|
+
return process.platform === 'win32' ? '.exe' : '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function platformKey() {
|
|
31
|
+
const os = process.platform === 'win32' ? 'win32' : process.platform;
|
|
32
|
+
return `${os}-${process.arch}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function tokenBinDir(dataDir) {
|
|
36
|
+
return join(dataDir, 'token-bin');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readJson(path) {
|
|
40
|
+
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function manifestVersion(manifest) {
|
|
44
|
+
const value = String(manifest?.version || '');
|
|
45
|
+
if (!/^\d+\.\d+\.\d+$/.test(value)) return null;
|
|
46
|
+
return value.split('.').map(Number);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function compareManifestVersions(a, b) {
|
|
50
|
+
const av = manifestVersion(a);
|
|
51
|
+
const bv = manifestVersion(b);
|
|
52
|
+
if (!av || !bv) return null;
|
|
53
|
+
for (let i = 0; i < 3; i++) {
|
|
54
|
+
if (av[i] !== bv[i]) return av[i] > bv[i] ? 1 : -1;
|
|
55
|
+
}
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validSha256(value) {
|
|
60
|
+
return typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function validCachedUpgrade(manifest, pkey) {
|
|
64
|
+
if (!manifestVersion(manifest)) return false;
|
|
65
|
+
const asset = manifest.assets?.[pkey];
|
|
66
|
+
if (!asset || !validSha256(asset.sha256) || typeof asset.url !== 'string') return false;
|
|
67
|
+
const expectedUrl = `https://github.com/tribgames/mixdog/releases/download/token-v${manifest.version}`
|
|
68
|
+
+ `/mixdog-token-${pkey}${binSuffix()}`;
|
|
69
|
+
return asset.url === expectedUrl;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function bundledManifest(options) {
|
|
73
|
+
if (options.bundledManifest) return options.bundledManifest;
|
|
74
|
+
return existsSync(BUNDLED_MANIFEST_PATH) ? readJson(BUNDLED_MANIFEST_PATH) : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function selectLocalManifest(dataDir, options = {}) {
|
|
78
|
+
const bundled = bundledManifest(options);
|
|
79
|
+
const cached = join(tokenBinDir(dataDir), 'manifest.json');
|
|
80
|
+
const cachedManifest = existsSync(cached) ? readJson(cached) : null;
|
|
81
|
+
if (bundled) {
|
|
82
|
+
// The installed manifest is the minimum policy. A cache may advance it,
|
|
83
|
+
// but only with a strict newer semver and a trusted, fully hashed asset.
|
|
84
|
+
if (compareManifestVersions(cachedManifest, bundled) === 1
|
|
85
|
+
&& validCachedUpgrade(cachedManifest, platformKey())) {
|
|
86
|
+
return cachedManifest;
|
|
87
|
+
}
|
|
88
|
+
return bundled;
|
|
89
|
+
}
|
|
90
|
+
return validCachedUpgrade(cachedManifest, platformKey()) ? cachedManifest : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function loadManifest(dataDir, options = {}) {
|
|
94
|
+
const local = selectLocalManifest(dataDir, options);
|
|
95
|
+
if (local) return local;
|
|
96
|
+
const fetchFn = options.fetch || fetch;
|
|
97
|
+
const res = await fetchFn(MANIFEST_URL, { signal: AbortSignal.timeout(30_000) });
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
throw new Error(`[token-fetcher] manifest fetch failed: ${res.status} ${res.statusText}`);
|
|
100
|
+
}
|
|
101
|
+
return res.json();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function sha256File(filePath) {
|
|
105
|
+
return createHash('sha256').update(await readFile(filePath)).digest('hex');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function downloadWithRetry(url, destPath) {
|
|
109
|
+
const delays = [1000, 3000, 9000];
|
|
110
|
+
let lastErr;
|
|
111
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
112
|
+
try {
|
|
113
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(180_000) });
|
|
114
|
+
if (res.status >= 400 && res.status < 500) {
|
|
115
|
+
throw new Error(`[token-fetcher] asset HTTP ${res.status} (terminal) — ${url}`);
|
|
116
|
+
}
|
|
117
|
+
if (!res.ok) throw new Error(`[token-fetcher] asset HTTP ${res.status} — ${url}`);
|
|
118
|
+
await pipeline(res.body, createWriteStream(destPath));
|
|
119
|
+
return;
|
|
120
|
+
} catch (err) {
|
|
121
|
+
lastErr = err;
|
|
122
|
+
if (String(err?.message || '').includes('(terminal)')) throw err;
|
|
123
|
+
if (attempt < 3) {
|
|
124
|
+
process.stderr.write(`[token-fetcher] download attempt ${attempt + 1} failed (${err?.message}), retrying in ${delays[attempt]}ms…\n`);
|
|
125
|
+
await new Promise((r) => setTimeout(r, delays[attempt]));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
throw lastErr;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function gcTokenBin(dir, keepFile) {
|
|
133
|
+
try {
|
|
134
|
+
for (const name of readdirSync(dir)) {
|
|
135
|
+
if (name === 'manifest.json' || name === keepFile) continue;
|
|
136
|
+
if (name.startsWith('mixdog-token')) {
|
|
137
|
+
try { rmSync(join(dir, name), { force: true }); } catch { /* best-effort */ }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} catch { /* dir may not exist yet */ }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function findCachedTokenBinary(dataDir, options = {}) {
|
|
144
|
+
try {
|
|
145
|
+
const dir = tokenBinDir(dataDir);
|
|
146
|
+
const manifest = selectLocalManifest(dataDir, options);
|
|
147
|
+
const asset = manifest?.assets?.[platformKey()];
|
|
148
|
+
const version = manifestVersion(manifest);
|
|
149
|
+
if (!version || !validSha256(asset?.sha256)) return null;
|
|
150
|
+
const fileName = `mixdog-token-${manifest.version}${binSuffix()}`;
|
|
151
|
+
const hit = join(dir, fileName);
|
|
152
|
+
if (!existsSync(hit)) return null;
|
|
153
|
+
const actual = createHash('sha256').update(readFileSync(hit)).digest('hex');
|
|
154
|
+
return actual === asset.sha256.toLowerCase() ? hit : null;
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let _inflight = null;
|
|
161
|
+
|
|
162
|
+
export function ensureTokenBinary(dataDir, options = {}) {
|
|
163
|
+
if (_inflight) return _inflight;
|
|
164
|
+
_inflight = (async () => {
|
|
165
|
+
const manifest = await loadManifest(dataDir, options);
|
|
166
|
+
const pkey = platformKey();
|
|
167
|
+
const asset = manifest.assets?.[pkey];
|
|
168
|
+
if (!asset || !asset.url || !validSha256(asset.sha256) || !manifestVersion(manifest)) {
|
|
169
|
+
// Unsupported platform/arch, or no token-v release published yet. The
|
|
170
|
+
// caller degrades to the in-process WASM worker, so the message only
|
|
171
|
+
// needs to explain why the accelerator is absent.
|
|
172
|
+
const supported = Object.keys(manifest.assets || {}).join(', ') || '(none)';
|
|
173
|
+
throw new Error(
|
|
174
|
+
`[token-fetcher] no prebuilt mixdog-token binary for platform ${pkey} `
|
|
175
|
+
+ `(token counting degrades to the in-process WASM worker). `
|
|
176
|
+
+ `Manifest platforms: ${supported}. `
|
|
177
|
+
+ `Build it locally: cargo build --release in native/mixdog-token.`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
const version = String(manifest.version || '0');
|
|
181
|
+
const dir = tokenBinDir(dataDir);
|
|
182
|
+
mkdirSync(dir, { recursive: true });
|
|
183
|
+
const fileName = `mixdog-token-${version}${binSuffix()}`;
|
|
184
|
+
const destPath = join(dir, fileName);
|
|
185
|
+
if (existsSync(destPath)) {
|
|
186
|
+
try { if (await sha256File(destPath) === asset.sha256.toLowerCase()) return destPath; } catch { /* re-download */ }
|
|
187
|
+
}
|
|
188
|
+
const tmpPath = `${destPath}.tmp-${process.pid}-${Date.now()}`;
|
|
189
|
+
await (options.download || downloadWithRetry)(asset.url, tmpPath);
|
|
190
|
+
const actual = await sha256File(tmpPath);
|
|
191
|
+
if (actual !== asset.sha256.toLowerCase()) {
|
|
192
|
+
try { rmSync(tmpPath, { force: true }); } catch { /* best-effort */ }
|
|
193
|
+
throw new Error(`[token-fetcher] sha256 mismatch for ${pkey}: expected ${asset.sha256}, got ${actual}`);
|
|
194
|
+
}
|
|
195
|
+
renameSync(tmpPath, destPath);
|
|
196
|
+
if (process.platform !== 'win32') { try { chmodSync(destPath, 0o755); } catch { /* best-effort */ } }
|
|
197
|
+
gcTokenBin(dir, fileName);
|
|
198
|
+
return destPath;
|
|
199
|
+
})().finally(() => { _inflight = null; });
|
|
200
|
+
return _inflight;
|
|
201
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.1.0",
|
|
3
|
+
"_comment": "Synced from immutable token-v release assets.",
|
|
4
|
+
"assets": {
|
|
5
|
+
"darwin-arm64": {
|
|
6
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/token-v0.1.0/mixdog-token-darwin-arm64",
|
|
7
|
+
"sha256": "1d5259803207dd5ace466c506d8db4087460a20c1e3f4f6cc249832a8a996f6b"
|
|
8
|
+
},
|
|
9
|
+
"darwin-x64": {
|
|
10
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/token-v0.1.0/mixdog-token-darwin-x64",
|
|
11
|
+
"sha256": "2212a434a89419ce0a4122ce4647e2feb2b4642e26d3e74e11b4de2819a6c2e3"
|
|
12
|
+
},
|
|
13
|
+
"linux-arm64": {
|
|
14
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/token-v0.1.0/mixdog-token-linux-arm64",
|
|
15
|
+
"sha256": "332839da398050218fbed4b6700a3e34dbdcba800893b9a3ba16ebca3e88b9ee"
|
|
16
|
+
},
|
|
17
|
+
"linux-x64": {
|
|
18
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/token-v0.1.0/mixdog-token-linux-x64",
|
|
19
|
+
"sha256": "ac79201259985705cf378d225a25a779591d1f75fefd626e470b398e3d8fdd9c"
|
|
20
|
+
},
|
|
21
|
+
"win32-x64": {
|
|
22
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/token-v0.1.0/mixdog-token-win32-x64.exe",
|
|
23
|
+
"sha256": "da6e39974811123b3e0c72f539c9e5b424261b66a3f2a30eb0e9cc837036d1ba"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -28,14 +28,14 @@ export function resolveHookRelayUrl(env = process.env) {
|
|
|
28
28
|
return raw || DEFAULT_RELAY_URL;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
function hookIdentityPath() {
|
|
32
32
|
return join(DATA_DIR, "relay-hook-device.json");
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Stable per-install identity (trust-on-first-use at the relay, mirroring
|
|
36
36
|
// the desktop leg). The secret never leaves this machine except toward the
|
|
37
37
|
// relay; the deviceId doubles as the public URL path segment.
|
|
38
|
-
|
|
38
|
+
function loadOrCreateHookIdentity() {
|
|
39
39
|
const path = hookIdentityPath();
|
|
40
40
|
try {
|
|
41
41
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
@@ -58,7 +58,7 @@ export function loadOrCreateHookIdentity() {
|
|
|
58
58
|
return identity;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
function hookPublicBase(relayUrl, deviceId) {
|
|
62
62
|
const url = new URL(relayUrl);
|
|
63
63
|
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
64
64
|
url.pathname = `/hook/${deviceId}`;
|
|
@@ -30,7 +30,7 @@ const EXTENSION_MIME = { '.webp': 'image/webp', '.png': 'image/png' };
|
|
|
30
30
|
const POSTER_TIMEOUT_MS = 20_000;
|
|
31
31
|
const MAX_POSTER_BYTES = 32 * 1024 * 1024;
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
const MEDIA_VARIANTS = Object.keys(SPECS);
|
|
34
34
|
|
|
35
35
|
/** Spec for a variant name, or null when the caller asked for the original. */
|
|
36
36
|
export function renditionSpec(variant) {
|
|
@@ -222,7 +222,7 @@ async function prewarmThumbs(assets) {
|
|
|
222
222
|
await Promise.all(workers).catch(() => {});
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
|
|
225
|
+
function getMediaAsset(id) {
|
|
226
226
|
ensureDirs();
|
|
227
227
|
return readIndex().find((entry) => entry.id === id) || null;
|
|
228
228
|
}
|
|
@@ -877,7 +877,10 @@ export function createMemoryActionHandlers({
|
|
|
877
877
|
return { text: `retro_eval_active: total=${total} archived=${archived} kept=${kept} updated=${updated} merged=${merged} errors=${errors}` }
|
|
878
878
|
}
|
|
879
879
|
|
|
880
|
-
return {
|
|
880
|
+
return {
|
|
881
|
+
text: `unknown memory action: ${action}; valid: core, status. Mutation verbs belong in op.`,
|
|
882
|
+
isError: true,
|
|
883
|
+
}
|
|
881
884
|
}
|
|
882
885
|
|
|
883
886
|
const handleToolCall = createToolCallHandler({ handleSearch, handleMemoryAction })
|
|
@@ -8,12 +8,24 @@ export const TOOL_DEFS = [
|
|
|
8
8
|
name: 'memory',
|
|
9
9
|
title: 'Memory Cycle',
|
|
10
10
|
annotations: { title: 'Memory Cycle', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
11
|
-
description:
|
|
11
|
+
description: [
|
|
12
|
+
'Core-memory mutation/status; recall retrieves.',
|
|
13
|
+
'Mutations use action=core with op; status uses action=status.',
|
|
14
|
+
'Store only durable compact ENGLISH facts/rules/preferences.',
|
|
15
|
+
].join(' '),
|
|
12
16
|
inputSchema: {
|
|
13
17
|
type: 'object',
|
|
14
18
|
properties: {
|
|
15
|
-
action: {
|
|
16
|
-
|
|
19
|
+
action: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
enum: ['core','status'],
|
|
22
|
+
description: 'core for mutations; status otherwise. Mutation verbs belong in op.',
|
|
23
|
+
},
|
|
24
|
+
op: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
enum: ['add','edit','delete','list','candidates','promote','dismiss'],
|
|
27
|
+
description: 'Required for action=core.',
|
|
28
|
+
},
|
|
17
29
|
id: { type: 'number', description: 'Exact memory id.' },
|
|
18
30
|
element: { type: 'string', maxLength: 40, description: 'Memory key/title. Defaults to the first 40 chars of summary. Max 40 chars.' },
|
|
19
31
|
summary: { type: 'string', maxLength: 100, description: 'Memory content: one short English clause, max 100 chars.' },
|
|
@@ -22,6 +34,10 @@ export const TOOL_DEFS = [
|
|
|
22
34
|
confirm: { type: 'string', description: 'Exact confirmation phrase for destructive actions.' },
|
|
23
35
|
project_id: { type: 'string', description: 'Core pool: explicit common or slug. Required for core add only (edit uses the id\'s stored pool); there is no default pool.' },
|
|
24
36
|
},
|
|
37
|
+
anyOf: [
|
|
38
|
+
{ properties: { action: { const: 'status' } }, required: ['action'] },
|
|
39
|
+
{ properties: { action: { const: 'core' } }, required: ['action', 'op'] },
|
|
40
|
+
],
|
|
25
41
|
additionalProperties: false,
|
|
26
42
|
required: ['action'],
|
|
27
43
|
},
|
|
@@ -118,10 +118,15 @@ export function withFileLockSync(lockPath, fn, opts = {}) {
|
|
|
118
118
|
} catch (err) {
|
|
119
119
|
lastErr = err;
|
|
120
120
|
if (!LOCK_WAIT_CODES.has(err?.code)) throw err;
|
|
121
|
-
// try-once
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
121
|
+
// Reclaim is itself try-once: the guard is acquired atomically and never
|
|
122
|
+
// waited on. This lets timeoutMs:0 recover a dead/pidless corpse without
|
|
123
|
+
// weakening its no-sleep contract.
|
|
124
|
+
try {
|
|
125
|
+
if (_tryReclaimStaleLock(lockPath, staleMs)) continue;
|
|
126
|
+
} catch {}
|
|
127
|
+
// try-once semantics: timeoutMs:0 makes one guarded reclaim attempt and
|
|
128
|
+
// never sleeps — live contention throws ELOCKCONTENDED immediately so
|
|
129
|
+
// the main loop is never blocked by Atomics.wait.
|
|
125
130
|
if (timeoutMs <= 0) {
|
|
126
131
|
const contErr = new Error(`atomic lock contended (try-once): ${lockPath} [${_describeLockHolder(lockPath)}]`);
|
|
127
132
|
contErr.code = 'ELOCKCONTENDED';
|
|
@@ -130,9 +135,6 @@ export function withFileLockSync(lockPath, fn, opts = {}) {
|
|
|
130
135
|
}
|
|
131
136
|
// Reclaim only when owner death is positively established. Age, an
|
|
132
137
|
// unreadable owner, or a same-pid foreign token remain contended.
|
|
133
|
-
try {
|
|
134
|
-
if (_tryReclaimStaleLock(lockPath, staleMs)) continue;
|
|
135
|
-
} catch {}
|
|
136
138
|
if (Date.now() >= deadline) break;
|
|
137
139
|
const base = DEFAULT_BACKOFFS_MS[Math.min(attempt, DEFAULT_BACKOFFS_MS.length - 1)];
|
|
138
140
|
const jitter = Math.floor(Math.random() * Math.min(75, Math.max(1, base)));
|
|
@@ -223,14 +225,19 @@ function _lockOwnedBySelf(lockPath) {
|
|
|
223
225
|
//
|
|
224
226
|
// Reclaim decision table (owner = identity recorded in the lock file):
|
|
225
227
|
// owner pid DEAD (ESRCH) → reclaim
|
|
226
|
-
// owner pid NULL/uncertain
|
|
228
|
+
// owner pid NULL/uncertain and lock age >= staleMs → reclaim
|
|
229
|
+
// owner pid NULL/uncertain and lock age < staleMs → back off
|
|
230
|
+
// same-pid foreign token → back off
|
|
227
231
|
// owner token MISMATCH on guarded re-read (new holder took over) → back off
|
|
228
232
|
// owner LIVE → back off regardless of age
|
|
229
233
|
function _tryReclaimStaleLock(lockPath, staleMs) {
|
|
230
|
-
|
|
234
|
+
let initial;
|
|
235
|
+
try { initial = statSync(lockPath); } catch { return false; }
|
|
231
236
|
const owner = _readLockOwner(lockPath);
|
|
232
237
|
const dead = owner.pid !== null && !_ownerIsLive(owner);
|
|
233
|
-
|
|
238
|
+
const pidlessStale = owner.pid === null
|
|
239
|
+
&& Date.now() - (Number(initial.mtimeMs) || 0) >= Math.max(0, staleMs);
|
|
240
|
+
if (!dead && !pidlessStale) return false;
|
|
234
241
|
const reclaim = _tryAcquireReclaimGuard(lockPath, staleMs);
|
|
235
242
|
if (reclaim === null) return false;
|
|
236
243
|
try {
|
|
@@ -243,7 +250,9 @@ function _tryReclaimStaleLock(lockPath, staleMs) {
|
|
|
243
250
|
// corpse — so we never yank a freshly-acquired live lock.
|
|
244
251
|
if (curOwner.pid !== owner.pid || curOwner.token !== owner.token) return false;
|
|
245
252
|
const curDead = curOwner.pid !== null && !_ownerIsLive(curOwner);
|
|
246
|
-
|
|
253
|
+
const curPidlessStale = curOwner.pid === null
|
|
254
|
+
&& Date.now() - (Number(cur.mtimeMs) || 0) >= Math.max(0, staleMs);
|
|
255
|
+
if (curDead || curPidlessStale) {
|
|
247
256
|
try { unlinkSync(lockPath); return true; } catch { return false; }
|
|
248
257
|
}
|
|
249
258
|
return false;
|
|
@@ -307,6 +316,11 @@ export async function withFileLock(lockPath, fn, opts = {}) {
|
|
|
307
316
|
} catch (err) {
|
|
308
317
|
lastErr = err;
|
|
309
318
|
if (!LOCK_WAIT_CODES.has(err?.code)) throw err;
|
|
319
|
+
// The guarded reclaim is non-blocking, so try-once callers can recover
|
|
320
|
+
// stale corpses while still returning immediately for a live holder.
|
|
321
|
+
try {
|
|
322
|
+
if (_tryReclaimStaleLock(lockPath, staleMs)) continue;
|
|
323
|
+
} catch {}
|
|
310
324
|
if (timeoutMs <= 0) {
|
|
311
325
|
const contErr = new Error(`atomic lock contended (try-once): ${lockPath} [${_describeLockHolder(lockPath)}]`);
|
|
312
326
|
contErr.code = 'ELOCKCONTENDED';
|
|
@@ -316,9 +330,6 @@ export async function withFileLock(lockPath, fn, opts = {}) {
|
|
|
316
330
|
// Stale-lock reclaim (shared with the sync variant): dead owner pid,
|
|
317
331
|
// a same-pid-but-foreign-token corpse from a reused pid, or a
|
|
318
332
|
// pidless/empty corpse lock. A live foreign holder is never stolen.
|
|
319
|
-
try {
|
|
320
|
-
if (_tryReclaimStaleLock(lockPath, staleMs)) continue;
|
|
321
|
-
} catch {}
|
|
322
333
|
if (Date.now() >= deadline) break;
|
|
323
334
|
const base = DEFAULT_BACKOFFS_MS[Math.min(attempt, DEFAULT_BACKOFFS_MS.length - 1)];
|
|
324
335
|
const jitter = Math.floor(Math.random() * Math.min(75, Math.max(1, base)));
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* {type:'file'}), so provider media normalization treats them identically.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
const AUTOMATION_ATTACHMENT_KINDS = ['image', 'text', 'pdf'];
|
|
11
11
|
export const MAX_AUTOMATION_ATTACHMENTS = 8;
|
|
12
12
|
// Base64 total across image/pdf items; text totals separately.
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const MAX_AUTOMATION_BINARY_TOTAL = 8_000_000;
|
|
14
|
+
const MAX_AUTOMATION_TEXT_TOTAL = 200_000;
|
|
15
15
|
|
|
16
16
|
/** Validate + strip a stored/list attachments value to the persisted shape (or null). */
|
|
17
17
|
export function normalizeAutomationAttachments(value) {
|
|
@@ -8,6 +8,20 @@ function positiveInt(value) {
|
|
|
8
8
|
return Number.isInteger(n) && n > 0 ? n : null;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export function childGuardianSpawnEnv(env = process.env) {
|
|
12
|
+
return {
|
|
13
|
+
PATH: env.PATH || '',
|
|
14
|
+
SystemRoot: env.SystemRoot || env.WINDIR || '',
|
|
15
|
+
WINDIR: env.WINDIR || env.SystemRoot || '',
|
|
16
|
+
// Desktop's engine runs inside an Electron utility process, where
|
|
17
|
+
// process.execPath points at electron.exe. Without Node mode Electron
|
|
18
|
+
// ignores --eval, starts a full app process, and the intended guardian
|
|
19
|
+
// loop never runs — every completed shell then leaks ~100 MB until the
|
|
20
|
+
// desktop exits.
|
|
21
|
+
ELECTRON_RUN_AS_NODE: '1',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
function guardianScript({ parentPid, childPid, childGroupPid, platform, pollMs, orphanGraceMs, forceGraceMs }) {
|
|
12
26
|
return `
|
|
13
27
|
const { spawnSync } = require('node:child_process');
|
|
@@ -88,11 +102,7 @@ export function startChildGuardian({
|
|
|
88
102
|
}),
|
|
89
103
|
], {
|
|
90
104
|
stdio: 'ignore',
|
|
91
|
-
env:
|
|
92
|
-
PATH: process.env.PATH || '',
|
|
93
|
-
SystemRoot: process.env.SystemRoot || process.env.WINDIR || '',
|
|
94
|
-
WINDIR: process.env.WINDIR || process.env.SystemRoot || '',
|
|
95
|
-
},
|
|
105
|
+
env: childGuardianSpawnEnv(),
|
|
96
106
|
...detachedSpawnOpts,
|
|
97
107
|
});
|
|
98
108
|
guardian.unref?.();
|
|
@@ -3,25 +3,19 @@
|
|
|
3
3
|
// ── Module-global child-spawn semaphore ──────────────────────────────────
|
|
4
4
|
//
|
|
5
5
|
// Single-daemon premise: tool execution is in-process, so a module-level
|
|
6
|
-
// singleton semaphore
|
|
7
|
-
// mixdog-graph, …) across ALL agents/workers in this daemon.
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// deadline stops tripping en masse — WITHOUT serializing: up to the cap run
|
|
11
|
-
// concurrently, only the overflow queues and drains as slots free.
|
|
6
|
+
// singleton semaphore CAN bound the number of concurrent child processes (rg,
|
|
7
|
+
// mixdog-graph, …) across ALL agents/workers in this daemon. By default the
|
|
8
|
+
// gate is UNBOUNDED — parallel waves must never queue behind it — and it only
|
|
9
|
+
// becomes a real cap when MIXDOG_CHILD_SPAWN_MAX_INFLIGHT is set explicitly.
|
|
12
10
|
//
|
|
13
11
|
// IMPORTANT: this is a resource-control knob and is deliberately NOT exposed
|
|
14
12
|
// on any tool JSON schema / tool parameter surface. The only tuning surface
|
|
15
|
-
// is the
|
|
16
|
-
|
|
17
|
-
import os from 'node:os';
|
|
13
|
+
// is the MIXDOG_CHILD_SPAWN_MAX_INFLIGHT env override.
|
|
18
14
|
|
|
19
15
|
function _defaultMaxInflight() {
|
|
20
16
|
const override = Number(process.env.MIXDOG_CHILD_SPAWN_MAX_INFLIGHT);
|
|
21
17
|
if (Number.isFinite(override) && override >= 1) return Math.floor(override);
|
|
22
|
-
|
|
23
|
-
try { cpus = os.cpus()?.length || 0; } catch { cpus = 0; }
|
|
24
|
-
return Math.max(4, cpus || 4);
|
|
18
|
+
return Infinity;
|
|
25
19
|
}
|
|
26
20
|
|
|
27
21
|
const MAX_INFLIGHT = _defaultMaxInflight();
|
|
@@ -4,10 +4,14 @@ import { requestMemoryPressureSnapshot } from './memory-snapshot.mjs';
|
|
|
4
4
|
|
|
5
5
|
const MB = 1024 * 1024;
|
|
6
6
|
|
|
7
|
+
// Concurrency defaults are UNBOUNDED by design: parallel agent/shell waves
|
|
8
|
+
// must never serialize behind an admission queue. The only default guards are
|
|
9
|
+
// the memory-pressure checks below; hard caps exist solely as opt-in env
|
|
10
|
+
// overrides (MIXDOG_MAX_CONCURRENT_AGENTS / _SHELLS / _HIGH_LOAD).
|
|
7
11
|
export const RESOURCE_ADMISSION_DEFAULTS = Object.freeze({
|
|
8
|
-
maxAgents:
|
|
9
|
-
maxShells:
|
|
10
|
-
maxHighLoad:
|
|
12
|
+
maxAgents: Infinity,
|
|
13
|
+
maxShells: Infinity,
|
|
14
|
+
maxHighLoad: Infinity,
|
|
11
15
|
maxQueue: 32,
|
|
12
16
|
minFreeMemoryMb: 1024,
|
|
13
17
|
maxRssMb: 3072,
|
|
@@ -47,9 +47,6 @@ function publicAgentReviews(sessionId) {
|
|
|
47
47
|
return [...turn.agents.values()].map((review) => ({ ...review }));
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/** Compatibility no-op: apply_patch tracking has no project warmup cost. */
|
|
51
|
-
export function prewarmTurnSnapshot() {}
|
|
52
|
-
|
|
53
50
|
/** Start a new user turn and invalidate the prior turn's child review group. */
|
|
54
51
|
export async function beginTurnSnapshot(_worktree, sessionId) {
|
|
55
52
|
const ownerSessionId = clean(sessionId);
|
|
@@ -49,6 +49,7 @@ export function createLifecycleApi(deps) {
|
|
|
49
49
|
createCurrentSession, refreshRouteEffort,
|
|
50
50
|
invalidateContextStatusCache, invalidatePreSessionToolSurface,
|
|
51
51
|
applyResolvedCwd, resolveRoute, applyDeferredToolSurface, getStandaloneTools,
|
|
52
|
+
beginRoutePreparation, clearRoutePreparation,
|
|
52
53
|
pushTranscriptRebind,
|
|
53
54
|
notificationListeners, remoteStateListeners, desktopSession,
|
|
54
55
|
} = deps;
|
|
@@ -342,7 +343,8 @@ export function createLifecycleApi(deps) {
|
|
|
342
343
|
pushTranscriptRebind?.();
|
|
343
344
|
return true;
|
|
344
345
|
},
|
|
345
|
-
async switchContext({ cwd, desktopSession: nextDesktopSession } = {}) {
|
|
346
|
+
async switchContext({ cwd, desktopSession: nextDesktopSession, forResume = false } = {}) {
|
|
347
|
+
clearRoutePreparation?.();
|
|
346
348
|
const session = getSession();
|
|
347
349
|
if (session?.id) {
|
|
348
350
|
const cleanupReason = 'desktop-context-switch';
|
|
@@ -376,7 +378,7 @@ export function createLifecycleApi(deps) {
|
|
|
376
378
|
// that session's provider/model. A fresh desktop task or project must
|
|
377
379
|
// return to the configured Lead route instead of inheriting the route
|
|
378
380
|
// of whichever session happened to be open immediately beforehand.
|
|
379
|
-
if (typeof setRoute === 'function' && typeof getConfig === 'function'
|
|
381
|
+
if (!forResume && typeof setRoute === 'function' && typeof getConfig === 'function'
|
|
380
382
|
&& typeof resolveRoute === 'function') {
|
|
381
383
|
setRoute(resolveRoute(getConfig(), {}));
|
|
382
384
|
await refreshRouteEffort?.();
|
|
@@ -405,7 +407,23 @@ export function createLifecycleApi(deps) {
|
|
|
405
407
|
prefetchSession(id) {
|
|
406
408
|
return mgr.prefetchSession?.(id, toolSpecForMode(getMode())) === true;
|
|
407
409
|
},
|
|
410
|
+
// Read-only session peek (desktop split panes treat every visible pane as
|
|
411
|
+
// foreground): the parsed session object without resuming, attaching, or
|
|
412
|
+
// touching ownership/liveness. The current session returns live memory.
|
|
413
|
+
peekSession(id) {
|
|
414
|
+
const sessionId = clean(id);
|
|
415
|
+
if (!sessionId || !/^[A-Za-z0-9_-]+$/.test(sessionId)) return null;
|
|
416
|
+
const current = getSession();
|
|
417
|
+
if (current?.id === sessionId) return current;
|
|
418
|
+
const session = mgr.loadSession?.(sessionId);
|
|
419
|
+
// `closed` means the previous live owner released this durable session;
|
|
420
|
+
// it is still a valid historical transcript. Desktop pane peeks are
|
|
421
|
+
// strictly read-only, so they may inspect it without resuming or
|
|
422
|
+
// changing ownership. Deleted/tombstoned sessions have no load result.
|
|
423
|
+
return session || null;
|
|
424
|
+
},
|
|
408
425
|
async resume(id) {
|
|
426
|
+
clearRoutePreparation?.();
|
|
409
427
|
const prev = getSession();
|
|
410
428
|
const previousId = prev?.id || null;
|
|
411
429
|
const previousMessages = prev?.messages || null;
|
|
@@ -430,22 +448,44 @@ export function createLifecycleApi(deps) {
|
|
|
430
448
|
}
|
|
431
449
|
setSession(resumed);
|
|
432
450
|
applyResolvedCwd(resolveResumeCwd(resumed, getCurrentCwd()), { markRefresh: false });
|
|
451
|
+
// Cwd application is synchronous even though MCP reconnect may continue
|
|
452
|
+
// in the background. Commit it before returning the resume transcript.
|
|
453
|
+
resumed.cwd = getCurrentCwd();
|
|
433
454
|
const route = getRoute();
|
|
434
455
|
// The resumed session's OWN effort wins. resolveRoute always returns an
|
|
435
456
|
// effort key, so this used to reinstate the effort of whichever session
|
|
436
457
|
// happened to be open before — a schedule/webhook session opened with a
|
|
437
458
|
// different effort than the one it actually ran with.
|
|
438
459
|
const resumeEffort = resumed.effort || (hasOwn(route, 'effort') ? route.effort : undefined);
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
460
|
+
const resumedRoute = resolveRoute(
|
|
461
|
+
getConfig(),
|
|
462
|
+
{ provider: resumed.provider, model: resumed.model, effort: resumeEffort },
|
|
463
|
+
);
|
|
464
|
+
setRoute(resumedRoute);
|
|
465
|
+
const finishRoutePreparation = async () => {
|
|
466
|
+
const preparedRoute = await refreshRouteEffort(null, resumedRoute);
|
|
467
|
+
// Session or route changed while provider metadata was loading.
|
|
468
|
+
if (!preparedRoute || getSession() !== resumed || getRoute() !== preparedRoute) return false;
|
|
469
|
+
const activeSession = getSession();
|
|
470
|
+
activeSession.effort = getRoute().effectiveEffort || null;
|
|
471
|
+
activeSession.cwd = getCurrentCwd();
|
|
472
|
+
applyDeferredToolSurface(
|
|
473
|
+
activeSession,
|
|
474
|
+
deferredSurfaceModeForLead(getMode()),
|
|
475
|
+
getStandaloneTools(),
|
|
476
|
+
{ provider: getRoute().provider },
|
|
477
|
+
);
|
|
478
|
+
writeStatuslineRoute(statusRoutes, activeSession, getRoute());
|
|
479
|
+
return true;
|
|
480
|
+
};
|
|
481
|
+
if (typeof beginRoutePreparation === 'function') {
|
|
482
|
+
beginRoutePreparation(finishRoutePreparation);
|
|
483
|
+
} else {
|
|
484
|
+
await finishRoutePreparation();
|
|
485
|
+
}
|
|
445
486
|
invalidatePreSessionToolSurface();
|
|
446
487
|
invalidateContextStatusCache();
|
|
447
488
|
setSessionNeedsCwdRefresh(false);
|
|
448
|
-
writeStatuslineRoute(statusRoutes, session, getRoute());
|
|
449
489
|
// Session swapped to the resumed one: repoint the worker to the current
|
|
450
490
|
// transcript instead of waiting for the next inbound steal.
|
|
451
491
|
pushTranscriptRebind?.();
|