mixdog 0.9.14 → 0.9.16

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 (125) hide show
  1. package/package.json +1 -1
  2. package/scripts/bench/cache-probe-tasks.json +1 -1
  3. package/scripts/bench/lead-review-tasks-r3.json +1 -1
  4. package/scripts/bench/r4-mixed-tasks.json +1 -1
  5. package/scripts/bench/review-tasks.json +1 -1
  6. package/scripts/build-runtime-windows.ps1 +242 -242
  7. package/scripts/explore-bench.mjs +5 -4
  8. package/scripts/ingest-pure-conversation-smoke.mjs +27 -0
  9. package/scripts/output-style-smoke.mjs +3 -3
  10. package/scripts/recall-usecase-cases.json +1 -1
  11. package/scripts/smoke-runtime-negative.ps1 +106 -106
  12. package/scripts/termio-input-smoke.mjs +199 -0
  13. package/scripts/tool-efficiency-diag.mjs +88 -0
  14. package/scripts/tool-smoke.mjs +40 -24
  15. package/scripts/tui-frame-harness-shim.mjs +32 -0
  16. package/scripts/tui-frame-harness.mjs +306 -0
  17. package/src/agents/heavy-worker/AGENT.md +6 -7
  18. package/src/agents/worker/AGENT.md +6 -7
  19. package/src/lib/keychain-cjs.cjs +28 -11
  20. package/src/lib/rules-builder.cjs +6 -10
  21. package/src/mixdog-session-runtime.mjs +90 -20
  22. package/src/output-styles/simple.md +22 -24
  23. package/src/rules/agent/00-core.md +12 -11
  24. package/src/rules/agent/30-explorer.md +22 -21
  25. package/src/rules/lead/01-general.md +8 -8
  26. package/src/rules/lead/02-channels.md +3 -3
  27. package/src/rules/lead/lead-brief.md +11 -12
  28. package/src/rules/shared/01-tool.md +25 -14
  29. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +4 -3
  30. package/src/runtime/agent/orchestrator/config.mjs +1 -1
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +22 -1
  32. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +21 -1
  33. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +29 -8
  34. package/src/runtime/agent/orchestrator/providers/gemini.mjs +13 -5
  35. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
  36. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +11 -0
  37. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +18 -0
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +30 -2
  40. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +19 -0
  41. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +37 -15
  44. package/src/runtime/agent/orchestrator/session/context-utils.mjs +55 -0
  45. package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +177 -0
  46. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +12 -19
  47. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +10 -0
  48. package/src/runtime/agent/orchestrator/session/loop.mjs +22 -1
  49. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +8 -0
  50. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +13 -18
  51. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +5 -1
  52. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -1
  53. package/src/runtime/agent/orchestrator/session/manager.mjs +59 -2
  54. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +18 -0
  55. package/src/runtime/agent/orchestrator/session/store.mjs +33 -25
  56. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
  57. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +7 -5
  58. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +38 -1
  59. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +24 -0
  60. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +2 -1
  61. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +14 -1
  62. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +25 -1
  63. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +73 -3
  64. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -10
  65. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +55 -0
  66. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +21 -13
  67. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +13 -2
  68. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +44 -6
  69. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -0
  70. package/src/runtime/channels/backends/telegram.mjs +29 -9
  71. package/src/runtime/channels/lib/event-queue.mjs +6 -2
  72. package/src/runtime/channels/lib/memory-client.mjs +66 -1
  73. package/src/runtime/channels/lib/webhook/deliveries.mjs +22 -1
  74. package/src/runtime/memory/index.mjs +95 -8
  75. package/src/runtime/memory/lib/cycle-scheduler.mjs +24 -5
  76. package/src/runtime/memory/lib/memory-cycle1.mjs +45 -3
  77. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +7 -3
  78. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +54 -10
  79. package/src/runtime/memory/lib/memory-cycle2.mjs +21 -8
  80. package/src/runtime/memory/lib/memory-embed.mjs +48 -0
  81. package/src/runtime/memory/lib/memory-recall-store.mjs +57 -13
  82. package/src/runtime/memory/lib/memory.mjs +88 -1
  83. package/src/runtime/memory/lib/session-ingest.mjs +34 -3
  84. package/src/runtime/memory/lib/trace-store.mjs +52 -3
  85. package/src/runtime/memory/lib/transcript-ingest.mjs +27 -4
  86. package/src/runtime/shared/child-guardian.mjs +4 -2
  87. package/src/runtime/shared/open-url.mjs +2 -1
  88. package/src/runtime/shared/singleton-owner.mjs +26 -0
  89. package/src/runtime/shared/tool-execution-contract.mjs +25 -0
  90. package/src/runtime/shared/transcript-writer.mjs +3 -1
  91. package/src/session-runtime/config-helpers.mjs +7 -2
  92. package/src/session-runtime/cwd-plugins.mjs +6 -1
  93. package/src/session-runtime/effort.mjs +6 -1
  94. package/src/session-runtime/plugin-mcp.mjs +116 -12
  95. package/src/session-runtime/settings-api.mjs +7 -1
  96. package/src/standalone/channel-worker.mjs +25 -3
  97. package/src/standalone/explore-tool.mjs +5 -5
  98. package/src/standalone/hook-bus/config.mjs +38 -2
  99. package/src/standalone/hook-bus/handlers.mjs +103 -11
  100. package/src/standalone/hook-bus.mjs +20 -6
  101. package/src/standalone/memory-runtime-proxy.mjs +40 -5
  102. package/src/tui/App.jsx +214 -30
  103. package/src/tui/app/core-memory-picker.mjs +6 -6
  104. package/src/tui/app/model-options.mjs +10 -4
  105. package/src/tui/app/settings-picker.mjs +5 -30
  106. package/src/tui/app/slash-commands.mjs +0 -1
  107. package/src/tui/app/slash-dispatch.mjs +0 -16
  108. package/src/tui/app/text-layout.mjs +57 -0
  109. package/src/tui/app/transcript-window.mjs +53 -2
  110. package/src/tui/app/use-mouse-input.mjs +60 -47
  111. package/src/tui/app/use-transcript-window.mjs +38 -10
  112. package/src/tui/components/PromptInput.jsx +18 -1
  113. package/src/tui/components/TextEntryPanel.jsx +97 -33
  114. package/src/tui/dist/index.mjs +567 -229
  115. package/src/tui/engine/tool-card-results.mjs +22 -5
  116. package/src/tui/engine.mjs +126 -7
  117. package/src/tui/index.jsx +4 -1
  118. package/src/workflows/default/WORKFLOW.md +27 -31
  119. package/vendor/ink/build/components/App.js +62 -17
  120. package/vendor/ink/build/ink.js +78 -3
  121. package/vendor/ink/build/input-parser.d.ts +9 -4
  122. package/vendor/ink/build/input-parser.js +45 -176
  123. package/vendor/ink/build/log-update.js +47 -2
  124. package/vendor/ink/build/termio-keypress.js +240 -0
  125. package/vendor/ink/build/termio-tokenize.js +253 -0
