mixdog 0.9.149 → 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 (113) hide show
  1. package/README.md +28 -12
  2. package/package.json +1 -1
  3. package/src/headless-exec.mjs +40 -4
  4. package/src/headless-exec.test.mjs +116 -1
  5. package/src/rules/lead/01-general.md +3 -2
  6. package/src/rules/shared/00-general.md +1 -0
  7. package/src/rules/shared/10-tool-workflow.md +11 -12
  8. package/src/rules/shared/30-exploration.md +4 -3
  9. package/src/rules/shared/60-verification.md +4 -3
  10. package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +0 -2
  11. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +1 -4
  12. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +0 -18
  13. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +0 -3
  14. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -1
  15. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +4 -72
  16. package/src/runtime/agent/orchestrator/session/approval-mode.mjs +0 -8
  17. package/src/runtime/agent/orchestrator/session/compact/handoff.mjs +2 -4
  18. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +18 -33
  19. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +21 -42
  20. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +20 -125
  21. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +50 -8
  22. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.test.mjs +31 -1
  23. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +27 -4
  24. package/src/runtime/agent/orchestrator/session/store/serialize.test.mjs +60 -0
  25. package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +78 -0
  26. package/src/runtime/agent/orchestrator/session/task-wait-control.test.mjs +78 -0
  27. package/src/runtime/agent/orchestrator/tools/builtin/absence-absorption.test.mjs +43 -0
  28. package/src/runtime/agent/orchestrator/tools/builtin/absolute-glob-expand.test.mjs +63 -0
  29. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +8 -0
  30. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +29 -10
  31. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -7
  32. package/src/runtime/agent/orchestrator/tools/builtin/enoent-outside-project.test.mjs +56 -0
  33. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +6 -2
  34. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +50 -23
  35. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +50 -1
  36. package/src/runtime/agent/orchestrator/tools/builtin/grep-output-budget.test.mjs +31 -0
  37. package/src/runtime/agent/orchestrator/tools/builtin/grep-single-file-rescue.test.mjs +106 -0
  38. package/src/runtime/agent/orchestrator/tools/builtin/lib/absolute-glob-expand.mjs +129 -0
  39. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +2 -1
  40. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-single-file-rescue.mjs +218 -0
  41. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +9 -2
  42. package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +6 -0
  43. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +50 -2
  44. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +7 -1
  45. package/src/runtime/agent/orchestrator/tools/builtin/noise-dir-visibility.test.mjs +69 -0
  46. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +33 -6
  47. package/src/runtime/agent/orchestrator/tools/builtin/read-glob-survey.test.mjs +71 -0
  48. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +6 -1
  49. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +7 -0
  50. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +54 -8
  51. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +90 -51
  52. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.test.mjs +56 -0
  53. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +25 -0
  54. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.test.mjs +16 -0
  55. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +30 -1
  56. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +60 -5
  57. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +54 -1
  58. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +49 -24
  59. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +1 -0
  60. package/src/runtime/agent/orchestrator/tools/builtin/write-symlink.test.mjs +153 -0
  61. package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -1
  62. package/src/runtime/agent/orchestrator/tools/code-graph/aggregate-anchor-relocation.test.mjs +55 -0
  63. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +54 -3
  64. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +16 -0
  65. package/src/runtime/agent/orchestrator/tools/env-scrub.test.mjs +33 -1
  66. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  67. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +9 -1
  68. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +44 -10
  69. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +57 -5
  70. package/src/runtime/agent/orchestrator/tools/patch/patch-symlink.test.mjs +141 -0
  71. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +7 -3
  72. package/src/runtime/agent/orchestrator/tools/patch/v4a-pure-move.test.mjs +57 -0
  73. package/src/runtime/agent/orchestrator/tools/patch/v4a-section-coalesce.test.mjs +81 -0
  74. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +42 -23
  75. package/src/runtime/media/renditions.mjs +7 -1
  76. package/src/runtime/memory/index.mjs +2 -4
  77. package/src/runtime/memory/lib/pg/process.mjs +29 -2
  78. package/src/runtime/memory/lib/pg/process.test.mjs +36 -0
  79. package/src/runtime/memory/lib/pg/supervisor.mjs +5 -1
  80. package/src/runtime/memory/lib/query-handlers.mjs +13 -19
  81. package/src/runtime/memory/lib/recall-format.mjs +13 -31
  82. package/src/runtime/memory/tool-defs.mjs +2 -2
  83. package/src/runtime/shared/memory-snapshot.mjs +57 -4
  84. package/src/runtime/shared/pristine-execution-contract.json +3 -0
  85. package/src/runtime/shared/pristine-execution.mjs +6 -3
  86. package/src/runtime/shared/tool-surface.mjs +6 -0
  87. package/src/runtime/shared/tool-surface.test.mjs +8 -0
  88. package/src/session-runtime/lifecycle-api.mjs +1 -0
  89. package/src/session-runtime/session-turn-api.mjs +5 -0
  90. package/src/standalone/agent-dispatch-broker.mjs +25 -80
  91. package/src/standalone/channel-worker-heartbeat.test.mjs +30 -0
  92. package/src/standalone/channel-worker.mjs +31 -1
  93. package/src/standalone/daemon.mjs +9 -9
  94. package/src/standalone/memory-runtime-proxy.mjs +5 -2
  95. package/src/standalone/session-client.mjs +23 -3
  96. package/src/standalone/session-runtime-host-health.test.mjs +36 -0
  97. package/src/standalone/session-runtime-host.mjs +26 -76
  98. package/src/standalone/session-runtime-worker.mjs +97 -0
  99. package/src/tui/App.jsx +3 -2
  100. package/src/tui/app/use-global-key-input.mjs +9 -0
  101. package/src/tui/app/use-mouse-input.mjs +77 -14
  102. package/src/tui/components/Spinner.jsx +1 -1
  103. package/src/tui/dist/index.mjs +83 -23
  104. package/src/tui/lib/voice-setup.mjs +3 -2
  105. package/src/tui/session/completion-card-restore.test.mjs +18 -0
  106. package/src/tui/session/session-api-ext.mjs +2 -1
  107. package/src/tui/session/session-api.mjs +8 -1
  108. package/src/tui/session/task-wait-submit.test.mjs +80 -0
  109. package/src/tui/session/turn.mjs +58 -6
  110. package/src/tui/spinner-verbs.mjs +1 -0
  111. package/src/workflows/default/WORKFLOW.md +12 -19
  112. package/src/workflows/headless/WORKFLOW.md +11 -0
  113. package/src/workflows/solo/WORKFLOW.md +6 -11
