mixdog 0.9.89 → 0.9.91

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 (40) hide show
  1. package/package.json +5 -1
  2. package/src/output-styles/detailed.md +27 -0
  3. package/src/output-styles/extreme-minimal.md +8 -8
  4. package/src/output-styles/minimal.md +7 -10
  5. package/src/output-styles/simple.md +13 -21
  6. package/src/rules/lead/01-general.md +9 -1
  7. package/src/rules/lead/lead-brief.md +15 -14
  8. package/src/rules/shared/01-tool.md +31 -37
  9. package/src/runtime/agent/orchestrator/agent-runtime/commit-message-completion.mjs +67 -0
  10. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +35 -1
  11. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +28 -1
  12. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +20 -2
  13. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +116 -3
  14. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -11
  15. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -10
  16. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +15 -3
  17. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +9 -0
  18. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +16 -2
  19. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +176 -16
  20. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  21. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +30 -1
  22. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +1 -1
  23. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +96 -5
  24. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +72 -1
  25. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  26. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +48 -9
  27. package/src/runtime/channels/backends/discord.mjs +21 -1
  28. package/src/runtime/channels/tool-defs.mjs +1 -1
  29. package/src/runtime/memory/lib/trace-store.mjs +25 -3
  30. package/src/runtime/memory/tool-defs.mjs +5 -5
  31. package/src/runtime/search/tool-defs.mjs +2 -18
  32. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  33. package/src/session-runtime/output-styles.mjs +7 -6
  34. package/src/session-runtime/tool-defs.mjs +0 -1
  35. package/src/session-runtime/tool-surface.mjs +9 -0
  36. package/src/session-runtime/workflow.mjs +7 -0
  37. package/src/standalone/explore-tool.mjs +2 -2
  38. package/src/workflows/default/WORKFLOW.md +2 -1
  39. package/src/workflows/solo/WORKFLOW.md +7 -5
  40. 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.89",
3
+ "version": "0.9.91",
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,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.
@@ -2,32 +2,24 @@
2
2
  name: simple
3
3
  title: Simple
4
4
  description: Outcome-first concise handoffs for coding work
5
- aliases: concise, handoff
5
+ aliases: concise, handoff, default
6
6
  keep-coding-instructions: true
7
7
  ---
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,34 @@
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
+ genuinely dependent step (unconditional follow-ups are not dependent —
18
+ batch them); only apply_patch executes in order.
22
19
  - 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.
20
+ `find` first for guessed path/name fragments; on ENOENT, find the basename.
21
+ Retry `EXPLORATION_FAILED` once with changed tokens.
22
+ - Stop when evidence covers the deliverable: a returned `path:line` or
23
+ nonzero `content_with_context` result is final act on it (inspecting it
24
+ via read/code_graph is valid); only zero/error results justify changed
25
+ tokens or scope. Don't re-locate, re-verify, or reread returned spans.
26
+ - `apply_patch` is the primary edit tool: send the patch as soon as target
27
+ path and new content are known. Hunk context comes verbatim from the newest
28
+ tool output of that span (`read`/`grep`/your own patchpost-patch content
29
+ after edits), never retyped from memory; one look-up beats a failed patch.
30
+ A same-turn shell after `apply_patch` runs once the patch lands.
31
+ - After starting or receiving a background task, end the turn its
32
+ completion notification resumes the work. Never poll, sleep-loop, or block;
33
+ explicit wait only for a result the current turn cannot proceed without.
34
+ 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();
@@ -128,6 +128,12 @@ export {
128
128
  // this catches tight deterministic-failure loops (e.g. a command that errors
129
129
  // the same way every time) far earlier than 100 iterations.
130
130
  const REPEAT_FAIL_LIMIT = 3;
131
+ // Structured provider continuations (endTurn=false / pause_turn) are honored,
132
+ // but must not sustain an unbounded text-only loop: a lead session was
133
+ // observed burning a 30-minute agent budget (26K output tokens, zero tool
134
+ // calls) on back-to-back continuations. After this many continuations with no
135
+ // intervening tool batch, the current text is accepted as the final answer.
136
+ const PROVIDER_CONTINUATION_NO_TOOL_LIMIT = Math.max(1, Number(process.env.MIXDOG_PROVIDER_CONTINUATION_NO_TOOL_LIMIT) || 8);
131
137
  // A provider max-output stop is not a completed assistant turn, even when it
132
138
  // contains useful text. Preserve each partial in the provider transcript and
133
139
  // grant at most three direct continuations before surfacing a hard truncation.
@@ -384,6 +390,12 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
384
390
  // hard iteration cap remains the sole bound on how long a provider may
385
391
  // keep declaring "not done" inside one user turn.
386
392
  let _providerContinuationCount = 0;
393
+ // Continuations since the last executed tool batch — bounds the text-only
394
+ // continuation runaway (see PROVIDER_CONTINUATION_NO_TOOL_LIMIT).
395
+ let _continuationsSinceToolBatch = 0;
396
+ // Loop-level transport replays consumed this ask (see send-with-recovery
397
+ // TRANSPORT_RETRY_MAX): bounded per turn, reset only with a fresh ask.
398
+ let _transportRetriesUsed = 0;
387
399
  // Claude Code parity: queued prompt/task notifications are attached after a
388
400
  // tool batch, before the continuation provider send. Normal batches drain
389
401
  // up to 'next'; a Sleep-like tool grants a 'later' flush.
@@ -627,6 +639,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
627
639
  () => sendWithRecovery({
628
640
  provider, messages, model, sendTools, tools: sendTools, opts,
629
641
  sessionId, sessionRef, nextIteration, contextOverflowRetryUsed,
642
+ transportRetriesUsed: _transportRetriesUsed, signal,
630
643
  }),
631
644
  );