@@ -12,34 +12,32 @@ Practical concise — outcome-first handoffs for coding work: summarize the
12
12
  result, do not narrate or explain the change.
13
13
 
14
14
  - Open with the outcome in one sentence: done, blocked, or awaiting a decision.
15
- - Summarize at the concept level: name the behavior and direction, not the code
16
- path. Cite a symbol/path only as an anchor, never as the explanation.
15
+ - Summarize at the concept level: name the behavior and direction, not the
16
+ code path; cite a symbol/path only as an anchor, never as the explanation.
17
17
  - Compress by cutting content (filler, hedging, pleasantries, restated facts),
18
- not by clipping grammar: keep natural, complete sentences in the user's
19
- language — never telegraph-style stub endings. Technical terms and code stay
20
- exact.
21
- - Summarize what the change accomplishes rather than listing every file and how
22
- each was edited. Name a path (`file_path:line_number`) only when the reader
23
- truly needs it to navigate not as a per-file changelog.
24
- - Keep controlled detail: usually 1–3 short bullets or 2–3 sentences total.
25
- State each point once — outcome or fix direction, not both restated. No
26
- step-by-step narration or file/line inventory.
27
- - Size budget: roughly HALF the Default style and TWICE Minimal per point one
28
- rendered line, whole reply ~5–7 lines. Above that you are writing Default;
18
+ not grammar: keep natural, complete sentences in the user's language — never
19
+ telegraph-style stub endings. Technical terms and code stay exact.
20
+ - Summarize what the change accomplishes, not a per-file changelog. Name a
21
+ path (`file_path:line_number`) only when the reader truly needs it to
22
+ navigate.
23
+ - Controlled detail: usually 1–3 short bullets or 2–3 sentences total; state
24
+ each point once outcome or fix direction, not both. No step-by-step
25
+ narration or file/line inventory.
26
+ - Size budget: roughly HALF the Default style and TWICE Minimal — one rendered
27
+ line per point, whole reply ~5–7 lines. Above that you are writing Default;
29
28
  below ~3 lines consider whether prose (Minimal) reads better.
30
29
  - Layout: one idea per bullet, ONE line each (two only when a verbatim
31
- path/error forces it). Lead each item with a short bold key phrase. Put a
32
- blank line between multi-line list items — never emit a dense wall of text.
33
- - If a point runs past one line, cut the elaboration instead of wrapping; detail
34
- beyond the key phrase + one clause belongs to the Default style.
35
- - On final handoffs, optional labels such as `바뀐 점`, `확인한 것`, and
36
- `남은 리스크/다음 단계` fit Korean-facing profiles; use plain English labels
37
- when the thread is English. Do not label interim progress.
30
+ path/error forces it), led with a short bold key phrase; blank line between
31
+ multi-line list items — never a dense wall of text. If a point runs past one
32
+ line, cut the elaboration detail beyond key phrase + one clause belongs to
33
+ Default.
34
+ - Final handoffs may use labels like `바뀐 점`, `확인한 것`,
35
+ `남은 리스크/다음 단계` for Korean-facing profiles (plain English labels in
36
+ English threads); do not label interim progress.
38
37
  - Synthesize agent or retrieval results; never forward raw reports, long file
39
38
  lists, tool traces, or session metadata.
40
- - Do not hide blockers, failed verification, or required follow-up; state them
41
- in one short clause.
39
+ - Do not hide blockers, failed verification, or required follow-up state
40
+ them in one short clause; if verification was not run, say so once.
42
41
  - Keep paths, commands, symbols, API names, code, and exact errors verbatim.
43
- - Skip filler, acknowledgments, hedging, and repeated conclusions; if
44
- verification was not run, say so once.
42
+ - Skip filler, acknowledgments, hedging, and repeated conclusions.
45
43
  - Never name this style unless asked.
@@ -1,19 +1,20 @@
1
1
  # Agent Constraints