@@ -444,7 +444,7 @@ export async function stopPg({ runtimeDir, pgdataDir }) {
444
444
  })
445
445
 
446
446
  if (r.status !== 0) {
447
- const msg = r.stderr?.toString() || r.stdout?.toString() || ''
447
+ const msg = r.error?.message || r.stderr?.toString() || r.stdout?.toString() || ''
448
448
  // Stale postmaster.pid — PG is already down; clean up and continue.
449
449
  if (
450
450
  msg.includes('no server running') ||
@@ -453,10 +453,16 @@ export async function stopPg({ runtimeDir, pgdataDir }) {
453
453
  ) {
454
454
  __mixdogMemoryLog(`[pg-process] stopPg: already stopped (${msg.slice(0, 80)})\n`)
455
455
  try { rmSync(join(pgdataDir, 'postmaster.pid'), { force: true }) } catch {}
456
+ return
456
457
  } else {
457
458
  __mixdogMemoryLog(`[pg-process] pg_ctl stop warning: ${msg}\n`)
459
+ throw new Error(`pg_ctl stop failed${msg ? `: ${msg.trim()}` : ''}`)
458
460
  }
459
461
  }
462
+ const remaining = readPostmasterInfo(pgdataDir)
463
+ if (remaining.pid && isPidAlive(remaining.pid)) {
464
+ throw new Error(`postgres postmaster pid=${remaining.pid} survived pg_ctl stop`)
465
+ }
460
466
  }
461
467
 
462
468
  // Synchronous best-effort variant for a process 'exit' hook (only sync work is