632
645
  if (_sendResult.action === 'retry') {
@@ -635,6 +648,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
635
648
  reactiveOverflowRetryPending = true;
636
649
  continue;
637
650
  }
651
+ if (_sendResult.action === 'retry_transport') {
652
+ _transportRetriesUsed += 1;
653
+ delete opts.cacheBreakIntent;
654
+ continue;
655
+ }
638
656
  response = _sendResult.response;
639
657
  opts.onToolCall = undefined;
640
658
  delete opts.cacheBreakIntent;
@@ -821,11 +839,26 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
821
839
  const continuationSignal = !isOutputLimitStop && stopReason !== 'refusal'
822
840
  ? providerContinuationSignal(response)
823
841
  : null;
824
- if (continuationSignal && pushIntermediateAssistantResponse(response)) {
842
+ if (continuationSignal && _continuationsSinceToolBatch >= PROVIDER_CONTINUATION_NO_TOOL_LIMIT) {
843
+ // Text-only continuation runaway: stop honoring the signal and
844
+ // fall through to the terminal handling below, which accepts
845
+ // the current content as the final answer (or ends the loop).
846
+ process.stderr.write(`[loop] provider continuation cap ${PROVIDER_CONTINUATION_NO_TOOL_LIMIT} reached without tool calls (sess=${sessionId || 'unknown'}); accepting current text as final.\n`);
847
+ try {
848
+ appendAgentTrace({
849
+ sessionId,
850
+ iteration: iterations,
851
+ kind: 'steer',
852
+ payload: { tag: 'provider_continuation_no_tool_cap', count: _continuationsSinceToolBatch },
853
+ agent: sessionAgent || null,
854
+ });
855
+ } catch { /* best-effort */ }
856
+ } else if (continuationSignal && pushIntermediateAssistantResponse(response)) {
825
857
  if (hasContent && !suppressMidTurnText) {
826
858
  try { opts.onAssistantText?.(response.content); } catch { /* best-effort */ }
827
859
  }
828
860
  _providerContinuationCount += 1;
861
+ _continuationsSinceToolBatch += 1;
829
862
  _emptyNudgeStreak = 0;
830
863
  try {
831
864
  appendAgentTrace({
@@ -1018,6 +1051,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
1018
1051
  // Settle the stop hook on the batch that actually executed.
1019
1052
  _toolFailureStopHook.endBatch(_callsToExecute);
1020
1053
  _toolBatchJustCompleted = true;
1054
+ _continuationsSinceToolBatch = 0;
1021
1055
  _lastToolBatchHadSleep = _callsToExecute.some(isSleepLikeToolCall);
1022
1056
  }
1023
1057
  // Classify WHY the loop ended so agent-tool can promote an empty/abnormal
@@ -28,6 +28,30 @@ const STORED_TOOL_ARG_LIMIT = 10_000;
28
28
  const STORED_TOOL_ARG_PREVIEW_HEAD = 360;
29
29
  const STORED_TOOL_ARG_PREVIEW_TAIL = 160;
30
30
 
31
+ // File paths a compacted patch touched, so the marker itself tells the model
32
+ // WHICH files to re-read instead of replaying the marker as patch input
33
+ // (measured: the compacted-placeholder resubmission was 39% of apply_patch
34
+ // failures). Marker contract: the returned text may not contain ']' or a
35
+ // newline, so bracket characters are stripped from paths.
36
+ function _compactedPatchTargets(value) {
37
+ const seen = new Set();
38
+ const add = (raw) => {
39
+ const p = String(raw || '').trim().replace(/[\[\]\r\n]/g, '');
40
+ if (p && p !== '/dev/null' && !seen.has(p)) seen.add(p);
41
+ };
42
+ const v4a = /^\*\*\*\s*(?:Update|Add|Delete) File:\s*(.+)$/gim;
43
+ for (let m; seen.size < 12 && (m = v4a.exec(value));) add(m[1]);
44
+ if (!seen.size) {
45
+ const uni = /^(?:\+\+\+|---)\s+(?:[ab]\/)?(\S+)/gm;
46
+ for (let m; seen.size < 12 && (m = uni.exec(value));) add(m[1]);
47
+ }
48
+ const all = [...seen];
49
+ const shown = all.slice(0, 4).map((p) => (p.length > 70 ? `…${p.slice(-70)}` : p));
50
+ if (!shown.length) return '';
51
+ const more = all.length > shown.length ? ` +${all.length - shown.length} more` : '';
52
+ return `${shown.join(', ')}${more}`;
53
+ }
54
+
31
55
  function compactStoredToolArgString(value, key = '', opts = {}) {
32
56
  if (typeof value !== 'string') return value;
33
57
  const isBody = STORED_TOOL_ARG_BODY_KEY_RE.test(key);
@@ -39,8 +63,11 @@ function compactStoredToolArgString(value, key = '', opts = {}) {
39
63
  // Body markers carry the recovery instruction inline: the compaction
40
64
  // detectors only require the `[mixdog compacted ...]` shape (no ']' or
41
65
  // newline inside), so the longer text stays fully compatible.
66
+ const targets = /^patch$/i.test(key) ? _compactedPatchTargets(value) : '';
42
67
  const marker = isBody
43
- ? `[mixdog compacted ${key}: ${value.length} chars, sha256:${hash}; already applied - do not copy; re-read the file and write a fresh patch]`
68
+ ? (targets
69
+ ? `[mixdog compacted ${key}: ${value.length} chars, sha256:${hash}; already applied to ${targets} - do not copy; re-read those files and write a fresh patch]`
70
+ : `[mixdog compacted ${key}: ${value.length} chars, sha256:${hash}; already applied - do not copy; re-read the file and write a fresh patch]`)
44
71
  : `[mixdog compacted ${key || 'string'}: ${value.length} chars, sha256:${hash}; do not copy]`;
45
72
  // Body args (patch / old_string / new_string / content / rewrite) are
46
73
  // apply_patch / edit inputs. Keeping a head/tail preview leaves real patch
@@ -318,7 +318,11 @@ export function createSession(opts) {
318
318
  const hasCallerAllow = Array.isArray(opts.schemaAllowedTools);
319
319
  const tools = finalizeSessionToolList(toolsForRouting, {
320
320
  schemaAllowedTools: hasCallerAllow ? opts.schemaAllowedTools : null,
321
- disallowedTools: hiddenAgent ? [...(Array.isArray(opts.disallowedTools) ? opts.disallowedTools : []), 'Skill'] : opts.disallowedTools,
321
+ disallowedTools: [
322
+ ...(Array.isArray(opts.disallowedTools) ? opts.disallowedTools : []),
323
+ ...(hiddenAgent ? ['Skill'] : []),
324
+ ...(!ownerIsAgent && workflowDisallowsAgentTool(opts.workflow) ? ['agent'] : []),
325
+ ],
322
326
  ownerIsAgent,
323
327
  resolvedAgent,
324
328
  });
@@ -499,6 +503,17 @@ const ACTIVE_OWNER_HB_FRESH_MS = 2 * 60 * 1000; // heartbeat freshness window
499
503
  const PREPARED_RESUME_LIMIT = 8;
500
504
  const _preparedResumes = new Map();
501
505
 
506
+ // A workflow that delegates to NOBODY (agents: declared empty — e.g. Solo)
507
+ // must not put the `agent` tool in the session tool list: policy rejects
508
+ // every call, so a schema-visible tool is a guaranteed error turn plus dead
509
+ // schema weight. Field source: workflowSummary() carries agentsConfigured /
510
+ // agents; older persisted sessions lack them and keep the tool (safe).
511
+ function workflowDisallowsAgentTool(workflow) {
512
+ return Boolean(workflow && typeof workflow === 'object'
513
+ && workflow.agentsConfigured === true
514
+ && Array.isArray(workflow.agents) && workflow.agents.length === 0);
515
+ }
516
+
502
517
  function _prepareResumeTools(session, preset) {
503
518
  const ownerIsAgent = isAgentOwner(session);
504
519
  const skills = ownerIsAgent ? [] : collectPromptSkillsCached(session.cwd);
@@ -521,7 +536,10 @@ function _prepareResumeTools(session, preset) {
521
536
  ownerIsAgent,
522
537
  tools: finalizeSessionToolList(toolsForRouting, {
523
538
  schemaAllowedTools: Array.isArray(session.schemaAllowedTools) ? session.schemaAllowedTools : null,
524
- disallowedTools: getHiddenAgent(session.agent || null) ? ['Skill'] : null,
539
+ disallowedTools: [
540
+ ...(getHiddenAgent(session.agent || null) ? ['Skill'] : []),
541
+ ...(!isAgentOwner(session) && workflowDisallowsAgentTool(session.workflow) ? ['agent'] : []),
542
+ ],
525
543
  ownerIsAgent,
526
544
  resolvedAgent: session.agent || null,
527
545
  }),