2
2
 
3
3
  - Use English for agent task communication.
4
- - NEVER PREAMBLE: no tool-call preambles, status/progress narration, "I
5
- will..." setup, or transition text before tool calls.
6
- - If tools are needed, call them immediately. Emit text only for the final
7
- handoff after tool work is done.
4
+ - ONE TURN = ONE BATCH: emit all independent tool calls of a step in the same
5
+ turn; a lone read-only call that could have ridden the previous turn is a
6
+ wasted round-trip.
7
+ - NEVER PREAMBLE: no status/progress narration, "I will..." setup, or any text
8
+ before tool calls — call needed tools immediately; emit text only for the
9
+ final handoff after tool work is done.
8
10
  - Final handoff: minimum characters, maximum information for Lead. Follow the
9
- role's stricter output contract if defined; else emit fragments — outcome
10
- (1 line), key `file:line`(s), verification result, material risks (only if
11
+ role's stricter output contract if defined; else fragments — outcome (1
12
+ line), key `file:line`(s), verification result, material risks (only if
11
13
  any).
12
- - Density over length: never repeat what Lead already knows (the brief, the
13
- process, how you searched); state only what changed and where to look.
14
- Verification = command + result in one line. Same fact twice = delete one.
15
- - Handoff cap ~30 lines unless `Deliver:` raises it — a safety ceiling, not
16
- a target.
14
+ - Never repeat what Lead already knows (the brief, the process, how you
15
+ searched); state only what changed and where to look. Verification =
16
+ command + result in one line. Same fact twice = delete one.
17
+ - Handoff cap ~30 lines unless `Deliver:` raises it — a ceiling, not a target.
17
18
  - Banned as pure cost: report headings, markdown tables (unless requested),
18
19
  prose narration, raw logs/tool traces, speculative next-checks, restated
19
20
  brief, articles/politeness.
@@ -6,28 +6,29 @@ kind: retrieval
6
6
 
7
7
  # Role: explorer
8
8
 
9
- You are a one-shot locator, not a researcher. You find WHERE, never WHY —
10
- no understanding code, explaining behaviour, or tracing logic. Coordinates
11
- are the entire deliverable.
9
+ Coordinate locator. Deliverable = WHERE as `path:line`, never WHY — no
10
+ explaining or tracing. You ARE the `explore` tool: never call it; shared
11
+ rules pointing at `explore` don't apply — use grep/code_graph/find/glob
12
+ directly.
12
13
 
13
- Default shape: ONE maximal-batch turn, then the answer. Fire everything at
14
- once: a single `grep` whose pattern[] carries ALL literal strings/errors/
15
- identifier guesses synonyms, casings, variants plus `code_graph`/`find`/
16
- `glob` in the SAME turn when useful. Width is free, turns are not.
14
+ Credibility is mechanical: hit = line/path contains ANY query token or
15
+ obvious synonym. No other standard "is this the real implementation" is
16
+ the caller's question. Sole exception: a generic-word-only match (schema,
17
+ handler, config, resolver…) with no SPECIFIC query token (product/library/
18
+ domain name) = zero, not a hit.
17
19
 
18
- Answer straight from search output: hits already carry `path:line` never
19
- spend a turn on `read` to confirm or polish an anchor. `dist`/generated
20
- hits are leads, not answers: trace them to source.
20
+ Rule zero after EVERY tool result: ≥1 `path:line` matching a query
21
+ token answer NOW with those coordinates; no verify, no re-grep, no
22
+ ranking; mark weak anchors `?`. Zero one more batch if budget remains.
23
+ No third branch: "hits exist but I want better ones" IS branch one.
21
24
 
22
- Budget: 5 turns of maximal batches. Credible `path:line` on screen means the
23
- search is OVER answer in that turn; no verifying or collecting extras;
24
- mark weak anchors `?`. Turns 2-5 exist ONLY for a zero-anchor turn 1: derive
25
- new tokens from returned paths/names or widen scope, packing ALL candidates
26
- into the next batch. Concept queries: grep implementation vocabulary
27
- (file/dir names, symbol fragments, config keys). `EXPLORATION_FAILED` only
28
- after all 5 turns found zero anchors — giving up early is as wrong as
29
- over-searching; at turn 5 answer best-so-far.
25
+ Turns: 3 tool turns HARD max; start each tool message with `turn N/3`.
26
+ Turn 1 answer; turns 2-3 are miss-recovery only, and must change tokens
27
+ OR scope never reword. One turn = ONE batch of AT MOST 5 calls: a single
28
+ grep packing ALL variants in pattern[] plus code_graph/find/glob; a 6th
29
+ call = packing failure. Flow/how questions: return first matching entry
30
+ anchors (≤5); the caller traces the chain.
30
31
 
31
- Answer format, nothing else:
32
- - up to 5 lines: `path:linesymbol/name short reason` (append `?` if weak)
33
- - or `EXPLORATION_FAILED`
32
+ Answer, nothing else: up to 5 lines `path:line — symbol — short reason`
33
+ (`?` if weak), or `EXPLORATION_FAILED`only after 3 turns of zero
34
+ anchors, or when every anchor matches only generic words.
@@ -1,11 +1,11 @@
1
1
  # General
2
2
 
3
- - Omit direct names, honorifics, headings, and labels in preambles.
4
- - Preambles are optional: use only when they add user-visible value, keep to
5
- one short sentence, skip routine lookup narration.
3
+ - Preambles are optional: one short sentence only when it adds user-visible
4
+ value; no direct names, honorifics, headings, labels, or routine lookup
5
+ narration.
6
6
  - Destructive/hard-to-reverse actions require explicit confirmation.