@@ -494,6 +500,7 @@ export function classifyOrphanTempPostmaster({
494
500
  args,
495
501
  uptimeSec,
496
502
  tempRoot,
503
+ ownerAlive = false,
497
504
  minUptimeSec = ORPHAN_TEMP_PG_MIN_UPTIME_SEC,
498
505
  }) {
499
506
  const commandLine = String(args || '')
@@ -506,9 +513,24 @@ export function classifyOrphanTempPostmaster({
506
513
  const root = normalizePathForMatch(tempRoot).replace(/\/+$/, '')
507
514
  if (!root || !dataDir.startsWith(`${root}/`)) return false
508
515
  if (!dataDir.includes('mixdog')) return false
516
+ if (ownerAlive) return false
509
517
  return Number(uptimeSec) >= Math.max(60, Number(minUptimeSec) || ORPHAN_TEMP_PG_MIN_UPTIME_SEC)
510
518
  }
511
519
 
520
+ function tempPostmasterOwnerAlive(args) {
521
+ const commandLine = String(args || '')
522
+ const dataDirMatch = commandLine.match(/(?:^|\s)-D\s+"?([^"]+?)"?(?:\s|$)/)
523
+ if (!dataDirMatch) return false
524
+ const dataRoot = join(dataDirMatch[1], '..')
525
+ for (const name of ['memory-runtime-owner.json', 'daemon-owner.json']) {
526
+ try {
527
+ const owner = JSON.parse(readFileSync(join(dataRoot, name), 'utf8'))
528
+ if (Number(owner?.pid) > 0 && isPidAlive(Number(owner.pid))) return true
529
+ } catch {}
530
+ }
531
+ return false
532
+ }
533
+
512
534
  function listPostmasterRows() {
513
535
  if (process.platform === 'win32') {
514
536
  const result = spawnSync('powershell', ['-NoProfile', '-Command',
@@ -545,7 +567,12 @@ export function sweepOrphanTempPostmasters({ tempRoot, minUptimeSec } = {}) {
545
567
  const root = tempRoot || os.tmpdir()
546
568
  for (const row of listPostmasterRows()) {
547
569
  if (!row.pid || row.pid === process.pid) continue
548
- if (!classifyOrphanTempPostmaster({ ...row, tempRoot: root, minUptimeSec })) continue
570
+ if (!classifyOrphanTempPostmaster({
571
+ ...row,
572
+ tempRoot: root,
573
+ ownerAlive: tempPostmasterOwnerAlive(row.args),
574
+ minUptimeSec,
575
+ })) continue
549
576
  try {
550
577
  process.kill(row.pid)
551
578
  reaped += 1
@@ -0,0 +1,36 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { classifyOrphanTempPostmaster } from './process.mjs';
5
+
6
+ const tempRoot = process.platform === 'win32'
7
+ ? 'C:/Users/test/AppData/Local/Temp'
8
+ : '/tmp';
9
+ const args = `"${tempRoot}/mixdog-headless-pristine-test/data/runtime/bin/postgres" `
10
+ + `-D "${tempRoot}/mixdog-headless-pristine-test/data/pgdata"`;
11
+
12
+ test('old unowned temp postmasters are sweep candidates', () => {
13
+ assert.equal(classifyOrphanTempPostmaster({
14
+ args,
15
+ uptimeSec: 3600,
16
+ tempRoot,
17
+ ownerAlive: false,
18
+ }), true);
19
+ });
20
+
21
+ test('a live owner protects a long-running temp postmaster', () => {
22
+ assert.equal(classifyOrphanTempPostmaster({
23
+ args,
24
+ uptimeSec: 3600,
25
+ tempRoot,
26
+ ownerAlive: true,
27
+ }), false);
28
+ });
29
+
30
+ test('the official data directory is never a sweep candidate', () => {
31
+ assert.equal(classifyOrphanTempPostmaster({
32
+ args: 'postgres -D "/home/test/.mixdog/data/pgdata"',
33
+ uptimeSec: 3600,
34
+ tempRoot,
35
+ }), false);
36
+ });
@@ -458,6 +458,7 @@ async function tryReusePgInstance({ pgdata, runtimeDir, healthcheckPg, source =
458
458
  // patchActiveInstance) so a concurrent restart/owner change is not
459
459
  // clobbered by this reuse-time snapshot.
460
460
  _restampReuseOwner({ pgdata, port: existingPort });
461
+ scheduleOrphanTempPostmasterSweep();
461
462
  return { host: '127.0.0.1', port: existingPort, runtimeDir: existingRtDir, pgdataDir: pgdata };
462
463
  }
463
464
  } catch {}
@@ -475,6 +476,7 @@ async function tryReusePgInstance({ pgdata, runtimeDir, healthcheckPg, source =
475
476
  pg_pgdata: pgdata,
476
477
  pg_runtime_dir: runtimeDir,
477
478
  }, { timeoutMs: 1000, background: true });
479
+ scheduleOrphanTempPostmasterSweep();
478
480
  return { host: '127.0.0.1', port: pm.port, runtimeDir, pgdataDir: pgdata };
479
481
  }
480
482
  } catch {}
@@ -674,6 +676,7 @@ export async function stopPgForShutdown() {
674
676
  __mixdogMemoryLog(`[supervisor-pg] PG stopped (via active-instance.json) on shutdown\n`);
675
677
  } catch (e) {
676
678
  __mixdogMemoryLog(`[supervisor-pg] stopPg error on shutdown (no _live): ${e?.message}\n`);
679
+ throw e;
677
680
  }
678
681
  // Bounded sync clear (<=4s: 2s + one 2s retry). A residual stale pg_port is
679
682
  // tolerated — readers healthcheck-verify the port before reuse.
@@ -681,14 +684,15 @@ export async function stopPgForShutdown() {
681
684
  return;
682
685
  }
683
686
  const snap = _live;
684
- _live = null;
685
687
  _ensureInFlight = null;
686
688
  try {
687
689
  const { stopPg } = await _getPgProc();
688
690
  await stopPg({ runtimeDir: snap.runtimeDir, pgdataDir: snap.pgdata });
691
+ _live = null;
689
692
  __mixdogMemoryLog(`[supervisor-pg] PG stopped gracefully on shutdown\n`);
690
693
  } catch (e) {
691
694
  __mixdogMemoryLog(`[supervisor-pg] stopPg error on shutdown: ${e?.message}\n`);
695
+ throw e;
692
696
  }
693
697
  // Bounded sync clear (<=4s: 2s + one 2s retry). A residual stale pg_port is
694
698
  // tolerated — readers healthcheck-verify the port before reuse.
@@ -136,7 +136,7 @@ export function createQueryHandlers({
136
136
  // Over-fetch before compact-only dedupe so duplicated legacy rows cannot
137
137
  // consume the requested page and hide distinct older context.
138
138
  const fetchLimit = compactHandoff
139
- ? Math.min(1000, Math.max(limit, limit * 10))
139
+ ? null
140
140
  : compactDigest
141
141
  ? Math.min(100, Math.max(limit, limit * 4))
142
142
  : limit
@@ -189,16 +189,17 @@ export function createQueryHandlers({
189
189
  })
190
190
  where.push(`(${clauses.join(' OR ')})`)
191
191
  }
192
- params.push(fetchLimit)
192
+ if (fetchLimit != null) params.push(fetchLimit)
193
+ const limitClause = fetchLimit == null ? '' : `LIMIT $${params.length}`
193
194
  let rows = (await db.query(`
194
195
  SELECT id, ts, role, content, source_ref, session_id, source_turn, time_source, chunk_root, is_root,
195
196
  element, category, summary, status, score, last_seen_at, project_id
196
197
  FROM entries
197
198
  WHERE ${where.join(' AND ')}
198
199
  ORDER BY ts DESC, source_turn DESC NULLS LAST, id DESC
199
- LIMIT $${params.length}
200
+ ${limitClause}
200
201
  `, params)).rows
201
- if (rows.length < fetchLimit) {
202
+ if (fetchLimit != null && rows.length < fetchLimit) {
202
203
  const seen = new Set(rows.map((row) => Number(row.id)).filter((id) => Number.isFinite(id)))
203
204
  const fillLimit = Math.max(0, fetchLimit - rows.length)
204
205
  const fillWhere = ['session_id = $1', 'id <> ALL($2::bigint[])', '(is_root = 1 OR chunk_root IS NULL OR chunk_root = id)']
@@ -220,7 +221,7 @@ export function createQueryHandlers({
220
221
  : []
221
222
  if (fillRows.length > 0) rows = [...rows, ...fillRows]
222
223
  }
223
- if (args.includeMembers === true && !compactHandoff) {
224
+ if (args.includeMembers === true) {
224
225
  const rootIds = rows
225
226
  .filter((row) => Number(row.is_root) === 1)
226
227
  .map((row) => Number(row.id))
@@ -243,7 +244,7 @@ export function createQueryHandlers({
243
244
  }
244
245
  }
245
246
  }
246
- if (compactHandoff) rows = compactHandoffRows(rows, limit)
247
+ if (compactHandoff) rows = compactHandoffRows(rows)
247
248
  else if (compactDigest) rows = compactDigestRows(rows, limit)
248
249
  return { text: renderEntryLines(rows, { pendingMarks: !compactDigest && !compactHandoff }) }
249
250
  }
@@ -507,18 +508,11 @@ export function createQueryHandlers({
507
508
  ? args.query.some((v) => String(v || '').trim())
508
509
  : String(args.query ?? '').trim() !== ''
509
510
  let sort = args.sort != null ? String(args.sort) : (hasQueryForSort ? 'importance' : 'date')
510
- // Chunk content is the primary recall output. Members default to true so
511
- // callers receive the raw chunk leaves (the cycle1-produced semantic
512
- // chunks) rather than just the root's cycle2-compressed summary line.
513
- // Explicit `includeMembers:false` keeps the legacy summary-only mode.
514
- const includeMembers = args.includeMembers !== false
515
- // Raw leg defaults ON: recall never drains/chunks inline anymore, so raw
516
- // (unchunked) rows — embedded by the always-on post-ingest/tick flush —
517
- // are the only way to see content cycle1 hasn't reached yet, regardless
518
- // of the recap toggle. The importance/query search leg only ranks chunked
519
- // entries; readRawRowsInWindow interleaves the raw rows into the hybrid
520
- // list. Explicit includeRaw:false keeps the chunked-only view.
521
- const includeRaw = args.includeRaw !== false
511
+ // Root summaries are the compact default recall output. Chunk members and
512
+ // unchunked raw/episode rows are explicit expansion legs for callers that
513
+ // need the underlying transcript evidence.
514
+ const includeMembers = args.includeMembers === true
515
+ const includeRaw = args.includeRaw === true
522
516
  const includeArchived = args.includeArchived !== false
523
517
  const category = args.category
524
518
  const temporal = parsePeriod(period, Boolean(query))
@@ -1094,7 +1088,7 @@ export function createQueryHandlers({
1094
1088
  // too and merge chronologically — original text first, no summaries.
1095
1089
  // Query-less + includeRaw:false callers keep the roots-only view.
1096
1090
  let merged = rows
1097
- if (sort === 'date' && args.includeRaw !== false) {
1091
+ if (sort === 'date' && includeRaw) {
1098
1092
  const rawRows = await readRawRowsInWindow(
1099
1093
  db,
1100
1094
  filters.ts_from ?? temporal?.startMs ?? null,
@@ -399,16 +399,21 @@ export function compactDigestRows(rows, limit = 30) {
399
399
  .slice(0, cap)
400
400
  }
401
401
 
402
- export function compactHandoffRows(rows, limit = 100) {
403
- const cap = Math.max(1, Math.floor(Number(limit) || 100))
404
- const deduped = collapseNearDuplicateRows(collapseExactDuplicateRows(Array.isArray(rows) ? rows : []))
405
- .filter((row) => !row?._dupStub)
402
+ export function compactHandoffRows(rows) {
403
+ const deduped = Array.isArray(rows) ? rows : []
406
404
  const roots = []
407
405
  const raw = []
408
406
  for (const row of deduped) {
409
407
  if (Number(row?.is_root) === 1) {
410
- const { members: _members, ...summaryRow } = row
411
- roots.push(summaryRow)
408
+ if (String(row?.summary ?? '').trim()) {
409
+ const { members: _members, ...summaryRow } = row
410
+ roots.push(summaryRow)
411
+ } else if (Array.isArray(row?.members) && row.members.length > 0) {
412
+ raw.push(...row.members)
413
+ } else {
414
+ const { members: _members, ...rawRoot } = row
415
+ raw.push(rawRoot)
416
+ }
412
417
  } else if (row?.chunk_root == null || Number(row.chunk_root) === Number(row.id)) {
413
418
  raw.push(row)
414
419
  }
@@ -419,32 +424,9 @@ export function compactHandoffRows(rows, limit = 100) {
419
424
  || (Number(a?.ts) || 0) - (Number(b?.ts) || 0)
420
425
  || (Number(a?.id) || 0) - (Number(b?.id) || 0)
421
426
  ))
422
- const endpoints = []
423
- let userRow = null
424
- let assistantRow = null
425
- let fallbackRow = null
426
- const flushEvent = () => {
427
- if (assistantRow || fallbackRow) endpoints.push(assistantRow || fallbackRow)
428
- if (userRow) endpoints.push(userRow)
429
- userRow = null
430
- assistantRow = null
431
- fallbackRow = null
432
- }
433
- for (const row of raw) {
434
- if (row?.role === 'user') {
435
- flushEvent()
436
- userRow = row
437
- } else if (row?.role === 'assistant') {
438
- assistantRow = row
439
- } else {
440
- fallbackRow = row
441
- }
442
- }
443
- flushEvent()
444
-
445
- const selected = [...roots, ...endpoints]
427
+ const selected = [...roots, ...raw]
446
428
  selected.sort(compareRecallNewestFirst)
447
- return compactDigestRows(selected, cap)
429
+ return selected
448
430
  }
449
431
 
450
432
  // Compact session label for group headers: keep short ids verbatim, shorten
@@ -46,8 +46,8 @@ export const TOOL_DEFS = [
46
46
  // handler validates against VALID_CATEGORY.
47
47
  category: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Category filter, string or array: rule|constraint|decision|fact|goal|preference|task|issue.' },
48
48
  includeArchived: { type: 'boolean', description: 'Include archived entries; default true.' },
49
- includeMembers: { type: 'boolean', description: 'Include chunk members; default true.' },
50
- includeRaw: { type: 'boolean', description: 'Include raw/episode rows; default true.' },
49
+ includeMembers: { type: 'boolean', default: false, description: 'Include chunk members; default false.' },
50
+ includeRaw: { type: 'boolean', default: false, description: 'Include raw/episode rows; default false.' },
51
51
  projectScope: { type: 'string', description: 'Pool: inferred from cwd, common, all, or slug.' },
52
52
  },
53
53
  additionalProperties: false,
@@ -2,6 +2,7 @@ import { execFile } from 'node:child_process';
2
2
  import {
3
3
  mkdir,
4
4
  open,
5
+ readFile,
5
6
  rename,
6
7
  stat,
7
8
  unlink,
@@ -59,6 +60,51 @@ function systemMemory() {
59
60
  }
60
61
  }
61
62
 
63
+ /**
64
+ * Private working set of THIS process. Raw RSS double-counts image pages the
65
+ * runtime binary shares across every mixdog process (~90-100MB each measured
66
+ * on the desktop bundle), so trend analysis needs the private number beside
67
+ * memoryUsage. Best-effort: one bounded probe per sample, null when the
68
+ * platform offers no cheap source.
69
+ */
70
+ async function selfPrivateMemory() {
71
+ if (process.platform === 'win32') {
72
+ try {
73
+ const { stdout } = await execFileAsync('powershell.exe', [
74
+ '-NoProfile', '-NonInteractive', '-Command',
75
+ `Get-CimInstance Win32_PerfFormattedData_PerfProc_Process -Filter "IDProcess=${process.pid}" | Select-Object -First 1 -ExpandProperty WorkingSetPrivate`,
76
+ ], {
77
+ encoding: 'utf8',
78
+ timeout: 3000,
79
+ windowsHide: true,
80
+ });
81
+ const bytes = Number(String(stdout).trim());
82
+ return Number.isFinite(bytes) && bytes >= 0 ? { workingSetPrivateBytes: bytes } : null;
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+ if (process.platform === 'linux') {
88
+ try {
89
+ const rollup = await readFile('/proc/self/smaps_rollup', 'utf8');
90
+ const kb = (field) => {
91
+ const match = rollup.match(new RegExp(`^${field}:\\s+(\\d+) kB$`, 'm'));
92
+ return match ? Number(match[1]) * 1024 : null;
93
+ };
94
+ const privateBytes = (kb('Private_Clean') ?? 0) + (kb('Private_Dirty') ?? 0);
95
+ const pssBytes = kb('Pss');
96
+ if (privateBytes <= 0 && pssBytes === null) return null;
97
+ return {
98
+ workingSetPrivateBytes: privateBytes > 0 ? privateBytes : null,
99
+ proportionalSetSizeBytes: pssBytes,
100
+ };
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+ return null;
106
+ }
107
+
62
108
  async function pathExists(path) {
63
109
  try {
64
110
  await stat(path);
@@ -134,6 +180,9 @@ function parseWindowsProcesses(stdout) {
134
180
  name: String(row?.Name || ''),
135
181
  pid: Number(row?.ProcessId),
136
182
  workingSetMb: workingSetMb(row?.WorkingSetSize),
183
+ workingSetPrivateMb: Number.isFinite(Number(row?.WorkingSetPrivate))
184
+ ? workingSetMb(row.WorkingSetPrivate)
185
+ : null,
137
186
  commandLine: nullableProcessText(row?.CommandLine),
138
187
  parentPid: nullablePid(row?.ParentProcessId),
139
188
  }))
@@ -164,10 +213,10 @@ async function topProcesses() {
164
213
  if (process.platform === 'win32') {
165
214
  const { stdout } = await execFileAsync('powershell.exe', [
166
215
  '-NoProfile', '-NonInteractive', '-Command',
167
- 'Get-CimInstance Win32_Process | Sort-Object WorkingSetSize -Descending | Select-Object -First 8 ProcessId,Name,WorkingSetSize,CommandLine,ParentProcessId | ConvertTo-Json -Compress',
216
+ '$private = @{}; Get-CimInstance Win32_PerfFormattedData_PerfProc_Process | ForEach-Object { $private[[int64]$_.IDProcess] = $_.WorkingSetPrivate }; Get-CimInstance Win32_Process | Sort-Object WorkingSetSize -Descending | Select-Object -First 8 ProcessId,Name,WorkingSetSize,CommandLine,ParentProcessId,@{n=\'WorkingSetPrivate\';e={$private[[int64]$_.ProcessId]}} | ConvertTo-Json -Compress',
168
217
  ], {
169
218
  encoding: 'utf8',
170
- timeout: 3000,
219
+ timeout: 5000,
171
220
  windowsHide: true,
172
221
  });
173
222
  return parseWindowsProcesses(stdout);
@@ -200,19 +249,23 @@ function baseSnapshot(reason) {
200
249
  async function captureMemoryPressureSnapshot(reason = 'memory-pressure') {
201
250
  if (!enabled()) return false;
202
251
  const entry = baseSnapshot(reason);
203
- const processes = await topProcesses();
252
+ const [privateMemory, processes] = await Promise.all([selfPrivateMemory(), topProcesses()]);
253
+ if (privateMemory) entry.privateMemory = privateMemory;
204
254
  if (processes) entry.topProcesses = processes;
205
255
  return appendSnapshot(entry);
206
256
  }
207
257
 
208
258
  /**
209
- * Periodic samples stay cheap, but once host free memory drops below the
259
+ * Periodic samples stay light — one bounded private-WS probe beside
260
+ * process.memoryUsage() — and once host free memory drops below the
210
261
  * attribution threshold the sample upgrades to a full top-process snapshot so
211
262
  * post-mortem analysis can name the consumer.
212
263
  */
213
264
  async function capturePeriodicMemorySnapshot() {
214
265
  if (!enabled()) return false;
215
266
  const entry = baseSnapshot('periodic');
267
+ const privateMemory = await selfPrivateMemory();
268
+ if (privateMemory) entry.privateMemory = privateMemory;
216
269
  const freeMemoryBytes = Number(entry.systemMemory?.freeMemoryBytes);
217
270
  if (Number.isFinite(freeMemoryBytes) && freeMemoryBytes < PERIODIC_ATTRIBUTION_FREE_BYTES) {
218
271
  const processes = await topProcesses();
@@ -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": [
@@ -84,7 +84,7 @@ export function buildMinimalPristineConfig({ provider, model, effort, fast } = {
84
84
  ...route,
85
85
  }],
86
86
  default: 'exec-explicit-route',
87
- workflow: { active: 'solo' },
87
+ workflow: { active: 'headless' },
88
88
  workflowRoutes: {},
89
89
  agents: {},
90
90
  modelSettings,
@@ -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;
@@ -69,6 +69,12 @@ export {
69
69
  isMemorySurface,
70
70
  };
71
71
 
72
+ export function isTaskWaitToolCall(name, args = {}) {
73
+ if (normalizeToolName(name) !== 'task') return false;
74
+ const parsed = parseToolArgs(args);
75
+ return String(parsed?.action || '').trim().toLowerCase() === 'wait';
76
+ }
77
+
72
78
  function patchOperationProfile(args = {}) {
73
79
  const a = parseToolArgs(args);
74
80
  const patchText = String(a.patch ?? '');
@@ -5,6 +5,7 @@ import {
5
5
  aggregateDoneCategories,
6
6
  formatAggregateHeader,
7
7
  formatToolActionHeader,
8
+ isTaskWaitToolCall,
8
9
  toolLoadingTargets,
9
10
  } from './tool-surface.mjs';
10
11
  import { deriveToolCardModel } from './tool-card-model.mjs';
@@ -77,3 +78,10 @@ test('aggregate loading cards preserve comma-separated tool and skill names', ()
77
78
  });
78
79
  assert.equal(model.labelText, 'Loaded grep, setup, memory');
79
80
  });
81
+
82
+ test('task wait display policy is scoped to the wait action', () => {
83
+ assert.equal(isTaskWaitToolCall('task', { action: 'wait', task_id: 'task_1' }), true);
84
+ assert.equal(isTaskWaitToolCall('functions.task', '{"action":"WAIT","task_id":"task_1"}'), true);
85
+ assert.equal(isTaskWaitToolCall('task', { action: 'read', task_id: 'task_1' }), false);
86
+ assert.equal(isTaskWaitToolCall('shell', { action: 'wait' }), false);
87
+ });
@@ -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;
@@ -14,6 +14,7 @@ import { traceTurnTiming } from '../runtime/agent/orchestrator/agent-trace.mjs';
14
14
  import { beginTurnSnapshot, cancelTurnSnapshot, completeTurnSnapshot } from '../runtime/shared/turn-snapshot.mjs';
15
15
  import { isVisibleStreamProgress } from '../runtime/shared/stream-progress.mjs';
16
16
  import { runAbortable, settleWithin, throwIfAborted } from '../runtime/shared/abort-race.mjs';
17
+ import { interruptTaskWaitForSession } from '../runtime/agent/orchestrator/session/task-wait-control.mjs';
17
18
 
18
19
  export function splitToolStatusCounts(rows) {
19
20
  const list = Array.isArray(rows) ? rows : [];
@@ -500,6 +501,10 @@ export function createSessionTurnApi(deps) {
500
501
  agentStatus() {
501
502
  return agentStatusState();
502
503
  },
504
+ interruptTaskWait(reason = 'user-message') {
505
+ const sessionId = getSession()?.id || getReservedSessionId?.() || '';
506
+ return interruptTaskWaitForSession(sessionId, reason);
507
+ },
503
508
  // Raw model-message read for LIVE transcript consumers (remote agent
504
509
  // handoff, daemon session.read). Answers from the in-memory session so a
505
510
  // just-committed final turn is readable immediately — the debounced disk