mixdog 0.9.19 → 0.9.21

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 (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -34
  3. package/package.json +3 -2
  4. package/scripts/build-tui.mjs +6 -0
  5. package/scripts/hook-bus-test.mjs +8 -0
  6. package/scripts/log-writer-guard-smoke.mjs +131 -0
  7. package/scripts/reactive-compact-persist-smoke.mjs +22 -6
  8. package/scripts/recall-bench-cases.json +0 -1
  9. package/scripts/recall-quality-cases.json +1 -2
  10. package/scripts/session-ingest-compaction-smoke.mjs +241 -0
  11. package/scripts/tool-smoke.mjs +150 -45
  12. package/src/defaults/skills/setup/SKILL.md +327 -0
  13. package/src/help.mjs +2 -5
  14. package/src/lib/mixdog-debug.cjs +13 -0
  15. package/src/mixdog-session-runtime.mjs +7 -3328
  16. package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
  17. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -2
  18. package/src/runtime/agent/orchestrator/providers/gemini.mjs +3 -0
  19. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -10
  20. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +3 -0
  21. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +40 -3
  22. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +73 -3
  23. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +663 -0
  24. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
  25. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
  26. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
  27. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
  28. package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
  29. package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
  30. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
  31. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
  32. package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
  33. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
  34. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
  35. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
  36. package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
  37. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
  38. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
  39. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
  40. package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
  41. package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
  42. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
  43. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
  44. package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
  45. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
  46. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
  47. package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
  48. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
  49. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
  50. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +108 -2
  51. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
  52. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
  53. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
  54. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
  55. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
  56. package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
  57. package/src/runtime/channels/index.mjs +6 -2183
  58. package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
  59. package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
  60. package/src/runtime/channels/lib/network-retry.mjs +23 -0
  61. package/src/runtime/channels/lib/owned-runtime.mjs +627 -0
  62. package/src/runtime/channels/lib/runtime-paths.mjs +20 -9
  63. package/src/runtime/channels/lib/transcript-binding.mjs +336 -0
  64. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
  65. package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
  66. package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
  67. package/src/runtime/channels/lib/worker-main.mjs +777 -0
  68. package/src/runtime/memory/index.mjs +163 -1725
  69. package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
  70. package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
  71. package/src/runtime/memory/lib/http-router.mjs +811 -0
  72. package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
  73. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +6 -0
  74. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +46 -9
  75. package/src/runtime/memory/lib/memory-cycle2.mjs +87 -11
  76. package/src/runtime/memory/lib/memory-embed.mjs +28 -7
  77. package/src/runtime/memory/lib/memory-recall-store.mjs +4 -4
  78. package/src/runtime/memory/lib/memory.mjs +39 -0
  79. package/src/runtime/memory/lib/query-handlers.mjs +2 -2
  80. package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
  81. package/src/runtime/shared/atomic-file.mjs +138 -80
  82. package/src/runtime/shared/child-guardian.mjs +61 -3
  83. package/src/session-runtime/boot-profile.mjs +36 -0
  84. package/src/session-runtime/channel-config-api.mjs +70 -0
  85. package/src/session-runtime/context-status.mjs +181 -0
  86. package/src/session-runtime/env.mjs +17 -0
  87. package/src/session-runtime/lifecycle-api.mjs +242 -0
  88. package/src/session-runtime/model-route-api.mjs +198 -0
  89. package/src/session-runtime/provider-auth-api.mjs +135 -0
  90. package/src/session-runtime/resource-api.mjs +282 -0
  91. package/src/session-runtime/runtime-core.mjs +2104 -0
  92. package/src/session-runtime/session-turn-api.mjs +274 -0
  93. package/src/session-runtime/tool-catalog.mjs +18 -264
  94. package/src/session-runtime/tool-defs.mjs +2 -2
  95. package/src/session-runtime/workflow-agents-api.mjs +238 -0
  96. package/src/standalone/agent-tool.mjs +2 -2
  97. package/src/standalone/channel-worker.mjs +67 -7
  98. package/src/standalone/folder-dialog.mjs +4 -1
  99. package/src/standalone/memory-runtime-proxy.mjs +154 -17
  100. package/src/standalone/seeds.mjs +28 -1
  101. package/src/tui/App.jsx +40 -28
  102. package/src/tui/app/core-memory-picker.mjs +1 -1
  103. package/src/tui/app/doctor.mjs +175 -0
  104. package/src/tui/app/slash-commands.mjs +1 -0
  105. package/src/tui/app/slash-dispatch.mjs +9 -0
  106. package/src/tui/app/use-mouse-input.mjs +6 -0
  107. package/src/tui/app/use-transcript-scroll.mjs +77 -3
  108. package/src/tui/dist/index.mjs +2851 -2162
  109. package/src/tui/engine/context-state.mjs +145 -0
  110. package/src/tui/engine/prompt-history.mjs +27 -0
  111. package/src/tui/engine/session-api-ext.mjs +478 -0
  112. package/src/tui/engine/session-api.mjs +564 -0
  113. package/src/tui/engine/session-flow.mjs +485 -0
  114. package/src/tui/engine/turn.mjs +1078 -0
  115. package/src/tui/engine.mjs +68 -2620
  116. package/src/tui/index.jsx +7 -0
  117. package/vendor/ink/build/ink.js +16 -1
  118. package/vendor/ink/build/output.js +30 -4
  119. package/vendor/ink/build/render.js +5 -0
  120. package/src/workflows/sequential/WORKFLOW.md +0 -51
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tribgames
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,49 +1,110 @@
1
1
  # mixdog
2
2
 
3
- Standalone coding-agent CLI/TUI for running a multi-provider AI workspace from
4
- one terminal.
3
+ [![npm](https://img.shields.io/npm/v/mixdog)](https://www.npmjs.com/package/mixdog)
4
+ ![node](https://img.shields.io/badge/node-%3E%3D22-brightgreen)
5
+ ![license](https://img.shields.io/badge/license-MIT-blue)
5
6
 
6
- Mixdog combines an Ink-based terminal UI, model/provider routing, workflow
7
- agents, MCP/plugin/skill loading, memory, web search, channel integrations, and
8
- repo-focused tools for reading, editing, testing, and reviewing code.
7
+ Standalone coding-agent CLI/TUI that runs an orchestrated, multi-provider
8
+ agent workflow from one terminal built for maximum performance at minimum
9
+ cost.
9
10
 
10
- ## Highlights
11
+ Mixdog combines an Ink-based terminal UI, per-role model routing across
12
+ providers, workflow agents, MCP/plugin/skill/hook support, lightweight
13
+ memory, web search, channel integrations, and repo-native tools for reading,
14
+ editing, testing, and reviewing code.
11
15
 
12
- - **Multi-provider model picker** for Anthropic, OpenAI, Google/Gemini, xAI/Grok,
13
- DeepSeek, OpenCode Go, OAuth-backed providers, OpenAI-compatible APIs, Ollama,
14
- and LM Studio/local endpoints.
15
- - **Live model catalog** from provider `/models` endpoints, enriched with
16
- LiteLLM/models.dev metadata for context windows, output limits, pricing, tool
17
- support, reasoning, web search, and recency.
18
- - **Full-screen TUI** with slash commands, provider setup, model/workflow
19
- pickers, usage dashboards, statusline integration, resumable sessions, and
20
- detailed tool cards.
21
- - **Workflow delegation** through `/agent` and the `agent` tool, including
22
- worker, heavy-worker, reviewer, debugger, maintainer, and explorer roles.
23
- - **Repo-native tools** for `read`, `grep`, `glob`, `list`, `code_graph`,
24
- `apply_patch`, `shell`, `cwd`, `explore`, web search, and memory recall.
25
- - **Remote/event workflows** via optional Discord channel, webhook, schedule,
26
- and channel-management integrations.
16
+ ## Quick start
27
17
 
28
- ## Requirements
29
-
30
- - Node.js >= 22
31
-
32
- ## Install
18
+ Requires Node.js >= 22.
33
19
 
34
20
  ```bash
35
21
  npm install -g mixdog
36
- mixdog --help
22
+ mixdog
37
23
  ```
38
24
 
25
+ First run walks you through onboarding: provider auth, model pick, and
26
+ workflow setup. Re-run it anytime with `mixdog --onboarding`.
27
+
28
+ ## Terminal-Bench 2.1 — 89.9% (self-reported)
29
+
30
+ ![Terminal-Bench 2.1 leaderboard with mixdog](https://raw.githubusercontent.com/tribgames/mixdog/main/benchmarks/terminal-bench-2.1/tb21-leaderboard.svg)
31
+
32
+ Single-run score of **80/89 = 89.9%** (k=1) on
33
+ `terminal-bench/terminal-bench-2-1`, using a cost-reduced per-role routing
34
+ config:
35
+
36
+ | Role | Model | Effort |
37
+ |---------------------|-----------------|-------------|
38
+ | Lead (orchestrator) | Claude Fable 5 | high |
39
+ | Explorer | Claude Haiku 4.5| default |
40
+ | Worker | Claude Opus 4.8 | medium |
41
+ | Heavy worker | Claude Opus 4.8 | high |
42
+ | Reviewer | GPT-5.5 | high (fast) |
43
+
44
+ Against published model scores this places second overall — behind GPT-5.6
45
+ Sol Ultra (91.9%), ahead of GPT-5.6 Sol (88.8%) and Claude Mythos 5 (88%),
46
+ and well above the same primary model run standalone (Claude Fable 5,
47
+ 84.3%). Measured once (k=1) due to cost, so treat it as indicative — a
48
+ max-effort k=5 run is planned and is expected to land higher. Per-task
49
+ results, raw Harbor jobs, and the harness adapter live in
50
+ `benchmarks/terminal-bench-2.1/`.
51
+
52
+ Full transparency: 4 tasks refused by the primary model's safety layer were
53
+ re-run routed to Claude Opus 4.8 (effort xhigh) and passed; timeouts and
54
+ task resources were left unmodified. Raw Harbor `result.json`/`config.json`
55
+ for every constituent run are included.
56
+
57
+ ## Why mixdog
58
+
59
+ **Maximum performance at minimum cost**
60
+
61
+ - Orchestrated agent workflow that mixes providers and models per role, so
62
+ each step runs on the cheapest model that can do the job well.
63
+ - Cache-aware prompt layout and aggressive context savings across turns.
64
+ - Lean output policy plus fine-grained session management: compaction,
65
+ resumable sessions, and usage dashboards.
66
+ - A custom harness with tool-call routing tuned for the fewest, most
67
+ effective calls (`code_graph`, batched `read`/`grep`, windowed reads).
68
+
69
+ **Any provider**
70
+
71
+ - Anthropic, OpenAI, Google/Gemini, xAI/Grok, DeepSeek, OpenCode Go,
72
+ OAuth-backed providers, OpenAI-compatible APIs, Ollama, and LM Studio/local
73
+ endpoints.
74
+ - Live model catalog from provider `/models` endpoints, enriched with
75
+ LiteLLM/models.dev metadata for context windows, output limits, pricing,
76
+ tool support, reasoning, and recency.
77
+ - Customizable web search and repo exploration tools.
78
+
79
+ **Any environment**
80
+
81
+ - Full-screen TUI with slash commands, provider setup, model/workflow
82
+ pickers, statusline integration, and detailed tool cards — plus headless
83
+ role mode for scripting.
84
+ - Optional Discord/Telegram channels, webhook endpoints, cron schedules, and
85
+ voice-message transcription for remote/event-driven workflows.
86
+
87
+ **Memory**
88
+
89
+ - Lightweight memory restores prior work context across sessions.
90
+ - Important memories are automatically promoted — and demoted when stale.
91
+
92
+ **Agent-ecosystem compatible**
93
+
94
+ - Skills, MCP servers, hooks, and plugins load through standard-compatible
95
+ interfaces.
96
+ - Workflow delegation through the `agent` tool and `/agents`: worker,
97
+ heavy-worker, reviewer, debugger, maintainer, and explorer roles.
98
+
99
+ ## Run
100
+
39
101
  For local development from this checkout:
40
102
 
41
103
  ```bash
42
104
  npm install
105
+ npm start
43
106
  ```
44
107
 
45
- ## Run
46
-
47
108
  ```bash
48
109
  # Start the TUI in the current project
49
110
  mixdog
@@ -73,13 +134,14 @@ mixdog reviewer "review the current diff"
73
134
  Common slash commands:
74
135
 
75
136
  ```text
76
- /help show help
77
137
  /providers configure provider auth and local endpoints
78
- /model choose provider/model, effort, and fast mode
138
+ /model choose the main provider/model (/effort, /fast tune it)
79
139
  /workflow choose the active workflow
80
- /agents show available workflow agents
81
- /agent manage active agent tasks
82
- /mode switch tool surface: full | readonly
140
+ /agents show workflow agents and per-agent model overrides
141
+ /setting open the runtime settings hub
142
+ /mcp manage MCP servers and tools
143
+ /skills choose a skill for the next request
144
+ /channels manage Discord/Telegram, schedules, webhooks, voice
83
145
  /compact compact older conversation context
84
146
  /clear reset the conversation and screen
85
147
  /OutputStyle show or switch Lead output style
@@ -156,3 +218,7 @@ vendor/
156
218
 
157
219
  `docs/` is not included in the published package unless `package.json#files` is
158
220
  changed.
221
+
222
+ ## License
223
+
224
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.19",
3
+ "version": "0.9.21",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -35,7 +35,7 @@
35
35
  "prepack": "npm run build:tui",
36
36
  "start": "node src/cli.mjs",
37
37
  "smoke": "node scripts/smoke.mjs",
38
- "smoke:all": "npm run smoke && npm run smoke:boot && npm run smoke:tools && npm run smoke:output && npm run smoke:tui && npm run smoke:freevars",
38
+ "smoke:all": "npm run smoke && npm run smoke:boot && npm run smoke:tools && npm run smoke:output && npm run smoke:tui && npm run smoke:freevars && npm run smoke:logguard",
39
39
  "smoke:boot": "node scripts/boot-smoke.mjs",
40
40
  "smoke:compact": "node scripts/compact-smoke.mjs",
41
41
  "smoke:loop": "node scripts/smoke-loop.mjs",
@@ -45,6 +45,7 @@
45
45
  "smoke:output": "node scripts/output-style-smoke.mjs",
46
46
  "smoke:tui": "node scripts/tui-render-smoke.mjs",
47
47
  "smoke:freevars": "node scripts/freevar-smoke.mjs",
48
+ "smoke:logguard": "node scripts/log-writer-guard-smoke.mjs",
48
49
  "test:toolcall": "node --test scripts/toolcall-args-test.mjs",
49
50
  "test:providers": "node --test scripts/provider-toolcall-test.mjs",
50
51
  "test:atomiclock": "node --test scripts/atomic-lock-tryonce-test.mjs",
@@ -44,6 +44,12 @@ await build({
44
44
  // Only `ink` is redirected to Mixdog's checked-in renderer instead of
45
45
  // node_modules/ink.
46
46
  packages: 'external',
47
+ // Bundled CJS helpers (e.g. src/lib/mixdog-debug.cjs) compile to esbuild's
48
+ // __require shim, which throws "Dynamic require of ..." in plain ESM.
49
+ // Provide a real module-scope require so those requires resolve at runtime.
50
+ banner: {
51
+ js: "import { createRequire as __mixdogCreateRequire } from 'node:module';\nconst require = __mixdogCreateRequire(import.meta.url);",
52
+ },
47
53
  external: [
48
54
  '../vendor/*',
49
55
  '../../vendor/*',
@@ -142,6 +142,14 @@ console.log(JSON.stringify({ hookSpecificOutput: {
142
142
  },
143
143
  });
144
144
 
145
+ // Untrusted project: executable project hooks must be neutered (security gate).
146
+ const busUntrusted = createStandaloneHookBus({ dataDir: join(root, 'data-untrusted') });
147
+ const blocked = await busUntrusted.beforeTool({ sessionId: 'sess_test', cwd: root, name: 'shell', args: { command: 'echo ok' } });
148
+ assert.equal(blocked, null);
149
+
150
+ // Trusted project: user-level trustedProjects list re-enables project command hooks.
151
+ mkdirSync(join(root, 'data-mixdog'), { recursive: true });
152
+ writeJson(join(root, 'data-mixdog', 'config.json'), { trustedProjects: [root] });
145
153
  const busWithMixdog = createStandaloneHookBus({ dataDir: join(root, 'data-mixdog') });
146
154
  const denied = await busWithMixdog.beforeTool({ sessionId: 'sess_test', cwd: root, name: 'shell', args: { command: 'echo ok' } });
147
155
  assert.equal(denied.action, 'deny');
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/log-writer-guard-smoke.mjs — unbounded `.log` writer regression gate.
4
+ *
5
+ * A new `.log` file name that lands in src/ without being covered by one of the
6
+ * bounded-writer contracts is an unbounded-growth footgun: the sibling-GC in
7
+ * mixdog-debug.cjs only prunes names it knows (canonical set + stale-sibling
8
+ * regexes), and the worker-boot rotation loop only rotates its explicit list.
9
+ * Any other steady `.log` append grows without limit.
10
+ *
11
+ * This scans src/ for string-literal `.log` file names (the targets that feed
12
+ * appendFile/appendFileSync/createWriteStream) and fails when a name is not:
13
+ * - in the canonical set (parsed live from mixdog-debug.cjs), or
14
+ * - in the worker-boot rotation list (parsed live from worker-bootstrap.mjs), or
15
+ * - matched by the dynamic / per-PID sibling patterns, or
16
+ * - in the explicit in-script allowlist below.
17
+ *
18
+ * Run: node scripts/log-writer-guard-smoke.mjs (or `npm run smoke:logguard`)
19
+ * Exit: 0 = every writer covered, 1 = uncovered `.log` name(s) found.
20
+ */
21
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
22
+ import { dirname, join, relative } from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
26
+ const SRC = join(ROOT, 'src');
27
+ // `dist` = built TUI bundle (generated from src/tui/*); vendor is third-party.
28
+ const SKIP_DIRS = new Set(['node_modules', 'dist', '.git', 'vendor']);
29
+ const SCAN_EXT = /\.(mjs|cjs|js|jsx)$/;
30
+
31
+ // `.log` file-name targets. Two shapes, both bounded to real string content:
32
+ // STATIC — a quoted/backtick literal (`'boot.log'`, `"logs\\a.log"`). The
33
+ // `+` stem keeps us off the bare `.endsWith('.log')` guards.
34
+ // TEMPLATE — a `${...}` interpolation immediately followed by a static `.log`
35
+ // tail (`${jobId}.stdout.log`, `${root}.log`). The dynamic prefix
36
+ // is unverifiable, so the trailing literal (down to bare `.log`)
37
+ // must be covered/allowlisted or it fails.
38
+ // Separators (`/` and `\`) are captured; basename() reduces to the file name.
39
+ const STATIC_RE = /['"`]([\w.\-/\\]+\.log)\b/g;
40
+ const TEMPLATE_RE = /\$\{[^}]*\}([\w.\-/\\]*\.log)\b/g;
41
+
42
+ /** Last path segment, splitting on both POSIX and Windows separators. */
43
+ function basename(p) {
44
+ const parts = p.split(/[\\/]/);
45
+ return parts[parts.length - 1];
46
+ }
47
+
48
+ /** Parse a `new Set([...])` / array of quoted `.log` names out of a source file. */
49
+ function parseLogNames(file, anchor) {
50
+ let src;
51
+ try { src = readFileSync(file, 'utf8'); } catch { return []; }
52
+ const at = src.indexOf(anchor);
53
+ if (at === -1) return [];
54
+ const tail = src.slice(at, at + 2000);
55
+ const end = tail.indexOf(']');
56
+ const block = end === -1 ? tail : tail.slice(0, end);
57
+ return [...block.matchAll(/['"]([\w.\-]+\.log)['"]/g)].map((m) => m[1]);
58
+ }
59
+
60
+ // ── Covered names ────────────────────────────────────────────────────
61
+ // Parsed live so a name added to either contract auto-covers here.
62
+ const canonical = new Set(
63
+ parseLogNames(join(SRC, 'lib/mixdog-debug.cjs'), 'CANONICAL_PLUGIN_LOG_NAMES'),
64
+ );
65
+ const rotation = new Set(
66
+ parseLogNames(join(SRC, 'runtime/channels/lib/worker-bootstrap.mjs'), '_rotLog of ['),
67
+ );
68
+
69
+ // Explicitly bounded / non-plugin-dir writers that live outside the sibling-GC
70
+ // contract by design (size-rotated in place, per-job spill, one-shot probes).
71
+ const ALLOWLIST = new Set([
72
+ 'session-start-critical.log', // size-bounded via rotateBoundedLog()
73
+ 'mixdog-ws-upgrade-probe.log', // one-shot WS upgrade probe dump
74
+ '.stdout.log', // shell-jobs per-jobId spill (`${jobId}.stdout.log`)
75
+ '.stderr.log', // shell-jobs per-jobId spill (`${jobId}.stderr.log`)
76
+ ]);
77
+
78
+ // Dynamic / per-PID sibling names (never literal, but keep the contract explicit).
79
+ const DYNAMIC_RE = [
80
+ /^(channels|memory)-worker\.\d+\.\d+\.log$/,
81
+ /-worker\.\d+\.\d+\.log$/,
82
+ /^mcp-debug\.\d+\.\d+\.log$/,
83
+ /^supervisor\.\d+\.log$/,
84
+ ];
85
+
86
+ function isCovered(name) {
87
+ return (
88
+ canonical.has(name) ||
89
+ rotation.has(name) ||
90
+ ALLOWLIST.has(name) ||
91
+ DYNAMIC_RE.some((re) => re.test(name))
92
+ );
93
+ }
94
+
95
+ function* walk(dir) {
96
+ let entries;
97
+ try { entries = readdirSync(dir); } catch { return; }
98
+ for (const name of entries) {
99
+ if (SKIP_DIRS.has(name)) continue;
100
+ const p = join(dir, name);
101
+ if (statSync(p).isDirectory()) yield* walk(p);
102
+ else if (SCAN_EXT.test(name)) yield p;
103
+ }
104
+ }
105
+
106
+ const offenders = [];
107
+ let scanned = 0;
108
+ for (const file of walk(SRC)) {
109
+ scanned++;
110
+ const lines = readFileSync(file, 'utf8').split('\n');
111
+ for (let i = 0; i < lines.length; i++) {
112
+ for (const re of [STATIC_RE, TEMPLATE_RE]) {
113
+ for (const m of lines[i].matchAll(re)) {
114
+ const name = basename(m[1]);
115
+ if (isCovered(name)) continue;
116
+ offenders.push(`${relative(ROOT, file)}:${i + 1} — uncovered \`.log\` writer target \`${name}\``);
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ if (offenders.length > 0) {
123
+ console.error('log-writer-guard: FAIL — uncovered unbounded `.log` writer(s):');
124
+ for (const line of offenders) console.error(` ${line}`);
125
+ console.error(
126
+ `log-writer-guard: ${offenders.length} offender(s). Add the name to CANONICAL_PLUGIN_LOG_NAMES ` +
127
+ '(mixdog-debug.cjs), the worker-boot rotation list, or the ALLOWLIST in this script.',
128
+ );
129
+ process.exit(1);
130
+ }
131
+ console.log(`log-writer-guard: ok — ${scanned} files scanned, every \`.log\` writer covered`);
@@ -102,23 +102,39 @@ try {
102
102
  thrownCode = err?.code || null;
103
103
  }
104
104
 
105
+ // Non-agent sessions hard-lock to recall-fasttrack (7/3 commit). This smoke has
106
+ // no memory subsystem registered, so ingest_session + search both fail — the
107
+ // exact "memory pipeline broken" condition the fail-safe must cover. Assert the
108
+ // fail-safe: recall-fasttrack aborts rather than dropping head behind a false
109
+ // "Full history is in memory" notice, so no context is silently lost and the
110
+ // overflow is surfaced instead of being masked by an empty summary shell.
105
111
  assert(threw, 'askSession should surface overflow error');
106
112
  assert(thrownCode === 'AGENT_CONTEXT_OVERFLOW', `expected AGENT_CONTEXT_OVERFLOW, got ${thrownCode}`);
107
- assert(mainSendCount === 2, `expected 2 main sends after reactive retry, got ${mainSendCount}`);
108
- assert(compactSendCount === 1, `expected 1 compact send, got ${compactSendCount}`);
113
+ // Recall-fasttrack aborted (memory failed), so the reactive retry never produced
114
+ // a smaller transcript to re-send: exactly one failing main send, no LLM compact.
115
+ assert(mainSendCount === 1, `expected 1 main send (reactive retry aborted by fail-safe), got ${mainSendCount}`);
116
+ assert(compactSendCount === 0, `expected 0 compact sends on recall-fasttrack path, got ${compactSendCount}`);
109
117
 
110
118
  const reloaded = loadSession(sessionId);
111
119
  assert(reloaded, 'session should reload from store after overflow');
112
- assert((reloaded.messages || []).length < beforeCount + 1, 'persisted transcript should reflect compaction shrink');
120
+ // Fail-safe keeps full history for the cycle: current turn appended, nothing dropped.
121
+ assert(
122
+ (reloaded.messages || []).length === beforeCount + 1,
123
+ `full history should be preserved on fail-safe abort (expected ${beforeCount + 1}, got ${(reloaded.messages || []).length})`,
124
+ );
113
125
  const summary = (reloaded.messages || []).find(
114
126
  (m) => m?.role === 'user' && typeof m.content === 'string' && m.content.startsWith(SUMMARY_PREFIX),
115
127
  );
116
- assert(summary, 'compacted transcript with SUMMARY_PREFIX should persist after overflow error');
128
+ assert(!summary, 'no empty summary shell should be injected when the memory pipeline fails');
129
+ assert(
130
+ !(reloaded.messages || []).some((m) => typeof m?.content === 'string' && m.content.includes('Full history is in memory')),
131
+ 'no false "Full history is in memory" recall notice should be injected on fail-safe abort',
132
+ );
117
133
  assert(
118
134
  (reloaded.messages || []).some((m) => m?.role === 'user' && String(m.content).includes('current overflow task must stay verbatim')),
119
135
  'current user turn should remain in persisted transcript',
120
136
  );
121
- assert(reloaded.compaction?.lastStage !== 'compacting', 'compaction lastStage should not remain stuck on compacting');
122
- assert(reloaded.providerState === undefined, 'providerState should clear after reactive compact persist');
137
+ assert(reloaded.compaction?.lastStage === 'overflow_failed', `compaction lastStage should be overflow_failed, got ${reloaded.compaction?.lastStage}`);
138
+ assert(reloaded.providerState === undefined, 'providerState should clear after reactive compact abort');
123
139
 
124
140
  process.stdout.write('reactive-compact-persist smoke passed ✓\n');
@@ -6,6 +6,5 @@
6
6
  { "id": "recent-dryrun-delta", "label": "recent-work topical: dryrun delta sum", "args": { "query": "드라이런 델타 합산", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["드라이런"], "topN": 5 } },
7
7
  { "id": "recent-remote-singleton", "label": "recent-work topical: remote singleton seat", "args": { "query": "remote 싱글톤 좌석", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["싱글톤"], "topN": 5 } },
8
8
  { "id": "recency-today", "label": "query+period recency: today's work (24h)", "args": { "query": "오늘 진행한 작업", "period": "24h", "limit": 10 }, "expect": { "kind": "results", "recencyOrdered": true } },
9
- { "id": "negative-projectaa", "label": "negative: every row must literally mention the term (no filler)", "args": { "query": "ProjectAA 밸런스", "limit": 10 }, "expect": { "kind": "allContain", "allContain": ["projectaa"] } },
10
9
  { "id": "xlang-embedding-crash", "label": "cross-language: embedding worker crash (strict topN)", "args": { "query": "embedding worker crash", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["embedding"], "topN": 3 } }
11
10
  ]
@@ -5,8 +5,7 @@
5
5
  { "id": "q-recall-general", "label": "bare recall keyword", "args": { "query": "recall" }, "expect": { "kind": "results", "topNContains": ["recall"], "topN": 5 } },
6
6
  { "id": "q-uc4-topic-kr", "label": "UC4 topic query with Korean qualifier", "args": { "query": "recall period last 개선", "limit": 8 }, "expect": { "kind": "results", "topNContains": ["개선"], "topN": 5 } },
7
7
  { "id": "q-recall-improve-kr", "label": "cache improvement topic", "args": { "query": "캐시 개선" }, "expect": { "kind": "results", "topNContains": ["개선"], "topN": 5 } },
8
- { "id": "q-battle-balance", "label": "ProjectAA topic", "args": { "query": "ProjectAA" }, "expect": { "kind": "results", "topNContains": ["ProjectAA"], "topN": 5 } },
9
- { "id": "q-fanout-recall-battle", "label": "fan-out array query cache+ProjectAA", "args": { "query": ["캐시 개선", "ProjectAA"], "limit": 5 }, "expect": { "kind": "results", "topNContains": ["개선", "ProjectAA"], "topN": 10 } },
8
+ { "id": "q-fanout-recall-cache", "label": "fan-out array query cache+recall", "args": { "query": ["캐시 개선", "recall"], "limit": 5 }, "expect": { "kind": "results", "topNContains": ["개선", "recall"], "topN": 10 } },
10
9
  { "id": "q-short-2tok-cycle", "label": "short 2-token cycle1 query", "args": { "query": "cycle1 drain", "limit": 8 }, "expect": { "kind": "results", "topNContains": ["cycle1"], "topN": 3 } },
11
10
  { "id": "q-recall-scope-project", "label": "project-scoped recall query", "args": { "query": "recall", "cwd": "C:\\Project\\mixdog" , "limit": 10 }, "expect": { "kind": "results", "topNContains": ["recall"], "topN": 5 } }
12
11
  ]