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.
Files changed (53) hide show
  1. package/README.md +28 -11
  2. package/package.json +1 -1
  3. package/src/headless-exec.mjs +16 -3
  4. package/src/headless-exec.test.mjs +44 -1
  5. package/src/rules/shared/30-exploration.md +2 -1
  6. package/src/runtime/agent/orchestrator/session/compact/handoff.mjs +2 -4
  7. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +18 -33
  8. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +21 -42
  9. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +20 -125
  10. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -20
  11. package/src/runtime/agent/orchestrator/tools/builtin/absolute-glob-expand.test.mjs +63 -0
  12. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +29 -10
  13. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
  14. package/src/runtime/agent/orchestrator/tools/builtin/enoent-outside-project.test.mjs +56 -0
  15. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +6 -2
  16. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +32 -16
  17. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +28 -0
  18. package/src/runtime/agent/orchestrator/tools/builtin/grep-single-file-rescue.test.mjs +106 -0
  19. package/src/runtime/agent/orchestrator/tools/builtin/lib/absolute-glob-expand.mjs +129 -0
  20. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-single-file-rescue.mjs +218 -0
  21. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +33 -0
  22. package/src/runtime/agent/orchestrator/tools/builtin/noise-dir-visibility.test.mjs +69 -0
  23. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +33 -6
  24. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +26 -6
  25. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +7 -66
  26. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +11 -0
  27. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +25 -1
  28. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +60 -5
  29. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +54 -1
  30. package/src/runtime/agent/orchestrator/tools/builtin/write-symlink.test.mjs +153 -0
  31. package/src/runtime/agent/orchestrator/tools/code-graph/aggregate-anchor-relocation.test.mjs +55 -0
  32. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +54 -3
  33. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  34. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +44 -10
  35. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +57 -5
  36. package/src/runtime/agent/orchestrator/tools/patch/patch-symlink.test.mjs +141 -0
  37. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +7 -3
  38. package/src/runtime/agent/orchestrator/tools/patch/v4a-pure-move.test.mjs +57 -0
  39. package/src/runtime/agent/orchestrator/tools/patch/v4a-section-coalesce.test.mjs +81 -0
  40. package/src/runtime/memory/index.mjs +2 -4
  41. package/src/runtime/memory/lib/pg/process.mjs +29 -2
  42. package/src/runtime/memory/lib/pg/process.test.mjs +36 -0
  43. package/src/runtime/memory/lib/pg/supervisor.mjs +5 -1
  44. package/src/runtime/memory/lib/query-handlers.mjs +7 -6
  45. package/src/runtime/memory/lib/recall-format.mjs +13 -31
  46. package/src/runtime/shared/pristine-execution-contract.json +3 -0
  47. package/src/runtime/shared/pristine-execution.mjs +5 -2
  48. package/src/session-runtime/lifecycle-api.mjs +1 -0
  49. package/src/standalone/channel-worker-heartbeat.test.mjs +30 -0
  50. package/src/standalone/channel-worker.mjs +31 -1
  51. package/src/standalone/memory-runtime-proxy.mjs +5 -2
  52. package/src/standalone/session-client.mjs +23 -3
  53. 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: 10, retryDelay: 100 });
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
- return true;
688
- } catch { return false; }
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 ────────────────────────────────────────────
@@ -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