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.
- package/LICENSE +21 -0
- package/README.md +100 -34
- package/package.json +3 -2
- package/scripts/build-tui.mjs +6 -0
- package/scripts/hook-bus-test.mjs +8 -0
- package/scripts/log-writer-guard-smoke.mjs +131 -0
- package/scripts/reactive-compact-persist-smoke.mjs +22 -6
- package/scripts/recall-bench-cases.json +0 -1
- package/scripts/recall-quality-cases.json +1 -2
- package/scripts/session-ingest-compaction-smoke.mjs +241 -0
- package/scripts/tool-smoke.mjs +150 -45
- package/src/defaults/skills/setup/SKILL.md +327 -0
- package/src/help.mjs +2 -5
- package/src/lib/mixdog-debug.cjs +13 -0
- package/src/mixdog-session-runtime.mjs +7 -3328
- package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -2
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -10
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +40 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +73 -3
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +663 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
- package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
- package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
- package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
- package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
- package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
- package/src/runtime/channels/index.mjs +6 -2183
- package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
- package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
- package/src/runtime/channels/lib/network-retry.mjs +23 -0
- package/src/runtime/channels/lib/owned-runtime.mjs +627 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +20 -9
- package/src/runtime/channels/lib/transcript-binding.mjs +336 -0
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
- package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
- package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
- package/src/runtime/channels/lib/worker-main.mjs +777 -0
- package/src/runtime/memory/index.mjs +163 -1725
- package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
- package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
- package/src/runtime/memory/lib/http-router.mjs +811 -0
- package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +6 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +46 -9
- package/src/runtime/memory/lib/memory-cycle2.mjs +87 -11
- package/src/runtime/memory/lib/memory-embed.mjs +28 -7
- package/src/runtime/memory/lib/memory-recall-store.mjs +4 -4
- package/src/runtime/memory/lib/memory.mjs +39 -0
- package/src/runtime/memory/lib/query-handlers.mjs +2 -2
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
- package/src/runtime/shared/atomic-file.mjs +138 -80
- package/src/runtime/shared/child-guardian.mjs +61 -3
- package/src/session-runtime/boot-profile.mjs +36 -0
- package/src/session-runtime/channel-config-api.mjs +70 -0
- package/src/session-runtime/context-status.mjs +181 -0
- package/src/session-runtime/env.mjs +17 -0
- package/src/session-runtime/lifecycle-api.mjs +242 -0
- package/src/session-runtime/model-route-api.mjs +198 -0
- package/src/session-runtime/provider-auth-api.mjs +135 -0
- package/src/session-runtime/resource-api.mjs +282 -0
- package/src/session-runtime/runtime-core.mjs +2104 -0
- package/src/session-runtime/session-turn-api.mjs +274 -0
- package/src/session-runtime/tool-catalog.mjs +18 -264
- package/src/session-runtime/tool-defs.mjs +2 -2
- package/src/session-runtime/workflow-agents-api.mjs +238 -0
- package/src/standalone/agent-tool.mjs +2 -2
- package/src/standalone/channel-worker.mjs +67 -7
- package/src/standalone/folder-dialog.mjs +4 -1
- package/src/standalone/memory-runtime-proxy.mjs +154 -17
- package/src/standalone/seeds.mjs +28 -1
- package/src/tui/App.jsx +40 -28
- package/src/tui/app/core-memory-picker.mjs +1 -1
- package/src/tui/app/doctor.mjs +175 -0
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +9 -0
- package/src/tui/app/use-mouse-input.mjs +6 -0
- package/src/tui/app/use-transcript-scroll.mjs +77 -3
- package/src/tui/dist/index.mjs +2851 -2162
- package/src/tui/engine/context-state.mjs +145 -0
- package/src/tui/engine/prompt-history.mjs +27 -0
- package/src/tui/engine/session-api-ext.mjs +478 -0
- package/src/tui/engine/session-api.mjs +564 -0
- package/src/tui/engine/session-flow.mjs +485 -0
- package/src/tui/engine/turn.mjs +1078 -0
- package/src/tui/engine.mjs +68 -2620
- package/src/tui/index.jsx +7 -0
- package/vendor/ink/build/ink.js +16 -1
- package/vendor/ink/build/output.js +30 -4
- package/vendor/ink/build/render.js +5 -0
- 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
|
-
|
|
4
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/mixdog)
|
|
4
|
+

|
|
5
|
+

|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+

|
|
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
|
|
138
|
+
/model choose the main provider/model (/effort, /fast tune it)
|
|
79
139
|
/workflow choose the active workflow
|
|
80
|
-
/agents show
|
|
81
|
-
/
|
|
82
|
-
/
|
|
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.
|
|
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",
|
package/scripts/build-tui.mjs
CHANGED
|
@@ -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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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, '
|
|
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
|
|
122
|
-
assert(reloaded.providerState === undefined, 'providerState should clear after reactive compact
|
|
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-
|
|
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
|
]
|