mixdog 0.9.90 → 0.9.92

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 (56) hide show
  1. package/package.json +5 -1
  2. package/scripts/tool-overhead-microbench.mjs +60 -0
  3. package/src/agents/debugger/agent.json +1 -1
  4. package/src/agents/explore/agent.json +1 -1
  5. package/src/agents/heavy-worker/agent.json +1 -1
  6. package/src/agents/maintainer/agent.json +1 -1
  7. package/src/agents/reviewer/agent.json +1 -1
  8. package/src/agents/worker/agent.json +1 -1
  9. package/src/lib/rules-builder.cjs +5 -5
  10. package/src/output-styles/detailed.md +27 -0
  11. package/src/output-styles/extreme-minimal.md +8 -8
  12. package/src/output-styles/minimal.md +7 -10
  13. package/src/output-styles/simple.md +12 -20
  14. package/src/rules/lead/01-general.md +9 -1
  15. package/src/rules/lead/lead-brief.md +15 -14
  16. package/src/rules/shared/01-tool.md +32 -37
  17. package/src/runtime/agent/orchestrator/agent-runtime/commit-message-completion.mjs +67 -0
  18. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +10 -0
  19. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -0
  20. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +10 -0
  21. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +35 -0
  22. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +47 -1
  23. package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +9 -0
  24. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +28 -1
  25. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +20 -2
  26. package/src/runtime/agent/orchestrator/session/result-classification.mjs +28 -0
  27. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +176 -3
  28. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -11
  29. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +6 -6
  30. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +15 -3
  31. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +9 -0
  32. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +16 -2
  33. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +176 -16
  34. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +5 -0
  35. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +30 -1
  36. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +1 -1
  37. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +96 -5
  38. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +72 -1
  39. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +48 -9
  41. package/src/runtime/channels/backends/discord.mjs +21 -1
  42. package/src/runtime/channels/tool-defs.mjs +1 -1
  43. package/src/runtime/memory/lib/trace-store.mjs +25 -3
  44. package/src/runtime/memory/tool-defs.mjs +1 -3
  45. package/src/runtime/search/tool-defs.mjs +2 -18
  46. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  47. package/src/session-runtime/output-styles.mjs +4 -6
  48. package/src/session-runtime/tool-defs.mjs +0 -1
  49. package/src/session-runtime/tool-surface.mjs +9 -0
  50. package/src/session-runtime/workflow.mjs +25 -8
  51. package/src/tui/dist/index.mjs +32 -1
  52. package/src/tui/engine/session-api.mjs +17 -0
  53. package/src/tui/engine/tui-steering-persist.mjs +24 -1
  54. package/src/workflows/default/WORKFLOW.md +7 -17
  55. package/src/workflows/solo/WORKFLOW.md +7 -5
  56. package/src/output-styles/default.md +0 -40
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.90",
3
+ "version": "0.9.92",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -100,6 +100,7 @@
100
100
  "test:route-scope": "node --test scripts/route-scope-isolation-test.mjs",
101
101
  "test:schedule-reload": "node --test scripts/schedule-reload-arm-test.mjs",
102
102
  "test:media": "node --test src/runtime/media/store.test.mjs src/runtime/media/renditions.test.mjs",
103
+ "test:shell-harness": "node --test scripts/shell-harness-regression-test.mjs",
103
104
  "failures": "node scripts/tool-failures.mjs",
104
105
  "trace:llm": "node scripts/llm-trace-summary.mjs",
105
106
  "diag:sessions": "node scripts/session-diag.mjs",
@@ -163,5 +164,8 @@
163
164
  "acorn": "^8.17.0",
164
165
  "esbuild": "^0.28.1",
165
166
  "eslint-scope": "^9.1.2"
167
+ },
168
+ "optionalDependencies": {
169
+ "@vscode/ripgrep": "^1.18.0"
166
170
  }
167
171
  }
