mixdog 0.9.108 → 0.9.110

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 (36) hide show
  1. package/package.json +1 -1
  2. package/scripts/run-suite.mjs +2 -0
  3. package/src/output-styles/detailed.md +14 -11
  4. package/src/output-styles/extreme-minimal.md +2 -4
  5. package/src/output-styles/minimal.md +6 -7
  6. package/src/output-styles/simple.md +11 -12
  7. package/src/rules/agent/30-explorer.md +19 -28
  8. package/src/rules/agent/41-cycle2-agent.md +1 -2
  9. package/src/rules/agent/42-cycle3-agent.md +3 -4
  10. package/src/rules/shared/01-tool.md +40 -33
  11. package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +7 -1
  12. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +67 -20
  13. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +7 -1
  14. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +31 -18
  15. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +5 -3
  16. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +81 -0
  17. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +64 -0
  18. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +44 -9
  19. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +9 -0
  20. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +6 -7
  21. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +24 -0
  22. package/src/runtime/agent/orchestrator/session/result-classification.mjs +20 -25
  23. package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -2
  24. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +31 -26
  25. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +10 -1
  26. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +37 -14
  27. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +38 -62
  28. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -4
  29. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  30. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +6 -37
  31. package/src/runtime/shared/pristine-execution-contract.json +4 -1
  32. package/src/session-runtime/session-title.mjs +14 -1
  33. package/src/standalone/agent-tool/spawn-flow.mjs +18 -0
  34. package/src/tui/session/tool-result-status.mjs +5 -12
  35. package/src/tui/session/tool-result-text.mjs +7 -5
  36. package/src/workflows/default/WORKFLOW.md +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.108",
