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
|
@@ -123,5 +123,8 @@ export {
|
|
|
123
123
|
flushSessionMetrics,
|
|
124
124
|
} from './manager/session-crud.mjs';
|
|
125
125
|
export { deleteSession } from './store.mjs';
|
|
126
|
+
// Read-only parsed-session access (desktop pane peek): no resume, no
|
|
127
|
+
// ownership, no liveness side effects.
|
|
128
|
+
export { loadSession } from './store.mjs';
|
|
126
129
|
export { closeSession, abortSessionTurn } from './manager/session-close.mjs';
|
|
127
130
|
export { sweepTombstones, startIdleCleanup, stopIdleCleanup } from './manager/idle-cleanup.mjs';
|
|
@@ -173,6 +173,25 @@ function scanSessionFiles(heartbeatMtimes = sessionHeartbeatMtimes()) {
|
|
|
173
173
|
(right.lastUsedAt || right.updatedAt || 0) - (left.lastUsedAt || left.updatedAt || 0));
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
function sessionFilesAreNewerThan(indexPath) {
|
|
177
|
+
const directory = join(dataDir(), 'sessions');
|
|
178
|
+
if (!existsSync(directory)) return false;
|
|
179
|
+
let indexMtime = 0;
|
|
180
|
+
try { indexMtime = statSync(indexPath).mtimeMs || 0; }
|
|
181
|
+
catch { return true; }
|
|
182
|
+
try {
|
|
183
|
+
for (const filename of readdirSync(directory)) {
|
|
184
|
+
if (!filename.endsWith('.json')) continue;
|
|
185
|
+
try {
|
|
186
|
+
if ((statSync(join(directory, filename)).mtimeMs || 0) > indexMtime) return true;
|
|
187
|
+
} catch { /* session disappeared after readdir */ }
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
176
195
|
export function listStoredSessionSummaries(options = {}) {
|
|
177
196
|
const heartbeatMtimes = sessionHeartbeatMtimes();
|
|
178
197
|
if (options.refreshFromStorage === true) return scanSessionFiles(heartbeatMtimes);
|
|
@@ -185,6 +204,13 @@ export function listStoredSessionSummaries(options = {}) {
|
|
|
185
204
|
.filter((row) => row && isLeadVisibleRow(row))
|
|
186
205
|
.sort((left, right) =>
|
|
187
206
|
(right.lastUsedAt || right.updatedAt || 0) - (left.lastUsedAt || left.updatedAt || 0));
|
|
207
|
+
// A crashed/non-blocking sidecar writer can leave a valid but old,
|
|
208
|
+
// non-empty index indefinitely. Detect only newer session JSONs
|
|
209
|
+
// (heartbeat churn does not count), then recover read-only from the
|
|
210
|
+
// authoritative files without trying to rewrite the stuck index.
|
|
211
|
+
if (options.rebuildIfMissing !== false && sessionFilesAreNewerThan(indexPath)) {
|
|
212
|
+
return scanSessionFiles(heartbeatMtimes);
|
|
213
|
+
}
|
|
188
214
|
if (rows.length > 0 || options.rebuildIfMissing === false) return rows;
|
|
189
215
|
}
|
|
190
216
|
} catch {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Chunked o200k BPE encode-count, shared by the main thread (context-utils)
|
|
2
|
+
// and the token-count worker. BPE merge cost is quadratic in WORD length, not
|
|
3
|
+
// text length: a degenerate single-word run ('p'.repeat(250k), base64/minified
|
|
4
|
+
// blobs) forms one giant pat_str word and encode() spins for minutes while the
|
|
5
|
+
// event loop is blocked. Encoding fixed-size slices caps the worst-case word at
|
|
6
|
+
// the chunk size, so cost stays linear. Chunk cuts snap back to the nearest
|
|
7
|
+
// whitespace so the next slice starts at a natural ` word` boundary — o200k
|
|
8
|
+
// tokenizes that identically to the unsliced text, keeping estimates EXACT for
|
|
9
|
+
// prose. Only a whitespace-free degenerate run falls back to a hard cut
|
|
10
|
+
// (±1 token per boundary). Hard cuts avoid splitting a surrogate pair so
|
|
11
|
+
// sliced emoji/CJK-ext code points still encode cleanly.
|
|
12
|
+
const BPE_ENCODE_CHUNK_CHARS = 4_096;
|
|
13
|
+
const BPE_CHUNK_BOUNDARY_SCAN = 512;
|
|
14
|
+
|
|
15
|
+
export function bpeEncodeCount(enc, s) {
|
|
16
|
+
if (s.length <= BPE_ENCODE_CHUNK_CHARS) return enc.encode(s, undefined, []).length;
|
|
17
|
+
let total = 0;
|
|
18
|
+
let i = 0;
|
|
19
|
+
while (i < s.length) {
|
|
20
|
+
let end = Math.min(s.length, i + BPE_ENCODE_CHUNK_CHARS);
|
|
21
|
+
if (end < s.length) {
|
|
22
|
+
// Prefer cutting BEFORE a whitespace run: the next chunk then
|
|
23
|
+
// starts with ` word`, which o200k merges exactly as it would
|
|
24
|
+
// mid-text. Scan a bounded window so degenerate inputs stay O(1).
|
|
25
|
+
let ws = -1;
|
|
26
|
+
const scanFloor = Math.max(i + 1, end - BPE_CHUNK_BOUNDARY_SCAN);
|
|
27
|
+
for (let j = end - 1; j >= scanFloor; j -= 1) {
|
|
28
|
+
const c = s.charCodeAt(j);
|
|
29
|
+
if (c === 0x20 || c === 0x0A || c === 0x0D || c === 0x09) { ws = j; break; }
|
|
30
|
+
}
|
|
31
|
+
if (ws > i) {
|
|
32
|
+
end = ws; // next chunk starts at the whitespace
|
|
33
|
+
} else {
|
|
34
|
+
const last = s.charCodeAt(end - 1);
|
|
35
|
+
if (last >= 0xD800 && last <= 0xDBFF) end += 1;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
total += enc.encode(s.slice(i, end), undefined, []).length;
|
|
39
|
+
i = end;
|
|
40
|
+
}
|
|
41
|
+
return total;
|
|
42
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Native mixdog-token client: persistent stdio server (transport lifecycle
|
|
2
|
+
// modeled on tools/patch/native-server.mjs) that serves precise o200k BPE
|
|
3
|
+
// counts ~5-12x faster than the in-process tiktoken WASM worker.
|
|
4
|
+
//
|
|
5
|
+
// Wire format: length-prefixed BINARY frames (raw utf8 payload, no JSON
|
|
6
|
+
// escape/parse on either side — that overhead dominated once the encoder
|
|
7
|
+
// went linear-time):
|
|
8
|
+
// request : u8 op (1=ping, 2=count) | u32le id | u32le byteLen | payload
|
|
9
|
+
// response : u8 op | u32le id | u64le count
|
|
10
|
+
// A stale JSON-only server never answers a binary ping, so the ready probe
|
|
11
|
+
// times out and this module degrades to the WASM worker path.
|
|
12
|
+
//
|
|
13
|
+
// Strictly a best-effort accelerator behind context-utils' async count path:
|
|
14
|
+
// unavailable binary / dead server / timeout all resolve `null`, and the
|
|
15
|
+
// caller falls back to the JS worker thread. Kill switch:
|
|
16
|
+
// MIXDOG_TOKEN_NATIVE=0 (mode `auto` is the default; MIXDOG_TOKEN_NATIVE_BIN
|
|
17
|
+
// overrides binary resolution).
|
|
18
|
+
import { existsSync } from 'node:fs';
|
|
19
|
+
import { spawn } from 'node:child_process';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { resolve as pathResolve, dirname as pathDirname, join as pathJoin } from 'node:path';
|
|
22
|
+
import { startChildGuardian } from '../../../shared/child-guardian.mjs';
|
|
23
|
+
import { getPluginData } from '../config.mjs';
|
|
24
|
+
import { ensureTokenBinary, findCachedTokenBinary } from '../tools/token-binary-fetcher.mjs';
|
|
25
|
+
|
|
26
|
+
const PLUGIN_ROOT = process.env.MIXDOG_ROOT
|
|
27
|
+
|| pathResolve(pathDirname(fileURLToPath(import.meta.url)), '../../../../..');
|
|
28
|
+
const BIN_NAME = process.platform === 'win32' ? 'mixdog-token.exe' : 'mixdog-token';
|
|
29
|
+
const LOCAL_BIN = pathJoin(PLUGIN_ROOT, 'native/mixdog-token/target/release', BIN_NAME);
|
|
30
|
+
const READY_TIMEOUT_MS = 10_000;
|
|
31
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
32
|
+
const OP_PING = 1;
|
|
33
|
+
const OP_COUNT = 2;
|
|
34
|
+
const RESPONSE_FRAME_BYTES = 13;
|
|
35
|
+
const UNKNOWN_OP_SENTINEL = 0xFFFF_FFFF_FFFF_FFFFn;
|
|
36
|
+
const EMPTY_PAYLOAD = Buffer.alloc(0);
|
|
37
|
+
|
|
38
|
+
function nativeTokenMode() {
|
|
39
|
+
return String(process.env.MIXDOG_TOKEN_NATIVE || 'auto').toLowerCase();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function nativeTokenModeEnabled() {
|
|
43
|
+
return !/^(0|false|no|off|js)$/i.test(nativeTokenMode());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let _binPath; // undefined = not resolved, null = none found
|
|
47
|
+
function _resolveBinary() {
|
|
48
|
+
if (_binPath !== undefined) return _binPath;
|
|
49
|
+
const candidates = [
|
|
50
|
+
String(process.env.MIXDOG_TOKEN_NATIVE_BIN || '').trim() || null,
|
|
51
|
+
LOCAL_BIN,
|
|
52
|
+
].filter(Boolean);
|
|
53
|
+
_binPath = candidates.find((candidate) => {
|
|
54
|
+
try { return existsSync(candidate); } catch { return false; }
|
|
55
|
+
}) || null;
|
|
56
|
+
if (!_binPath) {
|
|
57
|
+
// Release-supply cache (sha256-verified against the bundled manifest).
|
|
58
|
+
try { _binPath = findCachedTokenBinary(getPluginData()) || null; } catch { _binPath = null; }
|
|
59
|
+
}
|
|
60
|
+
return _binPath;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let _server = null; // { child, pending: Map, seq, ready } null after hard failure
|
|
64
|
+
let _serverFailed = false;
|
|
65
|
+
|
|
66
|
+
function _dropServer(error) {
|
|
67
|
+
const server = _server;
|
|
68
|
+
_server = null;
|
|
69
|
+
if (!server) return;
|
|
70
|
+
const reason = error instanceof Error ? error : new Error(String(error || 'mixdog-token server closed'));
|
|
71
|
+
for (const [, entry] of server.pending) {
|
|
72
|
+
clearTimeout(entry.timer);
|
|
73
|
+
entry.resolve(null);
|
|
74
|
+
void reason;
|
|
75
|
+
}
|
|
76
|
+
server.pending.clear();
|
|
77
|
+
try { server.child.kill(); } catch { /* already down */ }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function _ensureServer() {
|
|
81
|
+
if (_server || _serverFailed || !nativeTokenModeEnabled()) return _server;
|
|
82
|
+
const bin = _resolveBinary();
|
|
83
|
+
if (!bin) { _serverFailed = true; return null; }
|
|
84
|
+
let child;
|
|
85
|
+
try {
|
|
86
|
+
child = spawn(bin, [], { stdio: ['pipe', 'pipe', 'ignore'], windowsHide: true });
|
|
87
|
+
} catch {
|
|
88
|
+
_serverFailed = true;
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const server = { child, pending: new Map(), seq: 0, ready: null };
|
|
92
|
+
let inbox = EMPTY_PAYLOAD;
|
|
93
|
+
child.stdout.on('data', (chunk) => {
|
|
94
|
+
inbox = inbox.length ? Buffer.concat([inbox, chunk]) : chunk;
|
|
95
|
+
while (inbox.length >= RESPONSE_FRAME_BYTES) {
|
|
96
|
+
const id = inbox.readUInt32LE(1);
|
|
97
|
+
const count = inbox.readBigUInt64LE(5);
|
|
98
|
+
inbox = inbox.subarray(RESPONSE_FRAME_BYTES);
|
|
99
|
+
const entry = server.pending.get(id);
|
|
100
|
+
if (!entry) continue;
|
|
101
|
+
server.pending.delete(id);
|
|
102
|
+
clearTimeout(entry.timer);
|
|
103
|
+
entry.resolve(count === UNKNOWN_OP_SENTINEL ? null : Number(count));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
child.on('error', () => { _serverFailed = true; _dropServer('spawn error'); });
|
|
107
|
+
child.on('exit', () => { _dropServer('exited'); });
|
|
108
|
+
// Best-effort accelerator: never keeps the process alive. Unref the
|
|
109
|
+
// stdio PIPES too — child.unref() alone leaves the socket handles ref'd,
|
|
110
|
+
// which kept short-lived hosts (node --test suites) from ever exiting.
|
|
111
|
+
child.unref();
|
|
112
|
+
child.stdin?.unref?.();
|
|
113
|
+
child.stdout?.unref?.();
|
|
114
|
+
child.stdin?.on?.('error', () => { /* EPIPE on teardown */ });
|
|
115
|
+
try {
|
|
116
|
+
startChildGuardian({ childPid: child.pid, label: 'mixdog-token', orphanGraceMs: 8000, forceGraceMs: 3000 });
|
|
117
|
+
} catch { /* guardian best-effort */ }
|
|
118
|
+
server.ready = _request(server, OP_PING, null, READY_TIMEOUT_MS).then((count) => count !== null);
|
|
119
|
+
_server = server;
|
|
120
|
+
return server;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function _request(server, op, text, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
const id = ++server.seq;
|
|
126
|
+
const timer = setTimeout(() => {
|
|
127
|
+
server.pending.delete(id);
|
|
128
|
+
resolve(null);
|
|
129
|
+
}, timeoutMs);
|
|
130
|
+
timer.unref?.();
|
|
131
|
+
server.pending.set(id, { resolve, timer });
|
|
132
|
+
try {
|
|
133
|
+
const payload = text ? Buffer.from(text, 'utf8') : EMPTY_PAYLOAD;
|
|
134
|
+
const header = Buffer.allocUnsafe(9);
|
|
135
|
+
header[0] = op;
|
|
136
|
+
header.writeUInt32LE(id >>> 0, 1);
|
|
137
|
+
header.writeUInt32LE(payload.length >>> 0, 5);
|
|
138
|
+
const stdin = server.child.stdin;
|
|
139
|
+
stdin.cork();
|
|
140
|
+
stdin.write(header);
|
|
141
|
+
if (payload.length) stdin.write(payload);
|
|
142
|
+
stdin.uncork();
|
|
143
|
+
} catch {
|
|
144
|
+
server.pending.delete(id);
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
resolve(null);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** True when the native counter can be attempted (mode on + binary present). */
|
|
152
|
+
export function nativeTokenCounterEnabled() {
|
|
153
|
+
return nativeTokenModeEnabled() && !_serverFailed && Boolean(_resolveBinary());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Precise o200k count via the native server; null on any unavailability. */
|
|
157
|
+
export async function countTokensNative(text) {
|
|
158
|
+
const server = _ensureServer();
|
|
159
|
+
if (!server) return null;
|
|
160
|
+
const ready = await server.ready;
|
|
161
|
+
if (!ready) { _serverFailed = true; _dropServer('ready failed'); return null; }
|
|
162
|
+
const count = await _request(server, OP_COUNT, String(text ?? ''));
|
|
163
|
+
return Number.isFinite(count) && count >= 0 ? count : null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let _fetchAttempted = false;
|
|
167
|
+
|
|
168
|
+
/** Boot prewarm: spawn the server + build the encoder off the first estimate.
|
|
169
|
+
* With no local/env/cached binary, kick ONE background manifest fetch; on
|
|
170
|
+
* success the resolution cache resets so the next estimate adopts it. */
|
|
171
|
+
export function prewarmNativeTokenCounter() {
|
|
172
|
+
const server = _ensureServer();
|
|
173
|
+
if (!server) {
|
|
174
|
+
if (!_fetchAttempted && nativeTokenModeEnabled() && !_resolveBinary()) {
|
|
175
|
+
_fetchAttempted = true;
|
|
176
|
+
void ensureTokenBinary(getPluginData()).then((path) => {
|
|
177
|
+
if (!path) return;
|
|
178
|
+
_binPath = undefined;
|
|
179
|
+
_serverFailed = false;
|
|
180
|
+
}).catch(() => { /* soft degrade — WASM worker path remains */ });
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
server.ready?.catch?.(() => {});
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
@@ -32,6 +32,7 @@ import { crossTurnSignature, crossTurnDedupStub, isEditProgressTool } from './lo
|
|
|
32
32
|
import {
|
|
33
33
|
getToolKind,
|
|
34
34
|
isEagerDispatchable,
|
|
35
|
+
isParallelDispatchable,
|
|
35
36
|
isToolCallDedupEligible,
|
|
36
37
|
parseNativeToolSearchPayload,
|
|
37
38
|
} from './loop/tool-helpers.mjs';
|
|
@@ -181,7 +182,11 @@ export async function processToolBatch(ctx) {
|
|
|
181
182
|
// re-issue it cleanly in a new turn instead of copying back a
|
|
182
183
|
// `[mixdog compacted …]` placeholder. Emits a matching is_error
|
|
183
184
|
// tool_result so the assistant tool_use is not orphaned.
|
|
184
|
-
|
|
185
|
+
// Full-parallel caveat: a call that ALREADY started (pending eager
|
|
186
|
+
// promise) cannot be "skipped" — its side effects are running, so
|
|
187
|
+
// its real result must be consumed below. The gate only skips
|
|
188
|
+
// calls that have not started (apply_patch never starts eagerly).
|
|
189
|
+
if (_orderedMutationFailed && _isOrderedGateSkippable(call.name) && !pending.has(call.id)) {
|
|
185
190
|
if (call?.id) restoreToolCallBodyForId(assistantTurnMsg, calls, call.id);
|
|
186
191
|
pushToolResultMessage({
|
|
187
192
|
role: 'tool',
|
|
@@ -252,13 +257,18 @@ export async function processToolBatch(ctx) {
|
|
|
252
257
|
_executeOk = true;
|
|
253
258
|
} else {
|
|
254
259
|
// Fallback for providers that don't stream tool calls early:
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
if (
|
|
260
|
+
// start the whole remaining batch in parallel (full-parallel
|
|
261
|
+
// policy; only apply_patch stays in this serial body).
|
|
262
|
+
if (isParallelDispatchable(call.name)) {
|
|
258
263
|
startEagerRun(calls, callIndex, _duplicateCallIds);
|
|
259
264
|
}
|
|
260
265
|
let eager = pending.get(call.id);
|
|
261
|
-
|
|
266
|
+
// Post-mutation invalidation applies ONLY to read-only results:
|
|
267
|
+
// a read that raced an apply_patch re-executes for fresh
|
|
268
|
+
// content. Non-read-only parallel calls (shell/MCP/...) already
|
|
269
|
+
// ran — their side effects are real, so their results are
|
|
270
|
+
// consumed as-is and NEVER re-executed.
|
|
271
|
+
if (eager !== undefined && eager.mutationEpoch < epoch.mutation && isEagerDispatchable(call.name, tools)) {
|
|
262
272
|
pending.delete(call.id);
|
|
263
273
|
eager = undefined;
|
|
264
274
|
}
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
resolveExecutionMode,
|
|
40
40
|
} from '../../../../shared/background-tasks.mjs';
|
|
41
41
|
import { resolveShellFor } from './shell-runtime.mjs';
|
|
42
|
+
import { prewarmPwshStandbyPool } from '../lib/pwsh-standby-pool.mjs';
|
|
42
43
|
import { smartMiddleTruncate } from './shell-output.mjs';
|
|
43
44
|
import { normalizeOutputPath } from './path-utils.mjs';
|
|
44
45
|
import { normalizeErrorMessage } from './path-diagnostics.mjs';
|
|
@@ -58,6 +59,25 @@ import { resourceAdmission } from '../../../../shared/resource-admission.mjs';
|
|
|
58
59
|
export function _captureTrackedMtimes(_scope) {
|
|
59
60
|
return new Map();
|
|
60
61
|
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Boot prewarm: build the EXACT spawn env/spec a real shell call uses and warm
|
|
65
|
+
* the pwsh standby pool with it, so the first shell call of a session skips
|
|
66
|
+
* the pwsh startup cost. No-op off Windows / non-pwsh. Best-effort.
|
|
67
|
+
*/
|
|
68
|
+
export function prewarmShellStandbys() {
|
|
69
|
+
try {
|
|
70
|
+
const spec = resolveShellFor('default');
|
|
71
|
+
if (!spec || spec.shellType !== 'powershell') return false;
|
|
72
|
+
const env = { ...process.env, LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8' };
|
|
73
|
+
scrubProviderSecrets(env);
|
|
74
|
+
scrubLoaderVars(env);
|
|
75
|
+
scrubRuntimeRootVars(env);
|
|
76
|
+
return prewarmPwshStandbyPool({ shell: spec.shell, shellArgs: spec.shellArgs, env }) === true;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
61
81
|
export function _trackedDriftNoteAfter(_scope, _pre) {
|
|
62
82
|
return '';
|
|
63
83
|
}
|
|
@@ -74,13 +74,20 @@ export const BUILTIN_TOOLS = [
|
|
|
74
74
|
name: 'shell',
|
|
75
75
|
title: 'Mixdog Shell',
|
|
76
76
|
annotations: { title: 'Mixdog Shell', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, compressible: true },
|
|
77
|
-
description:
|
|
77
|
+
description: 'Run programs/change state; not file inspection. Calls in one turn run parallel; '
|
|
78
|
+
+ 'combine order-dependent commands. Use async for sleep/watch/dev loops.'
|
|
79
|
+
+ `${_shellSyntaxCheat} ${TOOL_ASYNC_EXECUTION_CONTRACT}`,
|
|
78
80
|
inputSchema: {
|
|
79
81
|
type: 'object',
|
|
80
82
|
properties: {
|
|
81
83
|
command: { type: 'string', description: 'Command.' },
|
|
82
84
|
cwd: { type: 'string', description: 'Working directory; persists across calls. Omit to reuse; absolute path changes it.' },
|
|
83
|
-
timeout: {
|
|
85
|
+
timeout: {
|
|
86
|
+
type: 'number',
|
|
87
|
+
description: `Timeout ms; default ${_shellDefaultTimeoutMs()}. `
|
|
88
|
+
+ 'Sync timeout may return task_id; explicit timeout becomes its deadline. '
|
|
89
|
+
+ 'Sleeps are killed, not promoted.',
|
|
90
|
+
},
|
|
84
91
|
merge_stderr: { type: 'boolean', description: 'Merge stderr.' },
|
|
85
92
|
mode: { type: 'string', enum: ['sync', 'async'], description: executionModeSchemaDescription('sync') },
|
|
86
93
|
shell: { type: 'string', enum: ['bash', 'powershell'], description: 'Force shell. Windows defaults to PowerShell; bash = Git Bash/POSIX.' },
|
|
@@ -117,6 +117,30 @@ function normalizeWindowsSnapshot(snapshot) {
|
|
|
117
117
|
return rows;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// Standby-pool support: does `pid` currently own live descendant processes?
|
|
121
|
+
// FRESH snapshot on purpose — the 750ms cache could miss a just-spawned
|
|
122
|
+
// grandchild and wrongly certify a polluted standby shell as clean. Unknown
|
|
123
|
+
// snapshots answer true (conservative: the pool discards instead of reusing).
|
|
124
|
+
export async function windowsPidHasDescendants(pid, { spawnFn = spawn } = {}) {
|
|
125
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
126
|
+
if (process.platform !== 'win32') return false;
|
|
127
|
+
let rows = null;
|
|
128
|
+
try { rows = await windowsProcessSnapshot({ fresh: true, spawnFn }); } catch { rows = null; }
|
|
129
|
+
if (!rows) return true;
|
|
130
|
+
const owned = new Set([pid]);
|
|
131
|
+
let discovered = true;
|
|
132
|
+
while (discovered) {
|
|
133
|
+
discovered = false;
|
|
134
|
+
for (const row of rows.values()) {
|
|
135
|
+
if (!owned.has(row.pid) && owned.has(row.parentPid)) {
|
|
136
|
+
owned.add(row.pid);
|
|
137
|
+
discovered = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return owned.size > 1;
|
|
142
|
+
}
|
|
143
|
+
|
|
120
144
|
// Invoke onQuiescent exactly once, synchronously when already quiescent or
|
|
121
145
|
// after positive liveness probes stop observing the owned tree/group.
|
|
122
146
|
export function trackProcessTreeQuiescence(
|
|
@@ -110,5 +110,13 @@ export function scrubProviderSecrets(env) {
|
|
|
110
110
|
export function scrubRuntimeRootVars(env) {
|
|
111
111
|
if (!env || typeof env !== 'object') return env;
|
|
112
112
|
delete env.MIXDOG_ROOT;
|
|
113
|
+
// Host-runtime leak: a packaged Electron/daemon host runs with
|
|
114
|
+
// NODE_ENV=production, and inheriting it silently corrupts model-spawned
|
|
115
|
+
// shells — `npm install` prunes devDependencies, React resolves its
|
|
116
|
+
// production build (no `act`), test suites flip red on developer machines
|
|
117
|
+
// while CI stays green. The value is OUR process's, not the user's OS
|
|
118
|
+
// env, so shells must not see it. A command that needs NODE_ENV sets it
|
|
119
|
+
// explicitly.
|
|
120
|
+
delete env.NODE_ENV;
|
|
113
121
|
return env;
|
|
114
122
|
}
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.1.
|
|
2
|
+
"version": "0.1.2",
|
|
3
3
|
"_comment": "Synced from immutable graph-v release assets.",
|
|
4
4
|
"assets": {
|
|
5
5
|
"darwin-arm64": {
|
|
6
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.
|
|
7
|
-
"sha256": "
|
|
6
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.2/mixdog-graph-darwin-arm64",
|
|
7
|
+
"sha256": "e54f09013af2ae057eae89c629237af79caeb248e59c335c64c88fc7b720d662"
|
|
8
8
|
},
|
|
9
9
|
"darwin-x64": {
|
|
10
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.
|
|
11
|
-
"sha256": "
|
|
10
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.2/mixdog-graph-darwin-x64",
|
|
11
|
+
"sha256": "d4506109fab2a392157ee016d9dfd2524d74363925004f008795eab3fba3de23"
|
|
12
12
|
},
|
|
13
13
|
"linux-arm64": {
|
|
14
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.
|
|
15
|
-
"sha256": "
|
|
14
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.2/mixdog-graph-linux-arm64",
|
|
15
|
+
"sha256": "ba5f3e52f95c6e0c976115fea9e60c1a1d4534565a15c2da7284ad9c59c1eeb5"
|
|
16
16
|
},
|
|
17
17
|
"linux-x64": {
|
|
18
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.
|
|
19
|
-
"sha256": "
|
|
18
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.2/mixdog-graph-linux-x64",
|
|
19
|
+
"sha256": "3642d773317a468fe5ba5efc4544ae91a68117b78d577ee2aad37e2639fcc01a"
|
|
20
20
|
},
|
|
21
21
|
"win32-x64": {
|
|
22
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.
|
|
23
|
-
"sha256": "
|
|
22
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.2/mixdog-graph-win32-x64.exe",
|
|
23
|
+
"sha256": "3d86870fc6d3ce1151dd97b91fc634fd2755e3f3c74f10809aaea34ae3e2aa02"
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
}
|