@@ -0,0 +1,60 @@
1
+ // Micro-bench: fixed per-call overhead of the shell tool path vs raw spawn.
2
+ // Usage: node scripts/tool-overhead-microbench.mjs [bash|powershell] [N]
3
+ // Prints per-call ms for executeBashTool('echo hi') and raw child spawn,
4
+ // so (tool - raw) isolates our tool-layer overhead (policy, wrappers, I/O).
5
+ import { spawn } from 'node:child_process';
6
+ import { executeBashTool } from '../src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs';
7
+
8
+ const shell = process.argv[2] || (process.platform === 'win32' ? 'powershell' : 'bash');
9
+ const N = Number(process.argv[3] || 15);
10
+
11
+ const stats = (arr) => {
12
+ const s = [...arr].sort((a, b) => a - b);
13
+ const sum = s.reduce((t, v) => t + v, 0);
14
+ return {
15
+ mean: (sum / s.length).toFixed(1),
16
+ p50: s[Math.floor(s.length / 2)].toFixed(1),
17
+ min: s[0].toFixed(1),
18
+ max: s[s.length - 1].toFixed(1),
19
+ };
20
+ };
21
+
22
+ const rawOnce = () => new Promise((resolveDone, reject) => {
23
+ const child = shell === 'powershell'
24
+ ? spawn('pwsh', ['-NoProfile', '-NonInteractive', '-Command', 'echo hi'])
25
+ : spawn('bash', ['-c', 'echo hi']);
26
+ let out = '';
27
+ child.stdout.on('data', (c) => { out += c; });
28
+ child.on('error', reject);
29
+ child.on('close', () => resolveDone(out));
30
+ });
31
+
32
+ const toolOnce = async () => {
33
+ const out = await executeBashTool({ command: 'echo hi', shell }, process.cwd(), {});
34
+ return String(out);
35
+ };
36
+
37
+ // Warm-up both paths (module init, shell resolution cache, standbys).
38
+ await toolOnce(); await toolOnce();
39
+ await rawOnce(); await rawOnce();
40
+
41
+ const toolMs = [];
42
+ for (let i = 0; i < N; i++) {
43
+ const t0 = performance.now();
44
+ await toolOnce();
45
+ toolMs.push(performance.now() - t0);
46
+ }
47
+ const rawMs = [];
48
+ for (let i = 0; i < N; i++) {
49
+ const t0 = performance.now();
50
+ await rawOnce();
51
+ rawMs.push(performance.now() - t0);
52
+ }
53
+
54
+ const t = stats(toolMs);
55
+ const r = stats(rawMs);
56
+ console.log(`shell=${shell} n=${N}`);
57
+ console.log(`tool mean=${t.mean}ms p50=${t.p50}ms min=${t.min}ms max=${t.max}ms`);
58
+ console.log(`raw mean=${r.mean}ms p50=${r.p50}ms min=${r.min}ms max=${r.max}ms`);
59
+ console.log(`overhead(mean tool-raw)=${(Number(t.mean) - Number(r.mean)).toFixed(1)}ms`);
60
+ process.exit(0);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "debugger",
3
3
  "name": "Debugger",
4
- "description": "Failure reproduction and root-cause analysis.",
4
+ "description": "Use for deep root-cause analysis or a bug surviving 2+ fix cycles.",
5
5
  "entry": "AGENT.md"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "explore",
3
3
  "name": "Explore",
4
- "description": "Broad codebase exploration and target narrowing.",
4
+ "description": "Use when targets are unknown: broad codebase exploration and narrowing.",
5
5
  "entry": "AGENT.md"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "heavy-worker",
3
3
  "name": "Heavy Worker",
4
- "description": "Broad or multi-file implementation.",
4
+ "description": "Use for complex, high-difficulty tasks.",
5
5
  "entry": "AGENT.md"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "maintainer",
3
3
  "name": "Maintainer",
4
- "description": "Maintenance, upkeep, and long-running health work.",
4
+ "description": "Use for memory upkeep and long-running maintenance work.",
5
5
  "entry": "AGENT.md"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "reviewer",
3
3
  "name": "Reviewer",
4
- "description": "Change review and risk finding.",
4
+ "description": "Use when an implementation finishes: cross-verify the change for correctness and risk before reporting.",
5
5
  "entry": "AGENT.md"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "worker",
3
3
  "name": "Worker",
4
- "description": "Scoped implementation.",
4
+ "description": "Use for simple, well-scoped tasks.",
5
5
  "entry": "AGENT.md"
6
6
  }
@@ -156,8 +156,8 @@ function stripFrontmatter(markdown) {
156
156
  }
157
157
 
158
158
  function normalizeOutputStyleName(value) {
159
- const name = String(value || 'default').trim();
160
- return /^[A-Za-z0-9_.-]+$/.test(name) ? name : 'default';
159
+ const name = String(value || 'simple').trim();
160
+ return /^[A-Za-z0-9_.-]+$/.test(name) ? name : 'simple';
161
161
  }
162
162
 
