mixdog 0.9.150 → 0.9.151
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 +28 -11
- package/package.json +1 -1
- package/src/headless-exec.mjs +16 -3
- package/src/headless-exec.test.mjs +44 -1
- package/src/rules/shared/30-exploration.md +2 -1
- package/src/runtime/agent/orchestrator/session/compact/handoff.mjs +2 -4
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +18 -33
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +21 -42
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +20 -125
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -20
- package/src/runtime/agent/orchestrator/tools/builtin/absolute-glob-expand.test.mjs +63 -0
- package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +29 -10
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/enoent-outside-project.test.mjs +56 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +32 -16
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +28 -0
- package/src/runtime/agent/orchestrator/tools/builtin/grep-single-file-rescue.test.mjs +106 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/absolute-glob-expand.mjs +129 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-single-file-rescue.mjs +218 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +33 -0
- package/src/runtime/agent/orchestrator/tools/builtin/noise-dir-visibility.test.mjs +69 -0
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +33 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +26 -6
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +7 -66
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +11 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +60 -5
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +54 -1
- package/src/runtime/agent/orchestrator/tools/builtin/write-symlink.test.mjs +153 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/aggregate-anchor-relocation.test.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +54 -3
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +44 -10
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +57 -5
- package/src/runtime/agent/orchestrator/tools/patch/patch-symlink.test.mjs +141 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/patch/v4a-pure-move.test.mjs +57 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-section-coalesce.test.mjs +81 -0
- package/src/runtime/memory/index.mjs +2 -4
- package/src/runtime/memory/lib/pg/process.mjs +29 -2
- package/src/runtime/memory/lib/pg/process.test.mjs +36 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +5 -1
- package/src/runtime/memory/lib/query-handlers.mjs +7 -6
- package/src/runtime/memory/lib/recall-format.mjs +13 -31
- package/src/runtime/shared/pristine-execution-contract.json +3 -0
- package/src/runtime/shared/pristine-execution.mjs +5 -2
- package/src/session-runtime/lifecycle-api.mjs +1 -0
- package/src/standalone/channel-worker-heartbeat.test.mjs +30 -0
- package/src/standalone/channel-worker.mjs +31 -1
- package/src/standalone/memory-runtime-proxy.mjs +5 -2
- package/src/standalone/session-client.mjs +23 -3
- package/src/tui/session/turn.mjs +12 -0
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
"MIXDOG_DISABLE_MCP": "1",
|
|
6
6
|
"MIXDOG_DISABLE_SKILLS": "1",
|
|
7
7
|
"MIXDOG_BOOT_CORE_MEMORY": "0",
|
|
8
|
+
"MIXDOG_DAEMON_SKIP_MEMORY": "1",
|
|
9
|
+
"MIXDOG_DISABLE_MEMORY_INGEST": "1",
|
|
8
10
|
"MIXDOG_DISABLE_CHANNEL_START": "1",
|
|
9
11
|
"MIXDOG_SKIP_USER_DATA_BACKUP": "1",
|
|
10
12
|
"MIXDOG_DISABLE_STAGED_INSTALL": "1",
|
|
11
13
|
"MIXDOG_DISABLE_STAGED_SWAP": "1",
|
|
12
14
|
"MIXDOG_FEATURE_WEB_SEARCH": "0",
|
|
13
15
|
"MIXDOG_FEATURE_MEMORY": "0",
|
|
16
|
+
"MIXDOG_MEMORY_DISABLE_CYCLES": "1",
|
|
14
17
|
"MIXDOG_MEMORY_CLIENT_GRACE_MS": "1"
|
|
15
18
|
},
|
|
16
19
|
"approvedExecutionEnv": [
|
|
@@ -244,7 +244,9 @@ export function createPristineExecutionBoundary({
|
|
|
244
244
|
};
|
|
245
245
|
const rootDir = mkdtempSync(join(tmpdir(), 'mixdog-headless-pristine-'));
|
|
246
246
|
const dataDir = join(rootDir, 'data');
|
|
247
|
+
const runtimeRoot = join(rootDir, 'runtime-root');
|
|
247
248
|
mkdirSync(dataDir, { recursive: true, mode: 0o700 });
|
|
249
|
+
mkdirSync(runtimeRoot, { recursive: true, mode: 0o700 });
|
|
248
250
|
let cleaned = false;
|
|
249
251
|
let restoreAuthBindings = () => {};
|
|
250
252
|
const cleanup = ({ preserveRoot = false, tolerateRootRemovalFailure = false } = {}) => {
|
|
@@ -259,7 +261,7 @@ export function createPristineExecutionBoundary({
|
|
|
259
261
|
// process exits; rmSync retries absorb that EBUSY/EPERM window.
|
|
260
262
|
if (!preserveRoot) {
|
|
261
263
|
try {
|
|
262
|
-
rmSync(rootDir, { recursive: true, force: true, maxRetries:
|
|
264
|
+
rmSync(rootDir, { recursive: true, force: true, maxRetries: 50, retryDelay: 100 });
|
|
263
265
|
} catch (error) {
|
|
264
266
|
if (tolerateRootRemovalFailure) return { rootRemovalError: error };
|
|
265
267
|
throw error;
|
|
@@ -306,6 +308,7 @@ export function createPristineExecutionBoundary({
|
|
|
306
308
|
}
|
|
307
309
|
setEnv('MIXDOG_HOME', rootDir);
|
|
308
310
|
setEnv('MIXDOG_DATA_DIR', dataDir);
|
|
311
|
+
setEnv('MIXDOG_RUNTIME_ROOT', runtimeRoot);
|
|
309
312
|
for (const [name, value] of Object.entries({
|
|
310
313
|
...inheritedExecutionEnv,
|
|
311
314
|
...approvedExecutionEnv,
|
|
@@ -358,7 +361,7 @@ export function createPristineExecutionBoundary({
|
|
|
358
361
|
authMode,
|
|
359
362
|
catalogCount,
|
|
360
363
|
});
|
|
361
|
-
return { rootDir, dataDir, config, audit, loadConfig, cleanup };
|
|
364
|
+
return { rootDir, dataDir, runtimeRoot, config, audit, loadConfig, cleanup };
|
|
362
365
|
} catch (error) {
|
|
363
366
|
cleanup();
|
|
364
367
|
throw error;
|
|
@@ -166,6 +166,7 @@ export function createLifecycleApi(deps) {
|
|
|
166
166
|
// them later). Never loads the memory runtime just for this — a null
|
|
167
167
|
// module promise means memory was never used this run, so skip.
|
|
168
168
|
async function ingestSessionIntoMemory(session) {
|
|
169
|
+
if (process.env.MIXDOG_DISABLE_MEMORY_INGEST === '1') return;
|
|
169
170
|
try {
|
|
170
171
|
const messages = Array.isArray(session?.messages) ? session.messages : [];
|
|
171
172
|
if (!session?.id || messages.length === 0) return;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import { pruneStaleChannelClientHeartbeats } from './channel-worker.mjs';
|
|
8
|
+
|
|
9
|
+
test('channel heartbeat sweep removes dead and expired rows only', () => {
|
|
10
|
+
const root = mkdtempSync(join(tmpdir(), 'mixdog-channel-heartbeat-test-'));
|
|
11
|
+
const now = 100_000;
|
|
12
|
+
try {
|
|
13
|
+
writeFileSync(join(root, '1.json'), JSON.stringify({ pid: 1, updatedAt: now - 1_000 }));
|
|
14
|
+
writeFileSync(join(root, '2.json'), JSON.stringify({ pid: 2, updatedAt: now - 60_000 }));
|
|
15
|
+
writeFileSync(join(root, '3.json'), JSON.stringify({ pid: 3, updatedAt: now - 1_000 }));
|
|
16
|
+
|
|
17
|
+
const removed = pruneStaleChannelClientHeartbeats(root, {
|
|
18
|
+
now,
|
|
19
|
+
maxAgeMs: 30_000,
|
|
20
|
+
pidAlive: (pid) => pid !== 3,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
assert.equal(removed, 2);
|
|
24
|
+
assert.equal(existsSync(join(root, '1.json')), true);
|
|
25
|
+
assert.equal(existsSync(join(root, '2.json')), false);
|
|
26
|
+
assert.equal(existsSync(join(root, '3.json')), false);
|
|
27
|
+
} finally {
|
|
28
|
+
rmSync(root, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { fork } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { mkdirSync, rmSync, writeFile } from 'node:fs';
|
|
3
|
+
import { mkdirSync, readFileSync, readdirSync, rmSync, writeFile } from 'node:fs';
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { appendBuffered } from '../runtime/shared/buffered-appender.mjs';
|
|
7
|
+
import { isPidAlive } from '../runtime/shared/pid-liveness.mjs';
|
|
7
8
|
import { ensurePrivateRuntimeRoot, resolveRuntimeRoot } from '../runtime/shared/runtime-root.mjs';
|
|
8
9
|
import { detachedSpawnOpts } from '../runtime/shared/spawn-flags.mjs';
|
|
9
10
|
import { scrubLoaderVars } from '../runtime/agent/orchestrator/tools/env-scrub.mjs';
|
|
@@ -34,6 +35,34 @@ function logLine(path, line) {
|
|
|
34
35
|
const CHANNEL_WORKER_EXIT_CLEANUPS = new Set();
|
|
35
36
|
let channelWorkerExitHookInstalled = false;
|
|
36
37
|
|
|
38
|
+
export function pruneStaleChannelClientHeartbeats(clientDir, {
|
|
39
|
+
now = Date.now(),
|
|
40
|
+
maxAgeMs = 30_000,
|
|
41
|
+
pidAlive = isPidAlive,
|
|
42
|
+
} = {}) {
|
|
43
|
+
let removed = 0;
|
|
44
|
+
try {
|
|
45
|
+
for (const name of readdirSync(clientDir)) {
|
|
46
|
+
if (!/^\d+\.json$/.test(name)) continue;
|
|
47
|
+
const target = join(clientDir, name);
|
|
48
|
+
let row = null;
|
|
49
|
+
try { row = JSON.parse(readFileSync(target, 'utf8')); } catch {}
|
|
50
|
+
const pid = Number(row?.pid);
|
|
51
|
+
const updatedAt = Number(row?.updatedAt);
|
|
52
|
+
const stale = !Number.isInteger(pid) || pid <= 0
|
|
53
|
+
|| !Number.isFinite(updatedAt)
|
|
54
|
+
|| now - updatedAt > Math.max(5_000, Number(maxAgeMs) || 30_000)
|
|
55
|
+
|| !pidAlive(pid);
|
|
56
|
+
if (!stale) continue;
|
|
57
|
+
try {
|
|
58
|
+
rmSync(target, { force: true });
|
|
59
|
+
removed += 1;
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
} catch {}
|
|
63
|
+
return removed;
|
|
64
|
+
}
|
|
65
|
+
|
|
37
66
|
function registerChannelWorkerExitCleanup(cleanup) {
|
|
38
67
|
if (typeof cleanup !== 'function') return () => {};
|
|
39
68
|
CHANNEL_WORKER_EXIT_CLEANUPS.add(cleanup);
|
|
@@ -82,6 +111,7 @@ export function createStandaloneChannelWorker({
|
|
|
82
111
|
try {
|
|
83
112
|
if (!clientDirReady) {
|
|
84
113
|
mkdirSync(clientDir, { recursive: true });
|
|
114
|
+
pruneStaleChannelClientHeartbeats(clientDir);
|
|
85
115
|
clientDirReady = true;
|
|
86
116
|
}
|
|
87
117
|
writeFile(clientPath, JSON.stringify({
|
|
@@ -650,9 +650,12 @@ export function createStandaloneMemoryRuntime({
|
|
|
650
650
|
const ownedChild = child;
|
|
651
651
|
const childExit = waitForExit && ownedChild && ownedChild.exitCode == null
|
|
652
652
|
? new Promise((resolveExit, rejectExit) => {
|
|
653
|
-
const onExit = () => {
|
|
653
|
+
const onExit = (code, signal) => {
|
|
654
654
|
clearTimeout(timer);
|
|
655
|
-
resolveExit(true);
|
|
655
|
+
if (code === 0) resolveExit(true);
|
|
656
|
+
else rejectExit(new Error(
|
|
657
|
+
`memory runtime exited unsuccessfully (${signal || code || 'unknown'})`,
|
|
658
|
+
));
|
|
656
659
|
};
|
|
657
660
|
const timer = setTimeout(() => {
|
|
658
661
|
ownedChild.off('exit', onExit);
|
|
@@ -677,15 +677,35 @@ export async function attachSession({
|
|
|
677
677
|
|
|
678
678
|
/** Ask a live daemon to exit. Used by dev/test teardown; normal clients just
|
|
679
679
|
* detach and let the client-grace shutdown fire. */
|
|
680
|
-
export async function shutdownDaemon(discovery = readSessionDiscovery()
|
|
680
|
+
export async function shutdownDaemon(discovery = readSessionDiscovery(), {
|
|
681
|
+
waitForExit = false,
|
|
682
|
+
timeoutMs = 15_000,
|
|
683
|
+
} = {}) {
|
|
681
684
|
if (!discovery?.port) return false;
|
|
685
|
+
const daemonPid = Number(discovery.pid);
|
|
682
686
|
try {
|
|
683
687
|
await request({
|
|
684
688
|
port: discovery.port, token: discovery.token, method: 'POST',
|
|
685
689
|
path: '/shutdown', body: {}, timeoutMs: 3000, control: true,
|
|
686
690
|
});
|
|
687
|
-
|
|
688
|
-
|
|
691
|
+
} catch {
|
|
692
|
+
return Number.isInteger(daemonPid) && daemonPid > 0 && !isPidAlive(daemonPid);
|
|
693
|
+
}
|
|
694
|
+
if (!waitForExit || !Number.isInteger(daemonPid) || daemonPid <= 0) return true;
|
|
695
|
+
const deadline = Date.now() + Math.max(1, Number(timeoutMs) || 15_000);
|
|
696
|
+
while (isPidAlive(daemonPid) && Date.now() < deadline) await delay(50);
|
|
697
|
+
if (isPidAlive(daemonPid)) {
|
|
698
|
+
throw new Error(`session daemon pid=${daemonPid} did not exit within ${timeoutMs}ms`);
|
|
699
|
+
}
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
export async function shutdownDaemonForRuntimeRoot(root, options = {}) {
|
|
704
|
+
const runtimeRoot = String(root || '').trim();
|
|
705
|
+
if (!runtimeRoot) return false;
|
|
706
|
+
const discovery = readSessionDiscovery(path.join(path.resolve(runtimeRoot), 'daemon.json'));
|
|
707
|
+
if (!discovery) return false;
|
|
708
|
+
return shutdownDaemon(discovery, options);
|
|
689
709
|
}
|
|
690
710
|
|
|
691
711
|
// ── Shared per-process attachment ────────────────────────────────────────────
|
package/src/tui/session/turn.mjs
CHANGED
|
@@ -1033,6 +1033,14 @@ export function createRunTurn(bag) {
|
|
|
1033
1033
|
invalidateExact: compactChanged,
|
|
1034
1034
|
compactedEstimateTokens: compactChanged ? event?.afterTokens : null,
|
|
1035
1035
|
});
|
|
1036
|
+
// syncContextStats only STAGES its patch on the draft (context-state
|
|
1037
|
+
// mjs stages through updateState); publication is the caller's, and
|
|
1038
|
+
// every other compact path already follows with this exact set. The
|
|
1039
|
+
// auto path did not, so the post-compact gauge reached React only if
|
|
1040
|
+
// some later mutation happened to carry it — the manual path
|
|
1041
|
+
// repainted immediately while auto looked stuck on the pre-compact
|
|
1042
|
+
// number (user: 오토 컴팩트 이후에 컨텍스트 원형바가 동기화가 안됨).
|
|
1043
|
+
set({ ...routeState(), stats: { ...getState().stats } });
|
|
1036
1044
|
},
|
|
1037
1045
|
onStageChange: async (stage, detail = null) => {
|
|
1038
1046
|
if (!markTurnProgress(`stage:${String(stage || '')}`)) return;
|
|
@@ -1200,6 +1208,10 @@ export function createRunTurn(bag) {
|
|
|
1200
1208
|
finalizeToolHeaders();
|
|
1201
1209
|
flushStreamBatch(); // force-flush any batched streaming text before finalization writes
|
|
1202
1210
|
syncContextStats({ allowEstimated: true });
|
|
1211
|
+
// Terminal reading for the turn. Without its own publication the last
|
|
1212
|
+
// gauge number React ever saw is the one the previous usage delta
|
|
1213
|
+
// carried, so an idle session sat on a stale count until the next turn.
|
|
1214
|
+
set({ stats: { ...getState().stats } });
|
|
1203
1215
|
|
|
1204
1216
|
const finalText = result?.content != null ? String(result.content) : '';
|
|
1205
1217
|
// Strip text already sealed as its own item(s) this turn (a tool
|