7
- - Never push, build, or deploy without an explicit user request.
8
- Implementation approval is not deploy approval.
9
- - Proactively handle what you can first rather than deferring to the user;
10
- propose only the parts needing a decision, consultatively ("Shall we
11
- proceed this way?").
7
+ - Never push, build, or deploy without an explicit user request
8
+ implementation approval is not deploy approval.
9
+ - Handle what you can proactively rather than deferring to the user; propose
10
+ only the parts needing a decision, consultatively ("Shall we proceed this
11
+ way?").
@@ -1,3 +1,3 @@
1
- # Channels
2
-
3
- - Channel features are handled by the runtime.
1
+ # Channels
2
+
3
+ - Channel features are handled by the runtime.
@@ -1,21 +1,20 @@
1
1
  # Lead Brief Contract
2
2
 
3
3
  - Brief = one-line fragments `Goal:` `Anchors:` `Allow/Forbid:` `Deliver:`
4
- `Verify:` (+`Stop:` heavy-worker). No role-known rules, background, or
5
- motivation minimum characters, maximum information.
6
- - Density over length: every line must carry information the agent cannot
7
- know — never restate role rules, prior context, or the same fact twice.
8
- A brief that feels long is a scope problem; split the scope, don't pad.
9
- - Anchors = `file:line` + one-line conclusion each. Never paste log/code
10
- bodies; the agent reads the file itself.
11
- - Instruct by outcome ("make X behave Y"), not by method (no code snippets,
12
- no step-by-step edits) unless the method IS the requirement.
4
+ `Verify:` (+`Stop:` heavy-worker). Minimum characters, maximum information:
5
+ no background/motivation, no role-known rules; never restate prior context
6
+ or the same fact twice. A brief that feels long is a scope problem — split
7
+ the scope, don't pad.
8
+ - Anchors = `file:line` + one-line conclusion each; never paste log/code
9
+ bodies the agent reads the file itself.
10
+ - Instruct by outcome ("make X behave Y"), not method (no code snippets, no
11
+ step-by-step edits) unless the method IS the requirement.
13
12
  - `Deliver:` states output size/shape (e.g. "fragments <=15 lines", "verdict
14
- + top-3 risks", "detail to file, path only"). Never request a long report
13
+ + top-3 risks", "detail to file, path only"); never request a long report
15
14
  in the handoff itself.
16
15
  - Full brief only on fresh spawn or `respawned: true` (dead-tag send = cold
17
- session; re-supply anchors). Live-session follow-ups = delta only; never
18
- restate Goal/rules.
16
+ session; re-supply anchors); live-session follow-ups = delta only, never
17
+ restating Goal/rules.
19
18
  - Never `send` mid-run; batch all adjustments into ONE follow-up after
20
19
  completion. Interrupt only to cancel.
21
20
  - All agent communication in English.
@@ -4,18 +4,29 @@
4
4
  prior result — including edit loops: batch the post-edit verification read
5
5
  with the next target's lookup.
6
6
  - Pick by target: symbols/callers/deps → `code_graph`; exact text in a verified
7
- scope → `grep`; unknown path/name → `find`; structure → `glob`; dirs →
8
- `list`; verified file `read`; broad unknown with no anchor → `explore`.
9
- Never `grep`/`read` guessed paths.
10
- - One concept one batched `grep pattern:[...]` with
11
- `output_mode:"content_with_context"` (or one `code_graph symbols[]`). Refine
12
- from returned paths; never repeat equivalent patterns or scopes.
13
- - On miss/error, normalize the target once and switch tool; on a plausible hit,
14
- stop and answer from the framed context.
15
- - Retrieval serves the NEXT action (edit, answer, handoff), not certainty. One
16
- anchor is enough to act on; re-reading/re-grepping an area seen this session
17
- is waste. When acting and looking are both possible, act.
18
- - `read` uses `offset`/`limit` only. For 2+ spans from known file(s), make one
19
- batched `read` with `{path,offset,limit}` region objects. Adjacent spans in
20
- one file (within a few hundred lines) are ONE window, not repeated reads.
7
+ scope → `grep`; dirs → `list`; verified file → `read`.
8
+ Locating files: concept-level or cross-file questions ("where is X handled",
9
+ "how does Y flow") → `explore`, even when you could guess a keyword — its
10
+ fan-out spends explorer context, not yours, and guess-and-retry keywords
11
+ belong to it, not grep. Confident name fragment → `find`; exact name
12
+ pattern `glob`; exact content literal `grep`. Flow/trace with a known
13
+ symbol `code_graph`, never `explore` it returns entry-point anchors
14
+ only; follow the chain yourself. Never `grep`/`read` guessed paths.
15
+ - One concept ONE search call: all synonyms/variants in one
16
+ `grep pattern:[...]` (or `code_graph symbols[]`), scopes as `path[]`;
17
+ single-pattern bursts = packing failure. A second call on the same concept
18
+ is a defect unless the first returned zero then change tokens or scope,
19
+ never reword.
20
+ - Content-mode `grep` always carries `-C 5`+ ; re-reading the same file after
21
+ a content grep is a wasted round-trip. Bare match lines = existence checks
22
+ only.
23
+ - Any path not returned by a tool this session is a guess: find first, then
24
+ grep/read the returned path. On ENOENT the next call is find on the
25
+ basename — never a second guess or the same path with changed args.
26
+ - Retrieval serves the NEXT action (edit, answer, handoff), not certainty:
27
+ the FIRST anchor ends the search — no verifying, ranking, or re-checking a
28
+ hit already on screen. When acting and looking are both possible, act.
29
+ - `read` uses `offset`/`limit` only; 2+ spans from known file(s) = ONE batched
30
+ `read` with `{path,offset,limit}` regions. Adjacent spans in one file
31
+ (within a few hundred lines) are ONE window, not repeated reads.
21
32
  - Don't mix `apply_patch` with shell or other state-changing calls in one turn.
@@ -207,12 +207,13 @@ function _redactLogText(text) {
207
207
  function classifyToolFailure(resultText, toolName) {
208
208
  const text = String(resultText ?? '').toLowerCase();
209
209
  if (/requires either|invalid arguments|unknown parameter|must be|schema|expected|required|old_string is .*>=/.test(text)) return 'schema/args';
210
- if (/enoent|cannot find|not found at this path|path does not exist|no such file|file not found in graph|unreadable/.test(text)) return 'path/cwd';
210
+ if (/not in allow-list|not allowed/.test(text)) return 'permission';
211
+ if (String(toolName || '') === 'shell' || /^\s*\[exit code:\s*\d+\]/i.test(String(resultText ?? ''))) return 'command-exit';
212
+ if (/enoent|cannot find|not found at this path|path does not exist|no such file|file not found in graph|unreadable/.test(text)) return 'path/enoent';
211
213
  if (/timed out|timeout|interrupted|aborted/.test(text)) return 'timeout/abort';
212
214
  if (/hunk rejected|patch failed|context mismatch|expected first old\/context|context not found/.test(text)) return 'patch/context';
213
- if (/not in allow-list|permission|denied|forbidden|not allowed/.test(text)) return 'permission';
215
+ if (/permission|denied|forbidden/.test(text)) return 'permission';
214
216
  if (/unknown tool|tool.*not.*available|missing.*tool/.test(text)) return 'tool-surface';
215
- if (String(toolName || '') === 'shell' || /^\s*\[exit code:\s*\d+\]/i.test(String(resultText ?? ''))) return 'command-exit';
216
217
  return 'runtime/failure';
217
218
  }
218
219
 
@@ -149,7 +149,7 @@ function buildDefaultConfig(options = {}) {
149
149
  const providers = {};
150
150
  // API providers — enabled if env key exists
151
151
  for (const [name, envKey] of Object.entries(AGENT_PROVIDER_ENV)) {
152
- const apiKey = process.env[envKey];
152
+ const apiKey = detectCredentials ? process.env[envKey] : undefined;
153
153
  providers[name] = {
154
154
  enabled: !!apiKey,
155
155
  apiKey: apiKey || undefined,
@@ -24,7 +24,7 @@ import {
24
24
  resolveAnthropicModelAfter404,
25
25
  ensureLatestAnthropicModel,
26
26
  } from './anthropic-model-resolve.mjs';
27
- import { sanitizeToolPairs, sanitizeAnthropicContentPairs } from '../session/context-utils.mjs';
27
+ import { sanitizeToolPairs, sanitizeAnthropicContentPairs, foldUserTextIntoToolResultTail } from '../session/context-utils.mjs';
28
28
  import {
29
29
  TOKEN_REFRESH_SKEW_MS,
30
30
  resolveCliVersion,
@@ -303,6 +303,11 @@ function nativeAnthropicTools(opts) {
303
303
  }
304
304
  function deferredAnthropicTools(activeTools, opts) {
305
305
  if (opts?.session?.deferredNativeTools !== true) return [];
306
+ // A request whose ONLY tools are deferred is rejected by the API with
307
+ // `400: At least one tool must have defer_loading=false` — happens on the
308
+ // iteration-cap final turn (loop sends tools: [] to force a text answer).
309
+ // No active tools ⇒ send no deferred catalog either.
310
+ if (!Array.isArray(activeTools) || activeTools.length === 0) return [];
306
311
  const active = new Set((activeTools || []).map((tool) => String(tool?.name || '').trim()).filter(Boolean));
307
312
  const catalog = Array.isArray(opts.session.deferredToolCatalog) ? opts.session.deferredToolCatalog : [];
308
313
  return catalog
@@ -362,6 +367,14 @@ function toAnthropicMessages(messages) {
362
367
  continue;
363
368
  }
364
369
 
370
+ // Claude Code parity: fold a user text turn that directly follows a
371
+ // tool_result turn into that tool_result's content. A sibling text
372
+ // turn after tool_result renders as `</function_results>\n\nHuman:`
373
+ // on the wire and trains the model toward 3-token empty end_turn
374
+ // completions (see foldUserTextIntoToolResultTail).
375
+ if (m.role === 'user' && foldUserTextIntoToolResultTail(result, normalizeContentForAnthropic(m.content))) {
376
+ continue;
377
+ }
365
378
  result.push({ role: m.role, content: normalizeContentForAnthropic(m.content) });
366
379
  }
367
380
  return sanitizeAnthropicContentPairs(result);
@@ -1055,6 +1068,14 @@ export class AnthropicOAuthProvider {
1055
1068
  if (attemptIndex > 0 && firstAttemptError) {
1056
1069
  try { firstAttemptError.midstreamRetries = attemptIndex; } catch {}
1057
1070
  try { firstAttemptError.midstreamClassifier = firstAttemptClassifier; } catch {}
1071
+ // firstAttemptError is what actually propagates here when
1072
+ // live text was emitted this attempt — stamp the unsafe
1073
+ // flags onto IT too, else upstream sees an unmarked error
1074
+ // and retries, duplicating already-streamed output.
1075
+ try {
1076
+ firstAttemptError.liveTextEmitted = true;
1077
+ firstAttemptError.unsafeToRetry = true;
1078
+ } catch {}
1058
1079
  throw firstAttemptError;
1059
1080
  }
1060
1081
  throw err;
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { loadConfig } from '../config.mjs';
3
- import { sanitizeToolPairs, sanitizeAnthropicContentPairs } from '../session/context-utils.mjs';
3
+ import { sanitizeToolPairs, sanitizeAnthropicContentPairs, foldUserTextIntoToolResultTail } from '../session/context-utils.mjs';
4
4
  import { classifyError, midstreamBackoffFor, sleepWithAbort, withRetry } from './retry-classifier.mjs';
5
5
  import { traceAgentUsage } from '../agent-trace.mjs';
6
6
  import {
@@ -310,6 +310,10 @@ function nativeAnthropicTools(opts) {
310
310
  }
311
311
  function deferredAnthropicTools(activeTools, opts) {
312
312
  if (opts?.session?.deferredNativeTools !== true) return [];
313
+ // Guard against an all-deferred tools array — the API rejects it with
314
+ // `400: At least one tool must have defer_loading=false` (iteration-cap
315
+ // final turn sends tools: []). See anthropic-oauth.mjs counterpart.
316
+ if (!Array.isArray(activeTools) || activeTools.length === 0) return [];
313
317
  const active = new Set((activeTools || []).map((tool) => String(tool?.name || '').trim()).filter(Boolean));
314
318
  const catalog = Array.isArray(opts.session.deferredToolCatalog) ? opts.session.deferredToolCatalog : [];
315
319
  return catalog
@@ -366,6 +370,12 @@ function toAnthropicMessages(messages) {
366
370
  }
367
371
  continue;
368
372
  }
373
+ // Claude Code parity: fold a user text turn that directly follows a
374
+ // tool_result turn into that tool_result's content (empty end_turn
375
+ // livelock prevention; see foldUserTextIntoToolResultTail).
376
+ if (m.role === 'user' && foldUserTextIntoToolResultTail(result, normalizeContentForAnthropic(m.content))) {
377
+ continue;
378
+ }
369
379
  result.push({ role: m.role, content: normalizeContentForAnthropic(m.content) });
370
380
  }
371
381
  return sanitizeAnthropicContentPairs(result);
@@ -808,6 +818,16 @@ export class AnthropicProvider {
808
818
  if (attemptIndex > 0 && firstAttemptError) {
809
819
  try { firstAttemptError.midstreamRetries = attemptIndex; } catch {}
810
820
  try { firstAttemptError.midstreamClassifier = firstAttemptClassifier; } catch {}
821
+ // firstAttemptError (from an earlier retried attempt)
822
+ // is what actually propagates when live text was
823
+ // emitted this attempt — stamp the unsafe/partial
824
+ // flags onto IT too, otherwise upstream sees an
825
+ // unmarked error and retries, duplicating streamed
826
+ // output.
827
+ try {
828
+ firstAttemptError.liveTextEmitted = true;
829
+ firstAttemptError.unsafeToRetry = true;
830
+ } catch {}
811
831
  throw firstAttemptError;
812
832
  }
813
833
  throw err;
@@ -47,22 +47,38 @@ export function geminiTruncatedStreamError(message) {
47
47
  // stall after visible output was silently retried. Visible-text stalls also
48
48
  // gain streamStalled + partialContent so the loop's partial-final path can
49
49
  // keep the streamed output instead of dropping the turn.
50
- export function stampGeminiStreamFailure(err, { relayedText = '', textLeakGuard = null } = {}) {
50
+ export function stampGeminiStreamFailure(err, { relayedText = '', textLeakGuard = null, sawFunctionCall = false } = {}) {
51
51
  if (!err || typeof err !== 'object') return err;
52
52
  const leaked = (textLeakGuard?.getLeakedToolCalls?.() || []).length > 0;
53
53
  const visible = relayedText.length > 0;
54
+ // A native functionCall chunk (not a text-leaked one) is also an in-flight
55
+ // tool use — replaying would double-dispatch, and partial-final must NOT
56
+ // treat it as a clean no-tool summary.
57
+ const pendingTool = leaked || sawFunctionCall === true;
54
58
  try {
55
59
  if (visible) { err.liveTextEmitted = true; err.unsafeToRetry = true; }
56
- if (leaked) { err.emittedToolCall = true; err.unsafeToRetry = true; }
57
- if (visible && !leaked && err.code === 'EGEMINITIMEOUT') {
60
+ if (pendingTool) { err.emittedToolCall = true; err.unsafeToRetry = true; }
61
+ // TRUNCATED_STREAM EOF after visible output must also carry the
62
+ // partial-final stamps (streamStalled/partialContent), aligning with the
63
+ // compat streams — otherwise live output is dropped instead of kept.
64
+ if (visible && !leaked
65
+ && (err.code === 'EGEMINITIMEOUT' || err.code === 'TRUNCATED_STREAM' || err.truncatedStream === true)) {
58
66
  err.streamStalled = true;
59
67
  if (typeof err.partialContent !== 'string') err.partialContent = relayedText;
60
- if (err.pendingToolUse === undefined) err.pendingToolUse = false;
68
+ if (err.pendingToolUse === undefined) err.pendingToolUse = pendingTool;
61
69
  }
62
70
  } catch { /* best-effort */ }
63
71
  return err;
64
72
  }
65
73
 
74
+ // True when a streamed Gemini chunk carries a native functionCall part (as
75
+ // opposed to a tool call leaked as plain text, tracked by textLeakGuard).
76
+ export function geminiChunkHasFunctionCall(chunk) {
77
+ const parts = chunk?.candidates?.[0]?.content?.parts;
78
+ if (!Array.isArray(parts)) return false;
79
+ return parts.some((p) => p && p.functionCall);
80
+ }
81
+
66
82
  /**
67
83
  * Aggregate streamed GenerateContentResponse chunks into one response object
68
84
  * (same shape as a non-streaming generateContent JSON body).
@@ -245,6 +261,7 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
245
261
  let idleTimer = null;
246
262
  let idleReject = null;
247
263
  let relayedText = '';
264
+ let sawFunctionCall = false;
248
265
 
249
266
  let firstByteTimer = setTimeout(() => {
250
267
  try { reader.cancel('first byte timeout'); } catch {}
@@ -332,6 +349,7 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
332
349
  }
333
350
  allChunks.push(parsed);
334
351
  try { onStreamDelta?.(); } catch {}
352
+ if (!sawFunctionCall && geminiChunkHasFunctionCall(parsed)) sawFunctionCall = true;
335
353
  if (onTextDelta || textLeakGuard) {
336
354
  const t = geminiChunkText(parsed);
337
355
  if (t) relayedText += t;
@@ -352,6 +370,7 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
352
370
  }
353
371
  allChunks.push(parsed);
354
372
  try { onStreamDelta?.(); } catch {}
373
+ if (!sawFunctionCall && geminiChunkHasFunctionCall(parsed)) sawFunctionCall = true;
355
374
  if (onTextDelta || textLeakGuard) {
356
375
  const t = geminiChunkText(parsed);
357
376
  if (t) relayedText += t;
@@ -362,7 +381,7 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
362
381
  }
363
382
  }
364
383
  } catch (err) {
365
- throw stampGeminiStreamFailure(err, { relayedText, textLeakGuard });
384
+ throw stampGeminiStreamFailure(err, { relayedText, textLeakGuard, sawFunctionCall });
366
385
  } finally {
367
386
  clearFirstByteTimer();
368
387
  if (idleTimer) clearTimeout(idleTimer);
@@ -380,7 +399,7 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
380
399
  try {
381
400
  assertGeminiStreamCompleted({ sawStreamChunk, finishReason, label });
382
401
  } catch (err) {
383
- throw stampGeminiStreamFailure(err, { relayedText, textLeakGuard });
402
+ throw stampGeminiStreamFailure(err, { relayedText, textLeakGuard, sawFunctionCall });
384
403
  }
385
404
  return aggregated;
386
405
  }
@@ -393,6 +412,7 @@ export async function consumeGeminiSdkStream(streamResult, { signal, onStreamDel
393
412
  let firstByteTimer = null;
394
413
  let inFlightReject = null;
395
414
  let relayedText = '';
415
+ let sawFunctionCall = false;
396
416
 
397
417
  const armFirstByteTimer = () => {
398
418
  if (firstByteTimer) clearTimeout(firstByteTimer);
@@ -504,6 +524,7 @@ export async function consumeGeminiSdkStream(streamResult, { signal, onStreamDel
504
524
  resetIdleTimer();
505
525
  if (step.value) collectedChunks.push(step.value);
506
526
  try { onStreamDelta?.(); } catch {}
527
+ if (!sawFunctionCall && geminiChunkHasFunctionCall(step.value)) sawFunctionCall = true;
507
528
  if (onTextDelta || textLeakGuard) {
508
529
  const t = geminiChunkText(step.value);
509
530
  if (t) relayedText += t;
@@ -519,7 +540,7 @@ export async function consumeGeminiSdkStream(streamResult, { signal, onStreamDel
519
540
  const reason = signal.reason;
520
541
  throw reason instanceof Error ? reason : new Error(`${label} aborted`);
521
542
  }
522
- throw stampGeminiStreamFailure(err, { relayedText, textLeakGuard });
543
+ throw stampGeminiStreamFailure(err, { relayedText, textLeakGuard, sawFunctionCall });
523
544
  } finally {
524
545
  clearFirstByteTimer();
525
546
  if (idleTimer) clearTimeout(idleTimer);
@@ -559,7 +580,7 @@ export async function consumeGeminiSdkStream(streamResult, { signal, onStreamDel
559
580
  try {
560
581
  assertGeminiStreamCompleted({ sawStreamChunk, finishReason, label });
561
582
  } catch (err) {
562
- throw stampGeminiStreamFailure(err, { relayedText, textLeakGuard });
583
+ throw stampGeminiStreamFailure(err, { relayedText, textLeakGuard, sawFunctionCall });
563
584
  }
564
585
  return raw;
565
586
  }
@@ -68,7 +68,7 @@ const DEFAULT_MODEL = MODELS[0].id;
68
68
  const MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
69
69
  // Bump when the on-disk cache shape changes so stale-shape entries are
70
70
  // discarded instead of misread.
71
- const GEMINI_MODEL_CACHE_SCHEMA_VERSION = 1;
71
+ const GEMINI_MODEL_CACHE_SCHEMA_VERSION = 2;
72
72
 
73
73
  // De-dupes concurrent force-refreshes so they share one HTTP round-trip,
74
74
  // mirroring anthropic-oauth's _modelRefreshInFlight.
@@ -155,6 +155,10 @@ export class GeminiProvider {
155
155
  const cfg = freshConfig.providers?.gemini;
156
156
  const newKey = cfg?.apiKey || process.env.GEMINI_API_KEY;
157
157
  if (newKey) {
158
+ // Keep this.config in sync so REST/cache paths (which read the
159
+ // key via _getApiKey() → this.config.apiKey) don't keep using
160
+ // the stale key after a rotation; genAI alone is not enough.
161
+ this.config = { ...(this.config || {}), ...(cfg || {}), apiKey: newKey };
158
162
  this.genAI = new GoogleGenerativeAI(newKey);
159
163
  }
160
164
  } catch { /* best effort */ }
@@ -871,10 +875,11 @@ export class GeminiProvider {
871
875
  if (!res.ok) throw new Error(`gemini list_models ${res.status}`);
872
876
  const data = await res.json();
873
877
  const items = Array.isArray(data?.models) ? data.models : [];
874
- // Filter to Gemini family; skip embedding/imagen endpoints.
878
+ // Filter to Gemini family; skip embedding/imagen and non-coding SKUs
879
+ // (robotics, computer-use) that the chat picker should never show.
875
880
  const normalized = items
876
881
  .filter(m => (m?.name || '').includes('gemini'))
877
- .filter(m => !/embedding|aqa|imagen/.test(m?.name || ''))
882
+ .filter(m => !/embedding|aqa|imagen|robotics|computer-use/.test(m?.name || ''))
878
883
  .map(m => {
879
884
  const id = (m.name || '').replace(/^models\//, '');
880
885
  const family = /flash-lite/.test(id) ? 'gemini-flash-lite'
@@ -886,8 +891,11 @@ export class GeminiProvider {
886
891
  display: m.displayName || id,
887
892
  family,
888
893
  provider: 'gemini',
889
- contextWindow: m.inputTokenLimit || 1000000,
890
- outputTokens: m.outputTokenLimit || 8192,
894
+ // No fabricated fallbacks: when the API omits limits leave
895
+ // them null so enrichModels can fill from the catalog and
896
+ // the picker shows no context label instead of a fake 1M.
897
+ contextWindow: m.inputTokenLimit || null,
898
+ outputTokens: m.outputTokenLimit || null,
891
899
  tier: 'version',
892
900
  latest: false,
893
901
  description: m.description || '',
@@ -250,7 +250,7 @@ function saveTokens(tokens) {
250
250
  expires_at: tokens.expires_at || 0,
251
251
  token_endpoint: tokens.token_endpoint || null,
252
252
  user_id: tokens.user_id || tokens.userId || _identityFromAccessToken(tokens.access_token).user_id || undefined,
253
- }, { lock: true, fsyncDir: true });
253
+ }, { lock: true, fsyncDir: true, mode: 0o600, secret: true });
254
254
  }
255
255
 
256
256
  function _scrubTokens(text) {
@@ -41,6 +41,7 @@ const NON_CHAT_RE = new RegExp(
41
41
  'dall[-_]?e', 'sora', 'imagen', 'imagine', 'veo', 'kling', 'runway',
42
42
  'realtime', 'transcribe', 'transcription', 'diarize',
43
43
  'guard', 'safety', 'classifier',
44
+ 'robotics', 'computer[-_]?use',
44
45
  ].join('|') +
45
46
  ')([-_/\\s]|$)',
46
47
  'i',
@@ -64,6 +65,15 @@ const NON_LLM_ID_PATTERNS = [
64
65
  /^gpt-audio(\b|-)/i, // gpt-audio, gpt-audio-mini, gpt-audio-1.5
65
66
  /^gpt-realtime(\b|-)/i, // gpt-realtime*, gpt-realtime-mini
66
67
  /-tts(\b|-)/i, // any *-tts SKU
68
+ // Purpose-specific SKUs that report mode:'chat' but are never useful for a
69
+ // coding agent (embodied/robotics, browser computer-use, audio-native,
70
+ // customtools picker-duplicate variants).
71
+ /(^|-)robotics(\b|-)/i, // gemini-robotics-er-*
72
+ /computer[-_]?use/i, // gemini-2.5-computer-use-preview-*
73
+ /native[-_]?audio/i, // gemini-2.5-flash-native-audio-*
74
+ /-customtools(\b|-|$)/i, // gemini-3.1-pro-preview-customtools
75
+ /^codex-auto-review$/i, // Codex backend auto-review model, not a picker choice
76
+ /^grok-build$/i, // proxy alias duplicating grok-build-0.1 (exact — 0.1 stays)
67
77
  ];
68
78
 
69
79
  function _isNonLlmId(lid) {
@@ -89,6 +99,7 @@ const LEGACY_PATTERNS = [
89
99
  /^claude-(1|2|3|instant)(\b|-|\.)/i,
90
100
  // Gemini legacy — gemini-1*, gemini-pro. gemini-2/3 not matched.
91
101
  /^gemini-1(\b|-|\.)/i,
102
+ /^gemini-2\.0(\b|-|\.)/i,
92
103
  /^gemini-pro(\b|-)/i,
93
104
  // Grok legacy — grok-1/2/3 and grok-beta. The digit boundary stops grok-3
94
105
  // from catching grok-4.20-beta.