163
163
  function loadOutputStyle({ PLUGIN_ROOT, DATA_DIR }) {
@@ -172,10 +172,10 @@ function loadOutputStyle({ PLUGIN_ROOT, DATA_DIR }) {
172
172
  const body = stripFrontmatter(readOptional(candidate));
173
173
  if (body) return body;
174
174
  }
175
- if (styleName !== 'default') {
175
+ if (styleName !== 'simple') {
176
176
  const fallback = [
177
- path.join(DATA_DIR, 'output-styles', 'default.md'),
178
- path.join(PLUGIN_ROOT, 'output-styles', 'default.md'),
177
+ path.join(DATA_DIR, 'output-styles', 'simple.md'),
178
+ path.join(PLUGIN_ROOT, 'output-styles', 'simple.md'),
179
179
  ];
180
180
  for (const candidate of fallback) {
181
181
  const body = stripFrontmatter(readOptional(candidate));
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: detailed
3
+ title: Detailed
4
+ description: Detailed engineering summaries
5
+ aliases: verbose, full
6
+ keep-coding-instructions: true
7
+ ---
8
+
9
+ # Output Style
10
+
11
+ Detailed — the fullest style, yet still summary-form, never essay-form.
12
+ Depth comes from picking the right facts, not explaining more.
13
+
14
+ - Lead with the outcome in one short sentence, then only the detail that
15
+ matters: what changed and the key facts (paths, commands, errors).
16
+ Conclusions, not reasoning; cite a symbol/path only as an anchor. Complete
17
+ sentences in the user's language; commands, code, and errors verbatim.
18
+ - Say each point once. Size budget: roughly TWICE Simple — ~2 rendered lines
19
+ per point, whole report ~10–15 lines.
20
+ - Short labels such as `Changes` or `Risks / next steps` in final reports
21
+ only; none on interim progress; collapse trivial tasks to a couple of
22
+ sentences. Never dump raw tool output.
23
+ - Do not hide blockers or failures; one short clause each.
24
+ - One bullet = one idea, at most 2 rendered lines, opened with a short
25
+ **bold key point**; blank line between multi-line items; nest one
26
+ sub-level at most.
27
+ - Never name this style unless asked.
@@ -11,12 +11,12 @@ keep-coding-instructions: true
11
11
  Extreme minimal — the most compressed style: exactly one sentence, under 100
12
12
  characters.
13
13
 
14
- - Reply with a SINGLE sentence, always under 100 characters. Never a second
15
- sentence, clause pile-up, or run-on that smuggles in extra facts.
16
- - State only the net result. Drop file lists, how-it-was-done, verification
17
- detail, and follow-ups unless one is the single most decisive fact.
18
- - No headings, bullets, numbered lists, labels, or sections — one plain sentence
19
- only, even when the request says "report" or "summary".
14
+ - A SINGLE sentence, always under 100 characters never a second sentence or
15
+ a run-on that smuggles in extra facts.
16
+ - Net result only: drop file lists, methods, and follow-ups unless one is
17
+ the single decisive fact.
18
+ - No headings, bullets, labels, or sections — one plain sentence, even when
19
+ the request says "report".
20
20
  - Preferred pattern: `<target> changed.`
21
- - Preserve only the single decisive path, command, symbol, or error verbatim,
22
- and only if it fits the limit.
21
+ - Preserve one decisive path, command, symbol, or error verbatim, only if it
22
+ fits the limit.
@@ -9,15 +9,12 @@ keep-coding-instructions: true
9
9
 
10
10
  Minimal — a very short summary: one or two sentences, nothing more.
11
11
 
12
- - Summarize only the net result in one short sentence; add a second short
13
- sentence only for a fact (verification, blocker) that genuinely needs it —
14
- never a run-on that crams extra facts in.
15
- - Size budget: roughly HALF the Simple style — 1–2 plain, complete sentences
16
- (~2–3 rendered lines) however large the task was, concept-level only.
17
- - Summarize, never itemize: no headings, bullets, labels, or sections and no
18
- file-by-file detail state only what the change accomplishes, even when
19
- the request says "report" or "summary".
20
- - Preferred pattern: `<target> changed. <verification> passed.` If
21
- verification was not run, say so.
12
+ - One short sentence with the net result; a second only for a fact that
13
+ genuinely needs it — never a run-on.
14
+ - Roughly HALF Simple: 1–2 plain sentences (~2–3 rendered lines) however
15
+ large the task, concept-level only.
16
+ - Never itemize: no headings, bullets, labels, sections, or file-by-file
17
+ detail even when the request says "report".
18
+ - Preferred pattern: `<target> changed.`
22
19
  - Preserve only the single decisive path, command, symbol, API name, code, or
23
20
  error verbatim.
@@ -8,26 +8,18 @@ keep-coding-instructions: true
8
8
 
9
9
  # Output Style
10
10
 
11
- Practical concise — outcome-first handoffs for coding work: summarize the
12
- result, do not narrate or explain the change.
11
+ Practical concise — outcome-first handoffs: summarize the result, do not
12
+ narrate the work.
13
13
 
14
14
  - Open with the outcome in one sentence: done, blocked, or awaiting a decision.
15
- - Summarize at the concept level what the change accomplishes, not a
16
- per-file changelog or code path; cite a path (`file_path:line_number`) only
17
- as a navigation anchor, never as the explanation.
18
- - Compress by cutting content (filler, acknowledgments, hedging, restated
19
- facts), not grammar: natural, complete sentences in the user's language;
20
- paths, commands, symbols, code, and exact errors stay verbatim.
21
- - Controlled detail: 1–3 short bullets or 2–3 sentences; state each point
22
- once. Size budget: roughly HALF the Default style and TWICE Minimal
23
- whole reply ~5–7 lines.
24
- - Layout: one idea per bullet, ONE line each, led with a short bold key
25
- phrase; blank line between multi-line list items — never a dense wall of
26
- text.
27
- - Final handoffs may use labels like `Changes`, `Verification`, and
28
- `Risks / next steps`; do not label interim progress.
29
- - Synthesize agent or retrieval results; never forward raw reports, long file
30
- lists, tool traces, or session metadata.
31
- - Do not hide blockers, failed verification, or required follow-up — state
32
- them in one short clause; if verification was not run, say so once.
15
+ - Concept-level summary of what changed, not a per-file changelog; cite a
16
+ path (`file:line`) only as an anchor. Complete sentences in the user's
17
+ language; paths, commands, symbols, code, and errors verbatim.
18
+ - 1–3 short bullets or 2–3 sentences, each point once; whole reply ~5–7
19
+ lines (HALF Detailed, TWICE Minimal).
20
+ - One idea per bullet, ONE line each, led by a short bold key phrase; blank
21
+ line between multi-line items.
22
+ - Final handoffs may use short labels like `Changes` or `Risks / next
23
+ steps`; none on interim progress. Never dump raw tool output.
24
+ - Do not hide blockers or failures; one short clause each.
33
25
  - Never name this style unless asked.
@@ -4,7 +4,15 @@
4
4
  multi-provider agent workflows. Never identify as generic OpenAI/ChatGPT.
5
5
  - A preamble is at most one useful sentence, with no direct names, honorifics,
6
6
  headings, labels, or routine lookup narration.
7
- - Destructive/hard-to-reverse action needs explicit confirmation.
7
+ - Destructive/hard-to-reverse action needs explicit confirmation and explicit
8
+ validated target paths — never `~`, a root, or unresolved variables/globs;
9
+ report material deletions with recoverability.
8
10
  - Act proactively; ask only for decisions.
11
+ - Mid-task input: a replacement supersedes current work, an addition folds
12
+ into it, a status question gets a brief answer while work continues; after
13
+ context compaction continue from the summary — never restart or redo
14
+ finished work.
15
+ - When blocked, exhaust safe in-scope checks once and report the blocker;
16
+ never spend turns without a tool call or new evidence.
9
17
  - Your final message ends the turn: answer only when the work is done. After a
10
18
  failed tool call, fix and re-run it, or state plainly that it is unresolved.
@@ -1,18 +1,19 @@
1
1
  # Lead Brief
2
2
 
3
- - Use one-line fragments. `Task:` is mandatory and lossless: preserve intent,
4
- required and forbidden outcomes, completion/stop boundary, user-supplied
5
- exact targets, and exact replacements/outputs. Never infer exactness from
6
- task name, file count, or difficulty.
7
- - Each role constructs its own `Task:` from the original request and official
8
- spec/test acceptance criteria, preserving every requirement and boundary.
3
+ - Minimum chars, maximum info: one-line fragments. `Task:` is mandatory and
4
+ lossless: each role
5
+ constructs it from the original request and official spec/test acceptance
6
+ criteria, preserving intent, required and forbidden outcomes,
7
+ completion/stop boundary, user-supplied exact targets, and exact
8
+ replacements/outputs. Never infer exactness from task name, file count, or
9
+ difficulty.
9
10
  - Omit role-known rules, repeated context/facts, and padding; split scope
10
11
  without discarding requirements.
11
- - Other fields are task-specific deltas: `Anchors:`, `Allow/Forbid:`,
12
- `Deliver:`. Omit empty fields. Anchors are `file:line` plus a one-line
13
- conclusion, never log/code bodies. State outcomes, not methods, unless the
14
- method is required. `Deliver:` sets handoff shape/size.
15
- - Send a full brief only for a fresh spawn or `respawned: true`; live follow-ups
16
- contain only the delta. A dead-tag send is cold and must re-supply anchors.
17
- - Never `send` mid-run; batch one follow-up after completion; interrupt only to
18
- cancel. Agent communication is English.
12
+ - Other fields are task-specific deltas `Anchors:` (`file:line` plus a
13
+ one-line conclusion, never log/code bodies), `Allow/Forbid:`, `Deliver:`
14
+ (sets handoff shape/size); omit empty fields. State outcomes, not methods,
15
+ unless the method is required.
16
+ - Full brief only for a fresh spawn or `respawned: true`; live follow-ups
17
+ carry only the delta; a dead-tag send is cold and must re-supply anchors.
18
+ - Never `send` mid-run; batch one follow-up after completion; interrupt only
19
+ to cancel. Agent communication is English.
@@ -1,40 +1,35 @@
1
1
  # Tool Use
2
2
 
3
- - Before the first call, gather every known facet in one tool message; for
4
- each facet choose exactly one shortest locator route:
5
- broad/uncertain→`explore` (roles without it: `find`); partial path/name
6
- `find`; verified root+wildcard→
7
- `glob`; quoted/non-identifier literal or regex→`grep`; exact code
8
- identifier/relation→`code_graph` before grep; known file/span→`read`
9
- directly without `grep`; verified directory→`list`; known edit→
10
- `apply_patch` directly, with no preparatory `read`;
11
- program/state change→`shell`; web/current external info→`search`.
12
- - Shortest total calls, maximum batching every turn. Combine variants,
13
- symbols, scopes, paths, and queries into one call; put all independent
14
- calls (probes, reads, hypotheses, commands) in one message concurrent
15
- regardless of tool, shell included. Sequential singles only for a
16
- genuinely dependent next step. Distinct facets, not alternative routes.
17
- Only apply_patch executes in order.
18
- - Batch compatible reads same-file regions as real `{path,offset,limit}`
19
- arrays covering the whole logical unit — in one `path[]` call, and graph
20
- targets in arrays. Don't reread returned spans. Put all new edits in one
21
- patch.
3
+ - Before the first call, gather every known facet in one tool message; one
4
+ shortest route per facet: broad/uncertain→`explore` (roles without it:
5
+ `find`); partial path/name→`find`; verified root+wildcard→`glob`;
6
+ quoted/non-identifier literal or regex→`grep`; exact code identifier/
7
+ relation→`code_graph` before grep; known file/span→`read` directly without
8
+ `grep`; verified directory→`list`; known edit→`apply_patch` (span already
9
+ seen; else `read`/`grep` first); program/state change→`shell`; web/current
10
+ external info→`search`.
11
+ - Shortest total calls, maximum batching — every turn: all independent calls
12
+ in one concurrent message (shell included); combine variants/symbols/
13
+ scopes/paths/queries per call; same-file regions as one real
14
+ `{path,offset,limit}` array; graph targets as arrays; `explore` facets in
15
+ one `query[]` (max 8, no rephrased duplicates); all new edits in one patch.
16
+ Distinct facets, not alternative routes; sequential singles only for a
17
+ step whose arguments require the previous result — fixed follow-ups
18
+ (pinned installs, known writes) go in the same batch; only apply_patch
19
+ executes in order.
22
20
  - Verified paths: project root, session cwd, user-provided, tool-returned.
23
- `find` first for guessed path/name fragments (same turn as other probes);
24
- on ENOENT, find the basename.
25
- - At task start, batch all `explore` facets in one `query[]` call, maximum 8,
26
- without rephrased duplicates. Retry `EXPLORATION_FAILED` once with changed
27
- tokens.
28
- - Stop when evidence covers the deliverable; don't re-locate or re-verify a
29
- sufficient anchor. A returned `path:line` freezes the location; inspecting
30
- its content with read/code_graph is valid.
31
- - A nonzero `content_with_context` result resolves that conceptact on it;
32
- only zero/error results justify changed tokens or scope.
33
- - `apply_patch` is the primary edit tool: send the patch as soon as the target
34
- path and new content are known. `read` is for discovery or for recovery
35
- after a patch failed on insufficient context.
36
- - A shell placed after `apply_patch` in the same turn runs after the patch
37
- lands batch edits and their verification freely.
38
- - A command promoted to background is a decision point: continue only if
39
- observed progress fits the budget, otherwise switch routes. Waiting is an
40
- explicit choice.
21
+ `find` first for guessed path/name fragments; on ENOENT, find the basename.
22
+ Retry `EXPLORATION_FAILED` once with changed tokens.
23
+ - Stop when evidence covers the deliverable: a returned `path:line` or
24
+ nonzero `content_with_context` result is final act on it (inspecting it
25
+ via read/code_graph is valid); only zero/error results justify changed
26
+ tokens or scope. Don't re-locate, re-verify, or reread returned spans.
27
+ - `apply_patch` is the primary edit tool: send the patch as soon as target
28
+ path and new content are known. Hunk context comes verbatim from the newest
29
+ tool output of that span (`read`/`grep`/your own patchpost-patch content
30
+ after edits), never retyped from memory; one look-up beats a failed patch.
31
+ A same-turn shell after `apply_patch` runs once the patch lands.
32
+ - After starting or receiving a background task, end the turn its
33
+ completion notification resumes the work. Never poll, sleep-loop, or block;
34
+ explicit wait only for a result the current turn cannot proceed without.
35
+ Long commands whose output the next step does not need go async.
@@ -0,0 +1,67 @@
1
+ import { loadConfig } from '../config.mjs';
2
+ import { getProvider, initProviders } from '../providers/registry.mjs';
3
+ import { resolveMaintenanceRoute } from './maintenance-route.mjs';
4
+
5
+ export const COMMIT_MESSAGE_SYSTEM_PROMPT = 'You are generating one git commit message from the provided diff. First line: imperative mood, at most 72 characters, no trailing period. Optionally add a blank line and a short body (wrapped at 72 characters) explaining WHY. Output ONLY the commit message - no preamble, no code fences, no quotes.';
6
+
7
+ export function commitMessageSystemPrompt(style = '') {
8
+ const hint = String(style || '').trim();
9
+ return hint ? `${COMMIT_MESSAGE_SYSTEM_PROMPT}\n${hint}` : COMMIT_MESSAGE_SYSTEM_PROMPT;
10
+ }
11
+
12
+ function resultText(result) {
13
+ if (typeof result === 'string') return result;
14
+ if (typeof result?.content === 'string') return result.content;
15
+ if (Array.isArray(result?.content)) {
16
+ return result.content
17
+ .map((part) => part?.type === 'text' ? String(part.text || '') : '')
18
+ .filter(Boolean)
19
+ .join('\n');
20
+ }
21
+ return '';
22
+ }
23
+
24
+ export function createCommitMessageCompletion(deps = {}) {
25
+ const load = deps.loadConfig || loadConfig;
26
+ const resolveRoute = deps.resolveMaintenanceRoute || resolveMaintenanceRoute;
27
+ const initialize = deps.initProviders || initProviders;
28
+ const providerFor = deps.getProvider || getProvider;
29
+
30
+ return async function generateCommitMessage(source, options = {}) {
31
+ const text = String(source || '').trim();
32
+ if (!text) return '';
33
+ const signal = options.signal || null;
34
+ const config = load();
35
+ // Commit messages are maintenance-class work: they ride the same
36
+ // route as session titles instead of the main conversation model.
37
+ const route = resolveRoute({
38
+ agent: 'title-agent',
39
+ config,
40
+ });
41
+ if (!route || typeof route !== 'object') {
42
+ throw new Error('Commit message maintenance route is unresolved.');
43
+ }
44
+ const providerName = String(route.provider || '').trim();
45
+ const model = String(route.model || '').trim();
46
+ if (!providerName || !model) {
47
+ throw new Error('Commit message maintenance route requires provider and model.');
48
+ }
49
+ await initialize(config.providers || {}, { signal });
50
+ const provider = providerFor(providerName);
51
+ if (!provider || typeof provider.send !== 'function') {
52
+ throw new Error(`Commit message provider is unavailable: ${providerName}`);
53
+ }
54
+ const response = await provider.send([
55
+ { role: 'system', content: commitMessageSystemPrompt(options.style) },
56
+ { role: 'user', content: text },
57
+ ], model, undefined, {
58
+ signal,
59
+ effort: String(route.effort || '').trim() || 'low',
60
+ fast: route.fast === true,
61
+ maxOutputTokens: 400,
62
+ });
63
+ return resultText(response).trim();
64
+ };
65
+ }
66
+
67
+ export const generateCommitMessage = createCommitMessageCompletion();
@@ -50,8 +50,10 @@ import {
50
50
  anthropicRequestTimeoutMs,
51
51
  classifyError,
52
52
  anthropicMaxAttempts,
53
+ createStallRetryBudget,
53
54
  midstreamBackoffFor,
54
55
  retryAfterMsFromError,
56
+ STREAM_STALL_RETRY_BUDGET_MS,
55
57
  withRetry,
56
58
  } from './retry-classifier.mjs';
57
59
  import {
@@ -774,6 +776,9 @@ export class AnthropicOAuthProvider {
774
776
  const MAX_MIDSTREAM_RETRIES = ANTHROPIC_MAX_MIDSTREAM_RETRIES;
775
777
  let firstAttemptError = null;
776
778
  let firstAttemptClassifier = null;
779
+ // Send-scoped stall window: in-place stall retries share one wall
780
+ // clock starting at the first stall (see createStallRetryBudget).
781
+ const stallRetryBudget = createStallRetryBudget();
777
782
 
778
783
  const recoverNonStreaming = async (midState, streamingError, controller) => {
779
784
  const exposedChars = Number(midState?.emittedTextChars) || 0;
@@ -1052,6 +1057,11 @@ export class AnthropicOAuthProvider {
1052
1057
  continue;
1053
1058
  }
1054
1059
  const classifier = _classifyMidstreamError(err, midState);
1060
+ if (classifier === 'stream_stalled' && !stallRetryBudget.allowStallRetry()) {
1061
+ try { process.stderr.write(`[anthropic-oauth] stall retry budget exhausted (${STREAM_STALL_RETRY_BUDGET_MS}ms since first stall) — surfacing for fresh-request retry\n`); } catch {}
1062
+ try { controller?.abort?.(err); } catch { /* best-effort teardown */ }
1063
+ throw err;
1064
+ }
1055
1065
  if (classifier && attemptIndex < MAX_MIDSTREAM_RETRIES) {
1056
1066
  firstAttemptError = err;
1057
1067
  firstAttemptClassifier = classifier;
@@ -8,8 +8,10 @@ import {
8
8
  anthropicMaxAttempts,
9
9
  anthropicRequestTimeoutMs,
10
10
  classifyError,
11
+ createStallRetryBudget,
11
12
  midstreamBackoffFor,
12
13
  sleepWithAbort,
14
+ STREAM_STALL_RETRY_BUDGET_MS,
13
15
  withRetry,
14
16
  retryAfterMsFromError,
15
17
  } from './retry-classifier.mjs';
@@ -249,6 +251,9 @@ export class AnthropicProvider {
249
251
  const MAX_MIDSTREAM_RETRIES = ANTHROPIC_MAX_MIDSTREAM_RETRIES;
250
252
  let firstAttemptError = null;
251
253
  let firstAttemptClassifier = null;
254
+ // Send-scoped stall window: in-place stall retries share one wall
255
+ // clock starting at the first stall (see createStallRetryBudget).
256
+ const stallRetryBudget = createStallRetryBudget();
252
257
 
253
258
  const buildReturnFromParse = (parseResult) => {
254
259
  const usageRaw = parseResult.usage?.raw || null;
@@ -592,6 +597,15 @@ export class AnthropicProvider {
592
597
  continue;
593
598
  }
594
599
  const classifier = _classifyMidstreamError(err, midState);
600
+ if (classifier === 'stream_stalled' && !stallRetryBudget.allowStallRetry()) {
601
+ try {
602
+ process.stderr.write(
603
+ `[${this.name}] stall retry budget exhausted (${STREAM_STALL_RETRY_BUDGET_MS}ms since first stall) — surfacing for fresh-request retry\n`,
604
+ );
605
+ } catch {}
606
+ try { streamController.abort?.(err); } catch {}
607
+ throw err;
608
+ }
595
609
  if (classifier && attemptIndex < MAX_MIDSTREAM_RETRIES) {
596
610
  firstAttemptError = err;
597
611
  firstAttemptClassifier = classifier;
@@ -36,9 +36,11 @@ import {
36
36
  classifyHandshakeError,
37
37
  classifyMidstreamError,
38
38
  createStreamSafetyStamps,
39
+ createStallRetryBudget,
39
40
  jitterDelayMs,
40
41
  MIDSTREAM_RETRY_POLICY,
41
42
  sleepWithAbort,
43
+ STREAM_STALL_RETRY_BUDGET_MS,
42
44
  } from './retry-classifier.mjs';
43
45
  import { stampStreamOutcome, STREAM_TRANSPORTS } from './lib/stream-outcome.mjs';
44
46
  import {
@@ -459,6 +461,9 @@ export async function sendViaWebSocket({
459
461
  const MAX_MIDSTREAM_RETRIES = MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT;
460
462
  let firstAttemptError = null;
461
463
  let firstAttemptClassifier = null;
464
+ // Send-scoped stall window: in-place stall retries share one wall clock
465
+ // starting at the first stall (see createStallRetryBudget).
466
+ const stallRetryBudget = createStallRetryBudget();
462
467
  // A generate:false prewarm is billable even if its main request later
463
468
  // retries on a fresh socket or falls back to HTTP. Retain one completed
464
469
  // result across the whole logical send and attach it to terminal errors.
@@ -1006,6 +1011,11 @@ export async function sendViaWebSocket({
1006
1011
  const classifier = err?.unsafeToRetry === true
1007
1012
  ? null
1008
1013
  : _classifyMidstreamError(err, midState);
1014
+ if (classifier === 'stream_stalled' && !stallRetryBudget.allowStallRetry()) {
1015
+ try { process.stderr.write(`[openai-oauth] stall retry budget exhausted (${STREAM_STALL_RETRY_BUDGET_MS}ms since first stall) — surfacing for fresh-request retry\n`); } catch {}
1016
+ emitSendSpan('error');
1017
+ throw _stampTool(_stampLiveText(err));
1018
+ }
1009
1019
  const retryLimit = classifier ? _midstreamRetryLimit(classifier) : 0;
1010
1020
  if (classifier && attemptIndex < retryLimit) {
1011
1021
  // Retry-eligible: stash the first-attempt error, emit progress,
@@ -314,6 +314,41 @@ export function jitterDelayMs(ms, ratio = PROVIDER_RETRY_JITTER_RATIO, mode = 's
314
314
  return Math.max(0, Math.round(base + offset))
315
315
  }
316
316
 
317
+ // ── Stall-retry wall-clock budget (send-scoped) ──────────────────────────────
318
+ // Mid-stream 'stream_stalled' recoveries retry in place, which is right for a
319
+ // one-off blip but lets a chronically dying stream burn a whole task budget
320
+ // slowly (observed live: one send stretched 149s→298s→556s across stall
321
+ // retries before the agent deadline killed the task). Reference stacks bound
322
+ // this instead of retrying forever: Claude Code caps each request at ~300s
323
+ // wall clock (API_TIMEOUT_MS) and Codex kills a stream after one 300s silent
324
+ // gap (stream_idle_timeout). This guard is the equivalent for our in-place
325
+ // recovery: the clock starts at the FIRST stall of a send, and stall-classified
326
+ // retries are allowed only inside that window; past it the stall error
327
+ // surfaces so loop-level transport retry issues a FRESH request. Healthy
328
+ // streams never consult the clock (no stall → no budget reads), so long
329
+ // thinking/output can never trip it.
330
+ export const STREAM_STALL_RETRY_BUDGET_MS = (() => {
331
+ const v = Number(process.env.MIXDOG_STREAM_STALL_BUDGET_MS)
332
+ return Number.isFinite(v) && v > 0 ? Math.floor(v) : 300_000
333
+ })()
334
+
335
+ // One instance per provider send() call (NOT per attempt — the whole point is
336
+ // bounding the cross-attempt stall window). `now` is injectable for tests.
337
+ export function createStallRetryBudget(budgetMs = STREAM_STALL_RETRY_BUDGET_MS, now = Date.now) {
338
+ let firstStallAt = 0
339
+ return {
340
+ // Record a stall-classified retry candidate. Returns true while the
341
+ // send's stall window still has budget; false once exhausted (the caller
342
+ // surfaces the error instead of retrying in place).
343
+ allowStallRetry() {
344
+ const t = now()
345
+ if (!firstStallAt) firstStallAt = t
346
+ return (t - firstStallAt) <= budgetMs
347
+ },
348
+ get firstStallAt() { return firstStallAt },
349
+ }
350
+ }
351
+
317
352
  // ── Shared network-resilience interface ──────────────────────────────────────
318
353
  // One home for the logic shared across providers: mid-stream classifier
319
354
  // (WS + SSE), transport fallback predicate, stream-safety stamp latches,