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.
- package/package.json +5 -1
- package/scripts/tool-overhead-microbench.mjs +60 -0
- package/src/agents/debugger/agent.json +1 -1
- package/src/agents/explore/agent.json +1 -1
- package/src/agents/heavy-worker/agent.json +1 -1
- package/src/agents/maintainer/agent.json +1 -1
- package/src/agents/reviewer/agent.json +1 -1
- package/src/agents/worker/agent.json +1 -1
- package/src/lib/rules-builder.cjs +5 -5
- package/src/output-styles/detailed.md +27 -0
- package/src/output-styles/extreme-minimal.md +8 -8
- package/src/output-styles/minimal.md +7 -10
- package/src/output-styles/simple.md +12 -20
- package/src/rules/lead/01-general.md +9 -1
- package/src/rules/lead/lead-brief.md +15 -14
- package/src/rules/shared/01-tool.md +32 -37
- package/src/runtime/agent/orchestrator/agent-runtime/commit-message-completion.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +10 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +10 -0
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +47 -1
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +28 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +176 -3
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -11
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +15 -3
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +16 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +176 -16
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +30 -1
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +96 -5
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +72 -1
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +48 -9
- package/src/runtime/channels/backends/discord.mjs +21 -1
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/lib/trace-store.mjs +25 -3
- package/src/runtime/memory/tool-defs.mjs +1 -3
- package/src/runtime/search/tool-defs.mjs +2 -18
- package/src/runtime/shared/tool-execution-contract.mjs +1 -1
- package/src/session-runtime/output-styles.mjs +4 -6
- package/src/session-runtime/tool-defs.mjs +0 -1
- package/src/session-runtime/tool-surface.mjs +9 -0
- package/src/session-runtime/workflow.mjs +25 -8
- package/src/tui/dist/index.mjs +32 -1
- package/src/tui/engine/session-api.mjs +17 -0
- package/src/tui/engine/tui-steering-persist.mjs +24 -1
- package/src/workflows/default/WORKFLOW.md +7 -17
- package/src/workflows/solo/WORKFLOW.md +7 -5
- 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.
|
|
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);
|
|
@@ -156,8 +156,8 @@ function stripFrontmatter(markdown) {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
function normalizeOutputStyleName(value) {
|
|
159
|
-
const name = String(value || '
|
|
160
|
-
return /^[A-Za-z0-9_.-]+$/.test(name) ? name : '
|
|
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 !== '
|
|
175
|
+
if (styleName !== 'simple') {
|
|
176
176
|
const fallback = [
|
|
177
|
-
path.join(DATA_DIR, 'output-styles', '
|
|
178
|
-
path.join(PLUGIN_ROOT, 'output-styles', '
|
|
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
|
-
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
- No headings, bullets,
|
|
19
|
-
|
|
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
|
|
22
|
-
|
|
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
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
12
|
-
|
|
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
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
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
|
-
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
method is required.
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
- Never `send` mid-run; batch one follow-up after completion; interrupt only
|
|
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;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
`
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
- `apply_patch`
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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 patch — post-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,
|