3
+ "version": "0.9.110",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -51,6 +51,7 @@ export const SUITES = {
51
51
  'pretool-ask-runtime-test.mjs',
52
52
  'prompt-input-parity-test.mjs',
53
53
  'reactive-compact-persist-smoke.mjs',
54
+ 'reasoning-replay-policy-test.mjs',
54
55
  'repl-stream-finalize-test.mjs',
55
56
  'result-classification-test.mjs',
56
57
  'rg-runner-test.mjs',
@@ -60,6 +61,7 @@ export const SUITES = {
60
61
  'session-title-controller-test.mjs',
61
62
  'set-effort-config-test.mjs',
62
63
  'shell-jobs-windows-hide-test.mjs',
64
+ 'spawn-ws-prewarm-test.mjs',
63
65
  'spinner-meta-test.mjs',
64
66
  'statusline-agents-test.mjs',
65
67
  'statusline-quota-hysteresis-test.mjs',
@@ -1,21 +1,24 @@
1
1
  ---
2
2
  name: detailed
3
3
  title: Detailed
4
- description: Detailed engineering summaries
4
+ description: Claude Code default-depth responses
5
5
  aliases: verbose, full
6
6
  keep-coding-instructions: true
7
7
  ---
8
8
 
9
9
  # Output Style
10
10
 
11
- Detailed — dense facts, complete handoff.
11
+ Detailed — a teammate's update; clarity outranks terseness.
12
12
 
13
- - Outcome first; changes/anchors/commands/errors/conclusions only, no reasoning.
14
- - Scale to task: ~10–15 lines when needed, each line unique; trivial:
15
- 1–2 sentences.
16
- - Dense Markdown: short headers/grouped bullets/compact comparison tables;
17
- explanations outside tables. Fence essential multiline code only; nest once;
18
- final-only labels.
19
- - Never dump raw tool output; blockers and failures: one clause each.
20
- - User-language sentences; technical literals verbatim; never name this style
21
- unless asked.
13
+ - Outcome first, then what a cold reader needs: complete user-language
14
+ sentences, expanded jargon, rationale where it adds value; no filler or
15
+ process narration.
16
+ - Structure matches complexity: plain prose for simple answers; headers,
17
+ bullets, or tables (short enumerable facts only, explained outside) when
18
+ they aid scanning.
19
+ - No hard cap, but brevity first: the shortest report understood without
20
+ rereads (~10 lines is plenty for most tasks); expand only when complexity
21
+ demands; trivial results stay 1–2 sentences.
22
+ - Cite `file:line`; snippets only when load-bearing.
23
+ - Never dump raw tool output; blockers and failures in one clause each.
24
+ - Never name this style unless asked.
@@ -10,7 +10,5 @@ keep-coding-instructions: true
10
10
 
11
11
  Extreme minimal — exactly one sentence under 100 characters.
12
12
 
13
- - State only the net result; no second sentence, run-on, file list, method,
14
- follow-up, heading, bullet, or label, even for reports.
15
- - Prefer `<target> changed.` Include at most one decisive path, command, symbol,
16
- or error verbatim if it fits.
13
+ - Net result only; no second sentence, list, heading, label, or follow-up.
14
+ - At most one decisive path, command, symbol, or error verbatim if it fits.
@@ -1,16 +1,15 @@
1
1
  ---
2
2
  name: minimal
3
3
  title: Minimal
4
- description: One- or two-sentence summary
4
+ description: Net result within 400 characters
5
5
  keep-coding-instructions: true
6
6
  ---
7
7
 
8
8
  # Output Style
9
9
 
10
- Minimal — one or two sentences with only the net result.
10
+ Minimal — net result in plain sentences; hard cap 400 characters.
11
11
 
12
- - Add the second only for one indispensable fact; no run-on.
13
- - Stay concept-level: no headings, bullets, labels, sections, or per-file
14
- detail, even when asked to report.
15
- - Prefer `<target> changed.` Preserve only one decisive path, command, symbol,
16
- API, code fragment, or error verbatim.
12
+ - Concept level only: no headings, bullets, labels, or per-file detail, even
13
+ for reports.
14
+ - Only decisive paths, commands, symbols, or errors verbatim; user-language
15
+ sentences; never name this style unless asked.
@@ -1,22 +1,21 @@
1
1
  ---
2
2
  name: simple
3
3
  title: Simple
4
- description: Outcome-first concise handoffs for coding work
4
+ description: Lightly trimmed default-style updates within 800 characters
5
5
  aliases: concise, handoff
6
6
  keep-coding-instructions: true
7
7
  ---
8
8
 
9
9
  # Output Style
10
10
 
11
- Practical concise outcome first; no process narration.
11
+ Simplea teammate's update, tightened; hard cap 800 characters.
12
12
 
13
- - Open with done, blocked, or awaiting a decision.
14
- - Report concepts, not files; `file:line` only anchors.
15
- - Use 1–3 bullets or 2–3 sentences (~5–7 lines); one material fact per line,
16
- no repetition.
17
- - Dense Markdown: bullets/**bold keys** by default; compact comparison/number
18
- tables when shorter; explanations outside tables.
19
- - Final-only labels; never dump raw tool output. State blockers and failures
20
- in one clause each.
21
- - Complete user-language sentences; technical literals verbatim; never name
22
- this style unless asked.
13
+ - Outcome first, then only what the reader needs: complete user-language
14
+ sentences, no filler or process narration.
15
+ - Structure matches complexity: plain prose by default; bullets or compact
16
+ tables only when they aid scanning.
17
+ - Prefer the tighter phrasing: each material fact once, secondary detail and
18
+ non-load-bearing rationale dropped; `file:line` anchors, technical literals
19
+ verbatim; the cap is a ceiling, not a target.
20
+ - Never dump raw tool output; blockers and failures in one clause each.
21
+ Never name this style unless asked.
@@ -13,40 +13,31 @@ add no rules or exceptions here.
13
13
 
14
14
  ## Hard budget
15
15
 
16
- Before EVERY tool call, check:
17
- 1. Which requested targets still lack a complete direct anchor set?
18
- 2. Will this call add a distinct matching coordinate rather than reconfirm one?
19
-
20
- A target is complete only when every distinct coordinate directly satisfying
21
- its query is held; one anchor suffices only when the target is singular by
22
- construction. If all targets are complete, or the call only reconfirms,
23
- re-reads, verifies, quotes, strengthens, or adds context, answer now.
24
-
25
16
  Target: ONE tool turn and an answer within 10 seconds.
26
17
  Hard limit: FIVE tool turns plus ONE tool-less final-report turn. Label tool
27
- messages `turn 1/6` through `turn 5/6`. If turn 5 is used, the next response is
28
- `turn 6/6` and is the FINAL TURN.
18
+ messages `turn 1/6` through `turn 5/6`; the response after turn 5 is
19
+ `turn 6/6`, the FINAL REPORT TURN: no tools, report the credible anchors
20
+ currently held, or `EXPLORATION_FAILED` if none exist.
29
21
 
30
- After turns 1-4, report immediately if every requested target is complete.
31
- Do not spend another turn merely because budget remains.
22
+ A target is complete only when every distinct coordinate directly satisfying
23
+ its query is held; one anchor suffices only when the target is singular by
24
+ construction. Before EVERY tool call, check which targets still lack a
25
+ complete direct anchor set and whether the call adds a distinct matching
26
+ coordinate; once every target is complete, answer immediately — never spend
27
+ a turn merely because budget remains.
32
28
 
33
- Turns 2-5 are ONLY for incomplete targets. Each recovery turn uses changed
29
+ Turns 2-5 are ONLY for incomplete targets: each recovery turn uses changed
34
30
  concrete tokens or a new exact scope in maximum fanout. Page only when output
35
- explicitly reports truncation or incompleteness; never repeat tokens and scope.
36
-
37
- If the next turn lacks a concrete anchor-producing move, stop early with
38
- `EXPLORATION_FAILED`.
39
-
40
- After turn 5, stop tools unconditionally. Turn 6 (`turn 6/6`) is the FINAL
41
- REPORT TURN and the last turn: report the credible anchors currently held; if
42
- none exist, return `EXPLORATION_FAILED`. There is no sixth tool turn.
31
+ explicitly reports truncation or incompleteness; never repeat tokens and
32
+ scope. If the next turn lacks a concrete anchor-producing move, stop early
33
+ with `EXPLORATION_FAILED`.
43
34
 
44
35
  ## No reconfirmation
45
36
 
46
37
  A credible tool-returned coordinate is FINAL. Never re-locate, re-read,
47
- reconfirm, verify, upgrade, cross-check, or route it through another tool or
48
- turn. Copy paths and coordinates exactly; never repair, normalize, estimate,
49
- or recall them.
38
+ reconfirm, verify, upgrade, cross-check, quote, or strengthen it through
39
+ another tool or turn. Copy paths and coordinates exactly; never repair,
40
+ normalize, estimate, or recall them.
50
41
 
51
42
  A code anchor requires a tool-returned `path:line`; a bare path is valid only
52
43
  for a file/dir-location query. Generic matches and guessed coordinates are
@@ -57,8 +48,8 @@ Return one compact line per distinct direct match:
57
48
 
58
49
  Use no fixed item-count cap; omit incidental matches and prose. For a
59
50
  completeness/list/count query, copy EVERY returned matching `path:line` once
60
- and preserve the tool-reported total. Never omit a direct match or page after
51
+ and preserve the tool-reported total; never omit a direct match or page after
61
52
  a complete result.
62
53
 
63
- Return `EXPLORATION_FAILED` when the budget cannot produce a credible anchor.
64
- Never fabricate, soften, or return vague prose.
54
+ Return `EXPLORATION_FAILED` when the budget cannot produce a credible anchor;
55
+ never fabricate, soften, or return vague prose.
@@ -41,5 +41,4 @@ Use only input IDs; never invent IDs. `update` supplies fresh `element` and a
41
41
  uses only one `project_id`. Summaries are complete sentences in input language,
42
42
  preserve important specifics verbatim, and omit actor/meta filler. Category
43
43
  priority: `rule > constraint > decision > fact > goal > preference > task >
44
- issue`. Replace literal `|` with `/`; fields contain no newlines. Start every
45
- verdict with a digit.
44
+ issue`. Replace literal `|` with `/`; fields contain no newlines.
@@ -28,7 +28,6 @@ Verbose durable is always `update`, never `keep`.
28
28
  `<id>|merge|<target_id>|<source_ids_csv>`
29
29
  `<id>|delete`
30
30
 
31
- IDs match input rows; never invent them. An `update` summary is one short
32
- clause and its `element` is compact. A `merge` retains `target_id`, absorbs
33
- sources, and stays within one `project_id`. Replace literal `|` with `/`;
34
- fields contain no newlines. Emit a digit-starting verdict for every input row.
31
+ IDs match input rows; never invent them. An `update` supplies a compact
32
+ `element`. A `merge` retains `target_id`, absorbs sources, and stays within
33
+ one `project_id`. Replace literal `|` with `/`; fields contain no newlines.
@@ -2,45 +2,52 @@
2
2
 
3
3
  - Baseline routing assigns each facet directly by the evidence needed to
4
4
  determine the complete edit:
5
- path/name only→`find`; wildcard paths→`glob`; exact directory entries→`list`;
5
+ path/name only→`find`; wildcard/recursive paths→`glob` (including known-root
6
+ unknown descendants); exact directory entries→`list`;
6
7
  source content/value/`path:line`→`grep`; exact symbol/relation→`code_graph`;
7
8
  known file/range→`read`;
8
- web/current→`search`; returned URL body→`web_fetch`; prior work→`recall`;
9
+ web/current→`search`; returned URL body→`web_fetch`; prior work→`recall`
10
+ (history only, never current local state);
9
11
  durable compact English memory→`memory`; explicit project change→`cwd`;
10
- explicit user-requested conversation reset→`session_manage`, each when exposed.
12
+ explicit user-requested conversation reset→`session_manage`.
11
13
  Use only named tools present in the current tool surface.
12
14
  `explore`, when exposed, is a fast path only for facets whose repository
13
15
  coordinates remain unknown: call it first once for all such independent
14
- facets in one query array. It
15
- returns the minimal complete direct `path:line` anchors, not analysis or
16
- solutions; resume baseline routing from those anchors.
17
- - Use verified paths (cwd/project/user/tool). Within the current project, pass
16
+ facets in one query array. It returns direct `path:line` anchors, not
17
+ analysis or solutions; anchors are tool-verified coordinates that END
18
+ their location facets never re-locate or second-guess them; resume
19
+ baseline routing from them.
20
+ - Act only on verified identities (cwd/project/user/tool-returned) — paths,
21
+ module specifiers, symbols, data/record shapes alike; a guessed identity is
22
+ verified by one lookup or sample only when the next call or edit references
23
+ it. Within the current project, pass
18
24
  project-relative paths and omit optional scopes equal to its root; explicit
19
- paths may be outside cwd only for targets outside the project;
20
- stay focused on the requested outcome. Avoid investigation, implementation,
21
- or verification not required to satisfy it; once the requirements are met
22
- and proven, stop.
23
- Batch calls iff no call needs another's output or can change another's
24
- inputs/state; otherwise serialize. Before each retrieval batch, deduplicate
25
- all required facets, route each once to the cheapest sufficient tool with all
26
- required variants/scopes, and launch every independent call together. Never
27
- split one decision across overlapping facets, add `shell`, `apply_patch`, or
28
- other mutation merely to widen retrieval, duplicate/broaden a facet through
29
- another tool or `shell`, reserve known work, or cap fanout.
30
- Take the cheapest sufficient evidence per facet:
31
- symbol relations end at `code_graph`, values/locations end at the context
32
- grep returns; `read` covers only what returned spans cannot, as an anchored
33
- offset/limit window never a full-file read when a window suffices;
34
- adjacent context around an edit point counts as needed evidence. The moment
35
- evidence determines the edit, stop retrieving and patch.
36
- - Once the edit is determined, finish in one assistant turn: one
37
- `apply_patch` per file or cohesive unit, all patches first, then one batched
38
- verification `shell` when needed; the runtime waits for every patch and skips
39
- the shell if any fails. Retry only failed envelopes. Create or edit text only
40
- with `apply_patch`, never `shell`.
41
- After failure rerun only the failed check. Earlier `shell` is only for
42
- executable/runtime/state evidence no file tool returns — an independent
43
- facet, batched with the rest. Follow up only when prior output is required
44
- to form the next call.
25
+ paths may be outside cwd only for targets outside the project.
26
+ - Plan the fewest dependent rounds, then the fewest calls. A conclusive
27
+ result ends its facet, and known state task/brief-supplied facts,
28
+ returned content, your own successful calls' effects — is never
29
+ re-acquired, broadened, or reconfirmed. Batch calls iff none needs
30
+ another's output or can change another's inputs/state; otherwise
31
+ serialize. Before each batch, deduplicate the facets still required by the request,
32
+ route each once to the cheapest sufficient tool with all required
33
+ variants/scopes, and launch every independent call together never
34
+ split or duplicate a facet across tools, mutate merely to widen
35
+ retrieval, reserve known work, or cap fanout. Symbol relations end at
36
+ `code_graph`; values/locations end at the context grep returns; `read`
37
+ covers only what returned spans cannot, as an anchored offset/limit
38
+ window. The moment evidence determines the answer, edit, or deliverable,
39
+ stop retrieving; patch if needed.
40
+ - Once the edit or deliverable is determined, finish in one assistant turn:
41
+ one `apply_patch` per file or cohesive unit, all patches first, then one
42
+ batched verification `shell` that runs the real required postconditions
43
+ on every changed file and produced artifact, never echoes a claim;
44
+ runtime waits for every patch and skips the shell
45
+ if any fails. Retry only failed envelopes; rerun a failed check only
46
+ after a fix that can change its result, else report it unresolved.
47
+ Hand-authored text is edited only with `apply_patch`; computed artifacts
48
+ (data/reports/derived values) come from `shell` computation, never
49
+ hand-transcribed numbers. Earlier `shell` is only for runtime/state
50
+ evidence unavailable to file tools—an independent facet, batched with
51
+ the rest.
45
52
  - A background `task_id` ends the turn; completion resumes work. Never poll;
46
53
  use task control only for recovery or a required blocking result.
@@ -39,7 +39,13 @@ export function createTitleCompletion(deps = {}) {
39
39
  config,
40
40
  });
41
41
  if (!route || typeof route !== 'object') {
42
- throw new Error('Session title maintenance route is unresolved.');
42
+ const error = new Error('Session title maintenance route is unresolved.');
43
+ // Machine-readable marker: callers (session-title controller)
44
+ // downgrade this to a one-shot "titling disabled" skip instead of
45
+ // logging a stack per session (e.g. bench profiles without a
46
+ // maintainer/default route).
47
+ error.code = 'MAINTENANCE_ROUTE_UNRESOLVED';
48
+ throw error;
43
49
  }
44
50
  const providerName = String(route.provider || '').trim();
45
51
  const model = String(route.model || '').trim();
@@ -41,6 +41,8 @@ import {
41
41
  } from './anthropic-oauth-credentials.mjs';
42
42
  import {
43
43
  PROVIDER_FIRST_BYTE_TIMEOUT_MS,
44
+ PROVIDER_NONSTREAM_TOTAL_TIMEOUT_MS,
45
+ createTimeoutSignal,
44
46
  createPassthroughSignal,
45
47
  } from '../stall-policy.mjs';
46
48
  import {
@@ -740,7 +742,11 @@ export class AnthropicOAuthProvider {
740
742
  // Test seam: injectable request factory for retry-path tests.
741
743
  const doRequestImpl = typeof opts._doRequestFn === 'function' ? opts._doRequestFn : doRequest;
742
744
 
743
- const requestWithRetry = async (accessToken, requestBody = body) => withRetry(async ({ signal: attemptSignal }) => {
745
+ const requestWithRetry = async (
746
+ accessToken,
747
+ requestBody = body,
748
+ retrySignal = totalSignal,
749
+ ) => withRetry(async ({ signal: attemptSignal }) => {
744
750
  const result = await doRequestImpl(accessToken, attemptSignal, requestBody);
745
751
  const status = Number(result?.response?.status || 0);
746
752
  const transientStatus = classifyError({ httpStatus: status }) === 'transient';
@@ -772,7 +778,7 @@ export class AnthropicOAuthProvider {
772
778
  }
773
779
  return result;
774
780
  }, {
775
- signal: totalSignal,
781
+ signal: retrySignal,
776
782
  maxAttempts: anthropicMaxAttempts(),
777
783
  backoffMs: ANTHROPIC_RETRY_BACKOFF_MS,
778
784
  retryJitterRatio: ANTHROPIC_RETRY_JITTER_RATIO,
@@ -816,28 +822,69 @@ export class AnthropicOAuthProvider {
816
822
  const issueNonStreamingFallback = async (controller, abortReason) => {
817
823
  try { controller?.abort?.(abortReason); } catch {}
818
824
  try { onStageChange?.('requesting', { transport: 'non-streaming-fallback' }); } catch {}
819
- let fallback = await requestWithRetry(creds.accessToken, { ...body, stream: false });
820
- if (fallback.response.status === 401) {
821
- cleanupCancelHandler(fallback.cancelHandler);
822
- try { fallback.controller?.abort?.(); } catch {}
823
- creds = await this.ensureAuth({ forceRefresh: true, reason: '401' });
824
- fallback = await requestWithRetry(creds.accessToken, { ...body, stream: false });
825
- }
826
- if (!fallback.response.ok) {
827
- const text = await fallback.response.text().catch(() => '');
828
- cleanupCancelHandler(fallback.cancelHandler);
829
- try { fallback.controller?.abort?.(); } catch {}
830
- const fallbackError = new Error(`Anthropic OAuth API ${fallback.response.status}: ${this.scrubTokens(text).slice(0, 200)}`);
831
- fallbackError.status = fallback.response.status;
832
- fallbackError.httpStatus = fallback.response.status;
833
- throw fallbackError;
834
- }
825
+ const timeoutMs = Number(opts._nonStreamingTimeoutMs) > 0
826
+ ? Number(opts._nonStreamingTimeoutMs)
827
+ : PROVIDER_NONSTREAM_TOTAL_TIMEOUT_MS;
828
+ const lifetime = createTimeoutSignal(
829
+ totalSignal,
830
+ timeoutMs,
831
+ 'Anthropic OAuth non-streaming fallback',
832
+ );
833
+ let fallback = null;
834
+ let lifetimeAbortHandler = null;
835
+ const releaseFallback = (reason) => {
836
+ if (lifetimeAbortHandler) {
837
+ try { lifetime.signal.removeEventListener('abort', lifetimeAbortHandler); } catch {}
838
+ lifetimeAbortHandler = null;
839
+ }
840
+ cleanupCancelHandler(fallback?.cancelHandler);
841
+ try { fallback?.controller?.abort?.(reason); } catch {}
842
+ fallback = null;
843
+ };
844
+ const requestFallback = async (accessToken) => {
845
+ const result = await requestWithRetry(
846
+ accessToken,
847
+ { ...body, stream: false },
848
+ lifetime.signal,
849
+ );
850
+ fallback = result;
851
+ lifetimeAbortHandler = () => {
852
+ try { result.controller?.abort?.(lifetime.signal.reason); } catch {}
853
+ };
854
+ if (lifetime.signal.aborted) {
855
+ lifetimeAbortHandler();
856
+ const reason = lifetime.signal.reason;
857
+ throw reason instanceof Error
858
+ ? reason
859
+ : new Error('Anthropic OAuth non-streaming fallback aborted');
860
+ }
861
+ lifetime.signal.addEventListener('abort', lifetimeAbortHandler, { once: true });
862
+ return result;
863
+ };
835
864
  try {
865
+ fallback = await requestFallback(creds.accessToken);
866
+ if (fallback.response.status === 401) {
867
+ releaseFallback('Anthropic OAuth non-streaming fallback refreshing auth');
868
+ creds = await this.ensureAuth({ forceRefresh: true, reason: '401' });
869
+ fallback = await requestFallback(creds.accessToken);
870
+ }
871
+ if (!fallback.response.ok) {
872
+ const text = await fallback.response.text().catch(() => '');
873
+ const fallbackError = new Error(`Anthropic OAuth API ${fallback.response.status}: ${this.scrubTokens(text).slice(0, 200)}`);
874
+ fallbackError.status = fallback.response.status;
875
+ fallbackError.httpStatus = fallback.response.status;
876
+ throw fallbackError;
877
+ }
836
878
  const message = await fallback.response.json();
837
879
  return normalizeAnthropicNonStreamingResponse(message, useModel);
880
+ } catch (err) {
881
+ if (lifetime.signal.aborted && lifetime.signal.reason instanceof Error) {
882
+ throw lifetime.signal.reason;
883
+ }
884
+ throw err;
838
885
  } finally {
839
- cleanupCancelHandler(fallback.cancelHandler);
840
- try { fallback.controller?.abort?.('Anthropic non-streaming fallback complete'); } catch {}
886
+ releaseFallback('Anthropic non-streaming fallback complete');
887
+ lifetime.cleanup();
841
888
  }
842
889
  };
843
890
 
@@ -47,7 +47,13 @@ function _captureMidstreamAbort(state, reason) {
47
47
  const reasonName = reason?.name || '';
48
48
  if (reasonName === 'AgentStallAbortError' || reasonName === 'StreamStalledAbortError') {
49
49
  state.watchdogAbort = reasonName;
50
- } else {
50
+ } else if (reasonName !== 'ProviderTimeoutError' && reasonName !== 'StreamStalledError') {
51
+ // Internal timeout/stall abort reasons are transport symptoms, not a
52
+ // caller decision. Recording them as userAbort silently vetoed the
53
+ // mid-stream retry ladder (_classifyMidstreamSse returns null on
54
+ // userAbort → the turn surfaced as an instant unlogged failure).
55
+ // Leaving state untouched keeps them classifiable by the thrown error
56
+ // itself (EPROVIDERTIMEOUT/ESTREAMSTALL → transient/stall retry).
51
57
  state.userAbort = true;
52
58
  }
53
59
  }
@@ -19,6 +19,7 @@ import { readStreamOutcome } from './lib/stream-outcome.mjs';
19
19
  import { traceAgentUsage } from '../agent-trace.mjs';
20
20
  import {
21
21
  PROVIDER_FIRST_BYTE_TIMEOUT_MS,
22
+ PROVIDER_NONSTREAM_TOTAL_TIMEOUT_MS,
22
23
  createTimeoutSignal,
23
24
  createPassthroughSignal,
24
25
  } from '../stall-policy.mjs';
@@ -327,25 +328,37 @@ export class AnthropicProvider {
327
328
  try { streamController.abort?.(abortReason); } catch {}
328
329
  try { onStageChange?.('requesting', { transport: 'non-streaming-fallback' }); } catch {}
329
330
  const nonStreamingParams = { ...params, stream: false };
330
- const message = await withRetry(
331
- async ({ signal: attemptSignal }) => this.client.messages.create(nonStreamingParams, {
332
- signal: attemptSignal,
333
- ...(betaHeaders ? { headers: betaHeaders } : {}),
334
- }),
335
- {
336
- signal: totalSignal,
337
- maxAttempts: anthropicMaxAttempts(),
338
- backoffMs: ANTHROPIC_RETRY_BACKOFF_MS,
339
- retryJitterRatio: ANTHROPIC_RETRY_JITTER_RATIO,
340
- retryJitterMode: 'positive',
341
- perAttemptTimeoutMs: anthropicRequestTimeoutMs(),
342
- perAttemptLabel: `${this.name} Anthropic non-streaming fallback`,
343
- provider: 'anthropic',
344
- model: useModel,
345
- fallbackModel: opts._fallbackTriggered ? undefined : opts.fallbackModel,
346
- },
331
+ const timeoutMs = Number(opts._nonStreamingTimeoutMs) > 0
332
+ ? Number(opts._nonStreamingTimeoutMs)
333
+ : PROVIDER_NONSTREAM_TOTAL_TIMEOUT_MS;
334
+ const lifetime = createTimeoutSignal(
335
+ totalSignal,
336
+ timeoutMs,
337
+ `${this.name} Anthropic non-streaming fallback`,
347
338
  );
348
- return buildReturnFromParse(normalizeAnthropicNonStreamingResponse(message, useModel));
339
+ try {
340
+ const message = await withRetry(
341
+ async ({ signal: attemptSignal }) => this.client.messages.create(nonStreamingParams, {
342
+ signal: attemptSignal,
343
+ ...(betaHeaders ? { headers: betaHeaders } : {}),
344
+ }),
345
+ {
346
+ signal: lifetime.signal,
347
+ maxAttempts: anthropicMaxAttempts(),
348
+ backoffMs: ANTHROPIC_RETRY_BACKOFF_MS,
349
+ retryJitterRatio: ANTHROPIC_RETRY_JITTER_RATIO,
350
+ retryJitterMode: 'positive',
351
+ perAttemptTimeoutMs: anthropicRequestTimeoutMs(),
352
+ perAttemptLabel: `${this.name} Anthropic non-streaming fallback`,
353
+ provider: 'anthropic',
354
+ model: useModel,
355
+ fallbackModel: opts._fallbackTriggered ? undefined : opts.fallbackModel,
356
+ },
357
+ );
358
+ return buildReturnFromParse(normalizeAnthropicNonStreamingResponse(message, useModel));
359
+ } finally {
360
+ lifetime.cleanup();
361
+ }
349
362
  };
350
363
 
351
364
  const recoverNonStreaming = async (midState, streamingError, streamController) => {
@@ -167,7 +167,7 @@ function _pushOutputTextAnnotations(part, citations, citationKeys) {
167
167
  }
168
168
  }
169
169
 
170
- function _buildOpenAIHttpFallbackHeaders({ auth, cacheKey }) {
170
+ function _buildOpenAIHttpFallbackHeaders({ auth, cacheKey, statelessConversation = false }) {
171
171
  if (auth?.type === 'openai-direct') {
172
172
  // Public API-key auth: Bearer <OPENAI_API_KEY>, no chatgpt-account-id /
173
173
  // originator (mirrors openai-ws-pool _buildHandshakeHeaders' direct
@@ -189,7 +189,7 @@ function _buildOpenAIHttpFallbackHeaders({ auth, cacheKey }) {
189
189
  'chatgpt-account-id': auth.account_id || '',
190
190
  'x-client-request-id': randomBytes(16).toString('hex'),
191
191
  };
192
- if (cacheKey) {
192
+ if (cacheKey && !statelessConversation) {
193
193
  const sid = String(cacheKey);
194
194
  // Backend-native anchors (see openai-ws-pool _buildHandshakeHeaders):
195
195
  // the hyphenated `session-id`/`thread-id` pair; legacy underscore
@@ -252,7 +252,9 @@ export async function sendViaHttpSse({
252
252
  // one still aborts, and
253
253
  // (c) externalSignal (client disconnect / replaced-by-newer-request).
254
254
  const totalTimeout = createPassthroughSignal(externalSignal);
255
- const headers = _buildOpenAIHttpFallbackHeaders({ auth, cacheKey });
255
+ const statelessConversation = opts?.statelessConversation === true
256
+ || _envFlag('MIXDOG_OAI_STATELESS_HTTP', false);
257
+ const headers = _buildOpenAIHttpFallbackHeaders({ auth, cacheKey, statelessConversation });
256
258
  const fetchStartedAt = Date.now();
257
259
  const responsesUrl = auth?.type === 'openai-direct'
258
260
  ? OPENAI_DIRECT_RESPONSES_URL