mixdog 0.9.34 → 0.9.35

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 (69) hide show
  1. package/README.md +6 -0
  2. package/package.json +4 -2
  3. package/scripts/_devtools-stub.mjs +1 -0
  4. package/scripts/_jitter-fuzz.jsx +42 -0
  5. package/scripts/_jitter-fuzz.mjs +44410 -0
  6. package/scripts/_jitter-fuzz2.jsx +30 -0
  7. package/scripts/_jitter-fuzz2.mjs +44400 -0
  8. package/scripts/_jitter-probe.jsx +35 -0
  9. package/scripts/_jitter-probe.mjs +44397 -0
  10. package/scripts/_jp2.jsx +16 -0
  11. package/scripts/_jp2.mjs +45614 -0
  12. package/scripts/agent-live-arg-guard-smoke.mjs +20 -0
  13. package/scripts/agent-live-path-suffix-smoke.mjs +34 -0
  14. package/scripts/agent-live-toolcall-args-smoke.mjs +45 -0
  15. package/scripts/apply-patch-edit-smoke.mjs +71 -0
  16. package/scripts/async-notify-settlement-test.mjs +99 -0
  17. package/scripts/forwarder-rebind-tail-test.mjs +67 -0
  18. package/scripts/live-worker-smoke.mjs +1 -1
  19. package/scripts/pending-completion-drop-test.mjs +46 -27
  20. package/scripts/provider-toolcall-test.mjs +1 -0
  21. package/scripts/recall-bench-cases.json +1 -0
  22. package/scripts/session-bench.mjs +44 -0
  23. package/scripts/shell-hardening-test.mjs +209 -0
  24. package/scripts/ship-mode-test.mjs +98 -0
  25. package/scripts/steering-drain-buckets-test.mjs +59 -0
  26. package/scripts/tool-smoke.mjs +46 -10
  27. package/scripts/worker-notify-rejection-test.mjs +51 -0
  28. package/src/lib/mixdog-debug.cjs +69 -0
  29. package/src/rules/agent/00-common.md +4 -5
  30. package/src/rules/agent/00-core.md +13 -20
  31. package/src/rules/agent/20-skip-protocol.md +3 -8
  32. package/src/rules/agent/30-explorer.md +31 -54
  33. package/src/rules/lead/01-general.md +3 -6
  34. package/src/rules/lead/lead-brief.md +10 -16
  35. package/src/rules/lead/lead-tool.md +3 -5
  36. package/src/rules/shared/01-tool.md +7 -9
  37. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +13 -2
  38. package/src/runtime/agent/orchestrator/providers/gemini.mjs +19 -23
  39. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +21 -5
  40. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +15 -0
  41. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +13 -13
  42. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +21 -7
  43. package/src/runtime/agent/orchestrator/tools/bash-policy-scan.mjs +1 -5
  44. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +71 -1
  45. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +17 -9
  46. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +127 -4
  47. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +105 -28
  48. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +2 -2
  49. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  50. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +7 -3
  51. package/src/runtime/memory/lib/memory-embed.mjs +1 -1
  52. package/src/runtime/memory/lib/query-handlers.mjs +25 -10
  53. package/src/runtime/memory/tool-defs.mjs +3 -3
  54. package/src/runtime/shared/background-tasks.mjs +21 -1
  55. package/src/runtime/shared/tool-execution-contract.mjs +29 -16
  56. package/src/session-runtime/model-route-api.mjs +3 -3
  57. package/src/session-runtime/provider-auth-api.mjs +13 -0
  58. package/src/session-runtime/runtime-core.mjs +40 -6
  59. package/src/session-runtime/tool-catalog.mjs +3 -2
  60. package/src/session-runtime/workflow-agents-api.mjs +2 -2
  61. package/src/standalone/agent-tool/notify.mjs +14 -1
  62. package/src/tui/dist/index.mjs +63 -25
  63. package/src/tui/engine/session-flow.mjs +26 -13
  64. package/src/tui/engine/turn.mjs +7 -8
  65. package/src/tui/engine.mjs +4 -2
  66. package/src/vendor/statusline/bin/statusline-route.mjs +60 -4
  67. package/src/vendor/statusline/src/gateway/route-meta.mjs +37 -8
  68. package/src/workflows/default/WORKFLOW.md +26 -32
  69. package/src/workflows/solo/WORKFLOW.md +12 -13
@@ -1,23 +1,16 @@
1
1
  # Agent Constraints
2
2
 
3
3
  - Use English for agent task communication.
4
- - ONE TURN = ONE BATCH: a read-only call that could have ridden the prior
5
- turn is a wasted round-trip.
6
- - NEVER PREAMBLE: no status/progress narration, "I will..." setup, or any text
7
- before tool calls call needed tools immediately; emit text only for the
8
- final handoff after tool work is done.
9
- - Final handoff: minimum characters, maximum information for Lead. Follow the
10
- role's stricter output contract if defined; else fragments outcome (1
11
- line), key `file:line`(s), verification result, material risks (only if
12
- any).
13
- - Never repeat what Lead already knows (the brief, the process, how you
14
- searched); state only what changed and where to look. Verification =
15
- command + result in one line. Same fact twice = delete one.
16
- - Handoff cap ~30 lines unless `Deliver:` raises it — a ceiling, not a target.
17
- - A blocked or partial report is a valid completion: state done/missing/
18
- blocker in fragments — never keep retrieving to avoid reporting.
19
- - Banned as pure cost: report headings, markdown tables (unless requested),
20
- prose narration, raw logs/tool traces, speculative next-checks, restated
21
- brief, articles/politeness.
22
- - Exception: a runtime wrap-up directive (exploration budget reached) overrides
23
- this — then summarize done/remaining/blocking as instructed.
4
+ - One turn = one batch; any read-only call that could have ridden the prior
5
+ turn is wasted.
6
+ - Never preamble/progress before tools; call tools immediately. Text only in
7
+ the final handoff after tool work.
8
+ - Final handoff: fragments only outcome, key `file:line`, verification
9
+ command+result, material risks/blockers. Follow stricter role contracts.
10
+ - Do not repeat the brief, process, search path, or any fact. Report done/
11
+ missing/blocker instead of retrieving to avoid reporting.
12
+ - Handoff cap ~30 lines unless `Deliver:` raises it; this is a ceiling.
13
+ - Ban headings, tables unless requested, prose narration, raw logs/tool traces,
14
+ speculative next-checks, restated brief, articles/politeness.
15
+ - Runtime wrap-up directives override this and may require done/remaining/
16
+ blocking summary.
@@ -1,10 +1,5 @@
1
1
  # Skip Protocol
2
2
 
3
- For inbound-event agent reports (`webhook-handler`, `scheduler-task`):
4
- if Lead has nothing actionable to relay
5
- (label-only, duplicate/dedup, no action needed, nothing to report),
6
- prefix the whole response with:
7
-
8
- ```
9
- [meta:silent]
10
- ```
3
+ For inbound-event reports (`webhook-handler`, `scheduler-task`) with nothing
4
+ actionable to relay (label-only, duplicate/dedup, no action needed/report),
5
+ prefix the whole response with `[meta:silent]`.
@@ -6,66 +6,43 @@ kind: retrieval
6
6
 
7
7
  # Role: explorer
8
8
 
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.
9
+ Coordinate locator. Deliver WHERE as `path:line`, never WHY. You ARE
10
+ `explore`; never call it.
12
11
 
13
- Tools: grep/find/glob/code_graph ONLY. `read` and `list` are FORBIDDEN
14
- grep match lines already carry every `path:line` you may output; opening a
15
- file to "confirm" or "understand" a hit is the WHY you must not answer.
16
- This ban has NO exception: not after a hit, not for the reason field, not
17
- "just one read" — a `read`/`list` call anywhere in the session is a defect.
12
+ Tools: grep/find/glob/code_graph ONLY. `read`/`list` are forbidden with no
13
+ exception; grep/code_graph lines already carry the `path:line` answer.
18
14
 
19
- Turn 1 is the whole search, ONE message: one grep whose `pattern[]` packs
20
- 3-6 token variants (camelCase/kebab-case/snake_case/SCREAMING_SNAKE casings,
21
- synonyms, library/domain names), plus `find` with name fragments, plus `code_graph`
22
- symbol_search when the query names an identifier. A single-pattern,
23
- single-tool first turn is a defect.
15
+ Turn 1 is the whole search in ONE message: grep `pattern[]` with 3-6 code-token
16
+ variants, find name fragments, and code_graph symbol_search for identifiers. A
17
+ single-pattern or single-tool first turn is a defect.
24
18
 
25
- A turn-1 broad grep MUST set `output_mode:"files_with_matches"` — an
26
- unscoped `content`/`content_with_context` scan reads every match body and is
27
- a defect (a full-content scan across the tree costs seconds). Use
28
- `content_with_context` ONLY against a `path` that appeared in an earlier
29
- result THIS session, and always with `head_limit`.
19
+ Broad grep must use `output_mode:"files_with_matches"`. Use
20
+ `content_with_context` only on a path returned earlier in THIS session and with
21
+ `head_limit`.
30
22
 
31
- Search tokens are CODE tokens: first translate natural-language or
32
- non-English queries into probable English identifiers (e.g. "최대 루프 반복
33
- 횟수" → maxLoop, loop-policy, iterations). Grep non-ASCII text only when
34
- the query quotes a literal string.
23
+ Translate natural/non-English queries to probable English identifiers first;
24
+ grep non-ASCII only for quoted literal strings.
35
25
 
36
- Search scope is ALWAYS the session working directory: omit `path` (default
37
- scope) or pass ONLY a path that appeared in an earlier result this session.
38
- Inventing a directory (`/workspace/...`, guessed `src/lib`, another repo's
39
- layout) is a defect — those results are always empty; on zero hits change
40
- TOKENS, never guess paths.
26
+ Scope = session working directory. Omit `path` or use only paths returned
27
+ earlier in this session. Never invent directories; after zero hits change
28
+ TOKENS/scope, not wording or guessed paths.
41
29
 
42
- Credibility is mechanical: hit = line/path contains ANY query token or
43
- obvious synonym — never judge "is this the real implementation". Sole
44
- exception: a generic-word-only match (schema, handler, config, resolver…)
45
- with no SPECIFIC query token (product/library/domain name) = zero, not a hit.
46
- A `code_graph` symbol hit (find_symbol/symbol_search returning `path:line`)
47
- IS an anchor — emit it directly; never re-locate it with grep.
30
+ Hit test is mechanical: any `path:line` containing a query token or obvious
31
+ synonym is an anchor; generic-only words (schema/handler/config/resolver…)
32
+ without a specific token are zero. code_graph `path:line` hits are anchors —
33
+ never re-locate them with grep.
48
34
 
49
- Rule zero after EVERY tool result: ≥1 `path:line` matching a query token
50
- answer NOW with those coordinates, mark weak anchors `?`; zero one more
51
- batch if budget remains. No third branch: "hits exist but I want better
52
- ones" IS branch one. Once ANY turn yields a matching line, the only legal
53
- next output is the final answer text — every further tool call is a defect.
35
+ Rule zero after every tool result: any matching anchor answer NOW (mark weak
36
+ anchors `?`); zero one more batch if budget remains. Further searching after
37
+ a matching anchor is a defect.
54
38
 
55
- Turns: hard max 3, expected 1; start each tool message with `turn N/3`.
56
- Turns 2-3 are miss-recovery only (prior turn had ZERO matching lines) and
57
- must change tokens OR scope, never reword.
58
- Flow/how questions: first matching entry/definition anchors ARE the answer,
59
- one anchor per concept — never trace the chain.
60
- Compound queries ("where is X and what value/default does it use"): the
61
- definition anchor answers BOTH parts — never launch extra searches for the
62
- value, threshold, or content; one anchor per concept, all from the same batch.
39
+ Turns: max 3, expected 1; start tool messages with `turn N/3`. Turns 2-3 are
40
+ miss recovery only and must change tokens or scope.
63
41
 
64
- Answer, nothing else: anchor lines `path:line symbol short reason`
65
- (`?` if weak), max 3 lines extra anchors are cost, not quality; pick the
66
- 3 with the most specific token match. Or `EXPLORATION_FAILED` — only after
67
- the turn budget is
68
- spent with zero anchors, or when every anchor matches only generic words.
69
- Before emitting `EXPLORATION_FAILED`, re-scan ALL earlier tool results: if
70
- any line anywhere matched a specific query token, answer with that anchor
71
- (`?` if weak) instead — a weak anchor beats a false miss.
42
+ Flow/how and compound queries: first matching entry/definition anchors answer
43
+ the concept/value/default; never trace chains or launch extra value searches.
44
+
45
+ Answer only: up to 3 lines `path:line — symbol — short reason` (`?` if weak),
46
+ choosing the most specific token matches. Emit `EXPLORATION_FAILED` only after
47
+ budget is spent with zero specific-token anchors; before failing, re-scan prior
48
+ results and prefer any weak specific-token anchor over a false miss.
@@ -1,11 +1,8 @@
1
1
  # General
2
2
 
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.
3
+ - Preambles are optional: one short sentence only when useful; no direct names,
4
+ honorifics, headings, labels, or routine lookup narration.
6
5
  - Destructive/hard-to-reverse actions require explicit confirmation.
7
6
  - Never push, build, or deploy without an explicit user request —
8
7
  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?").
8
+ - Handle what you can proactively; ask only for decisions, consultatively.
@@ -1,21 +1,15 @@
1
1
  # Lead Brief Contract
2
2
 
3
3
  - Brief = one-line fragments `Goal:` `Anchors:` `Allow/Forbid:` `Deliver:`
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.
12
- - `Deliver:` states output size/shape (e.g. "fragments <=15 lines", "verdict
13
- + top-3 risks", "detail to file, path only"); never request a long report
14
- in the handoff itself.
15
- - Full brief only on fresh spawn or `respawned: true` (dead-tag send = cold
16
- session; re-supply anchors); live-session follow-ups = delta only, never
17
- restating Goal/rules.
18
- - Never `send` mid-run; batch all adjustments into ONE follow-up after
19
- completion. Interrupt only to cancel.
4
+ `Verify:` (+`Stop:` heavy-worker). Minimum chars, maximum info: no
5
+ background/motivation, role-known rules, repeated context, or duplicate facts.
6
+ If it feels long, split scope instead of padding.
7
+ - Anchors = `file:line` + one-line conclusion; never paste log/code bodies.
8
+ - Instruct by outcome, not method, unless the method is the requirement.
9
+ - `Deliver:` states output size/shape; never request a long report in the handoff.
10
+ - Full brief only on fresh spawn or `respawned: true`; live-session follow-ups
11
+ are delta only. Dead-tag send = cold session, re-supply anchors.
12
+ - Never `send` mid-run; batch adjustments into ONE follow-up after completion.
13
+ Interrupt only to cancel.
20
14
  - All agent communication in English.
21
15
  - Referenced spec/test file beats its summary in the brief.
@@ -1,8 +1,6 @@
1
1
  # Lead Tool Use
2
2
 
3
- - Write-role agents run their own build/test verification via `shell`; Lead
4
- runs cross-scope verification, benches, and everything git via `shell`
5
- directly.
6
- - Use the session's current project/workspace. Change the work project only
7
- when the user asks for another project or a tool call needs another root.
3
+ - Write-role agents self-verify via `shell`; Lead runs cross-scope verification,
4
+ benches, and all git via `shell`.
5
+ - Use the current project/workspace; change it only on user request or tool need.
8
6
  - Use `agent` for scoped implementation, research, review, and debugging.
@@ -7,20 +7,18 @@
7
7
  solo call. Merge variants/scopes into ONE call wherever the schema takes
8
8
  arrays (`pattern[]`, `path[]`, `symbols[]`, `query[]`), chain shell with
9
9
  `;`/`&&` or run them in parallel, and put every known edit in ONE patch.
10
- - Route by what is already known: known symbol/relation → `code_graph`;
11
- exact text in a known scope → `grep`; unknown location, machine-wide/
12
- out-of-repo whereabouts, or concept-level question → `explore` (which uses
13
- the hardened `find` internally); name fragment → `find`; exact name pattern
14
- → `glob`; known directory → `list`; known file/region → `read`.
10
+ - Route by verified inputs: symbol/relation → `code_graph`; exact text in a
11
+ verified scope → `grep`; unknown/out-of-repo/machine-wide location or concept
12
+ `explore`; name fragment `find`; exact pattern → `glob`; verified dir →
13
+ `list`; verified file/span → `read`.
15
14
  - `explore` fan-out: at task start, decompose what the task needs to know
16
15
  into independent facets (implementation site, config/load path, tests,
17
16
  error origin, ...) and send them as ONE `query[]` call — facets run in
18
17
  parallel. Never fan out rephrasings of the same target; on
19
18
  EXPLORATION_FAILED, retry once with changed tokens.
20
- - Valid anchors come from user input or tool output in this session; locate
21
- anything else with `find` before `grep`/`read`. On ENOENT the next call is
22
- `find` on the basename never a retried guess; and never guess an absolute
23
- path outside the project — `find` from a verified broad root instead.
19
+ - Verified paths = user-provided or tool-returned. Guessed/unknown path
20
+ `find`/`glob`/`explore` first; ENOENT `find` basename, never retry a guess
21
+ or invent absolute paths outside the project.
24
22
  - Retrieval stops when evidence covers the deliverable: single-answer tasks
25
23
  end at the first sufficient anchor; enumeration tasks (review, audit) end
26
24
  when the stated scope is covered. Never re-verify a hit already on screen;
@@ -4,6 +4,7 @@ import { dirname, join } from 'path';
4
4
  import os from 'os';
5
5
  import { getPluginData } from './config.mjs';
6
6
  import { readServicePort, markServiceUnreachable, isConnRefuseError } from '../../shared/service-discovery.mjs';
7
+ import { isDiagnosticIOEnabled } from '../../../lib/mixdog-debug.cjs';
7
8
 
8
9
  const WARNED_KEYS = new Set();
9
10
 
@@ -94,8 +95,13 @@ function _rotateLocalTraceIfNeeded(path) {
94
95
  function _resolveLocalTracePath() {
95
96
  if (process.env.MIXDOG_AGENT_TRACE_LOCAL_DISABLE === '1') return null;
96
97
  if (_localTracePath) return _localTracePath;
98
+ const explicit = process.env.MIXDOG_AGENT_TRACE_PATH;
99
+ // Ship-mode default: skip diagnostic local trace file IO unless dev/debug,
100
+ // MIXDOG_DIAGNOSTICS, or an explicit trace path opts back in. In-memory
101
+ // buffer + memory-service flush (session metrics) are unaffected.
102
+ if (!explicit && !isDiagnosticIOEnabled()) return null;
97
103
  try {
98
- _localTracePath = process.env.MIXDOG_AGENT_TRACE_PATH
104
+ _localTracePath = explicit
99
105
  || join(getPluginData(), 'history', 'agent-trace.jsonl');
100
106
  // R4 data-at-rest: trace rows may carry tool payloads / prompts;
101
107
  // clamp dir to owner-only on POSIX (advisory on Windows).
@@ -332,8 +338,12 @@ function appendAgentTrace(record = {}) {
332
338
  function _resolveToolFailurePath() {
333
339
  if (process.env.MIXDOG_TOOL_FAILURE_LOG_DISABLE === '1') return null;
334
340
  if (_toolFailurePath) return _toolFailurePath;
341
+ const explicit = process.env.MIXDOG_TOOL_FAILURE_LOG_PATH;
342
+ // Ship-mode default: skip diagnostic tool-failure log file IO unless
343
+ // dev/debug, MIXDOG_DIAGNOSTICS, or an explicit path opts back in.
344
+ if (!explicit && !isDiagnosticIOEnabled()) return null;
335
345
  try {
336
- _toolFailurePath = process.env.MIXDOG_TOOL_FAILURE_LOG_PATH
346
+ _toolFailurePath = explicit
337
347
  || join(getPluginData(), 'history', 'tool-failures.jsonl');
338
348
  mkdirSync(dirname(_toolFailurePath), { recursive: true, mode: 0o700 });
339
349
  return _toolFailurePath;
@@ -422,6 +432,7 @@ export {
422
432
  drainAgentTrace,
423
433
  normalizeSessionId,
424
434
  warnAgentOnce,
435
+ _resolveLocalTracePath,
425
436
  _resolveToolFailurePath,
426
437
  _appendToolFailureRow,
427
438
  };
@@ -784,21 +784,29 @@ export class GeminiProvider {
784
784
  // exact value the trace already recorded (including the
785
785
  // cachedFallback when cachedContentTokenCount / total_cached_tokens
786
786
  // under-reports).
787
- let cachedTokens = 0;
787
+ let resolvedUsage = null;
788
788
  if (um) {
789
789
  const {
790
790
  inputTokens,
791
791
  reportedCachedTokens,
792
792
  cachedFallbackTokens,
793
- cachedTokens: resolvedCachedTokens,
793
+ cachedTokens,
794
794
  cacheTokenSource,
795
795
  } = _resolveGeminiCacheUsage({
796
796
  usageMetadata: um,
797
797
  cachedContent,
798
798
  providerState: opts.providerState,
799
799
  });
800
- cachedTokens = resolvedCachedTokens;
801
- const outputTokens = (um.candidatesTokenCount || 0) + (um.thoughtsTokenCount || 0);
800
+ const outputTokens = (um.candidatesTokenCount || um.candidates_token_count || 0)
801
+ + (um.thoughtsTokenCount || um.thoughts_token_count || 0);
802
+ resolvedUsage = {
803
+ inputTokens,
804
+ outputTokens,
805
+ cachedTokens,
806
+ // Gemini promptTokenCount is total (cachedContentTokenCount is
807
+ // a subset). Alias the resolver's normalized total directly.
808
+ promptTokens: inputTokens,
809
+ };
802
810
  if (cachedContent && inputTokens > 0 && cachedTokens <= 0) {
803
811
  try {
804
812
  appendAgentTrace({
@@ -820,11 +828,11 @@ export class GeminiProvider {
820
828
  traceAgentUsage({
821
829
  sessionId: opts.sessionId || opts.session?.id || null,
822
830
  iteration: Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null,
823
- inputTokens,
824
- outputTokens,
825
- cachedTokens,
831
+ inputTokens: resolvedUsage.inputTokens,
832
+ outputTokens: resolvedUsage.outputTokens,
833
+ cachedTokens: resolvedUsage.cachedTokens,
826
834
  cacheWriteTokens: 0,
827
- promptTokens: inputTokens,
835
+ promptTokens: resolvedUsage.promptTokens,
828
836
  model: useModel,
829
837
  modelDisplay: useModel,
830
838
  rawUsage: um,
@@ -837,21 +845,9 @@ export class GeminiProvider {
837
845
  toolCalls,
838
846
  citations: citations.length ? citations : undefined,
839
847
  providerState: opts.providerState,
840
- usage: um ? (() => {
841
- const input = um.promptTokenCount || um.totalTokenCount || 0;
842
- return {
843
- inputTokens: input,
844
- outputTokens: (um.candidatesTokenCount || 0) + (um.thoughtsTokenCount || 0),
845
- // Use the already-computed cachedTokens (with
846
- // cache-create fallback applied) rather than the raw
847
- // metadata field, so the returned usage matches what
848
- // traceAgentUsage recorded for this same call.
849
- cachedTokens,
850
- // Gemini promptTokenCount is total (cachedContentTokenCount
851
- // is a subset). Alias directly into promptTokens.
852
- promptTokens: input,
853
- };
854
- })() : undefined,
848
+ // Use the same normalized usage object traceAgentUsage recorded,
849
+ // including snake_case SDK aliases and cache-create fallback.
850
+ usage: resolvedUsage || undefined,
855
851
  };
856
852
  }
857
853
 
@@ -405,16 +405,32 @@ export function getModelMetadataSync(id, provider) {
405
405
  // id, and add fields LiteLLM lacks, such as opencode-go reasoning_options.
406
406
  if (mappedProvider) {
407
407
  const md = _modelsDevMetadataSync(id, provider);
408
- if (md) meta = mergeModelMetadata(meta, md, { preserveBaseCosts: metaFromPricingOverride });
408
+ if (md) meta = mergeModelMetadata(meta, md, {
409
+ preserveBaseCosts: metaFromPricingOverride,
410
+ preserveBaseLimits: metaFromPricingOverride,
411
+ });
409
412
  }
410
- if (providerUsesEndpointScopedLimits(provider) && !providerNative && meta) {
413
+ if (providerUsesEndpointScopedLimits(provider) && !providerNative && meta && !metaFromPricingOverride) {
411
414
  // OAuth/backend routes can expose smaller account/backend windows than
412
415
  // the public API SKU. External catalogs remain useful for costs and
413
416
  // capabilities, but their limits are not authoritative for these routes.
414
417
  meta = { ...meta, contextWindow: null, outputTokens: null };
415
418
  }
416
419
  if (providerNative) {
417
- meta = mergeModelMetadata(meta, providerNative, { preserveBaseCosts: true });
420
+ // Provider cache limits are only authoritative for endpoint-scoped
421
+ // routes (OAuth/backend), where the cached row reflects the live
422
+ // account/backend window. For every other provider the cache is a
423
+ // best-effort snapshot that can go stale, so it must NOT override the
424
+ // catalog/known limits — otherwise this function returns cache limits
425
+ // labelled as catalog data and downstream catalog-vs-cache staleness
426
+ // checks (context-meta, statusline route-meta) compare stale-vs-stale
427
+ // and can never correct an outdated cached window. Capabilities and
428
+ // gap-filling (base limit null) still flow through the merge.
429
+ const nativeLimitsAuthoritative = providerUsesEndpointScopedLimits(provider);
430
+ meta = mergeModelMetadata(meta, providerNative, {
431
+ preserveBaseCosts: true,
432
+ preserveBaseLimits: metaFromPricingOverride || !nativeLimitsAuthoritative,
433
+ });
418
434
  }
419
435
  return meta;
420
436
  }
@@ -444,8 +460,8 @@ function mergeModelMetadata(base, overlay, opts = {}) {
444
460
  if (!overlay) return base;
445
461
  return {
446
462
  ...base,
447
- contextWindow: overlay.contextWindow || base.contextWindow || null,
448
- outputTokens: overlay.outputTokens || base.outputTokens || null,
463
+ contextWindow: opts.preserveBaseLimits ? (base.contextWindow || overlay.contextWindow || null) : (overlay.contextWindow || base.contextWindow || null),
464
+ outputTokens: opts.preserveBaseLimits ? (base.outputTokens || overlay.outputTokens || null) : (overlay.outputTokens || base.outputTokens || null),
449
465
  // Provider-scoped models.dev rates beat generic base (LiteLLM) rates
450
466
  // when present — the overlay is the provider-scoped source, so a
451
467
  // non-null overlay value always wins over the generic fallback —
@@ -275,7 +275,22 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
275
275
  // so the statusline context gauge climbs as the turn accumulates
276
276
  // tool output instead of freezing at the pre-turn snapshot. Cleared
277
277
  // on turn commit (below) and in the ask finally.
278
+ //
279
+ // Also commit the user turn to the live session BEFORE the provider
280
+ // call. Previously the prompt only reached session.messages after
281
+ // agentLoop returned. If a worker/lead session was closed or aborted
282
+ // before first response, closeSession() wrote a tombstone from the
283
+ // still-system-only session and the handoff brief vanished forever
284
+ // (agent row showed messages=2). Pre-committing makes cancellation,
285
+ // close, and post-mortem files retain the exact user task; completion
286
+ // below overwrites this provisional transcript with the fully mutated
287
+ // outgoing history and appends the assistant result, so no duplicate
288
+ // user turn is introduced.
289
+ session.messages = sanitizeSessionMessagesForModel(outgoing);
278
290
  session.liveTurnMessages = outgoing;
291
+ saveSessionAsync(session, { expectedGeneration: askGeneration }).catch((err) => {
292
+ try { process.stderr.write(`[session] preflight user-turn save failed: ${err?.message || err}\n`); } catch {}
293
+ });
279
294
  // Per-turn injected-context trace row (complements kind:"usage").
280
295
  // Cheap byte-length accounting — no hashing, no payload bodies.
281
296
  // Honors the same MIXDOG_AGENT_TRACE_DISABLE gate as usage rows;
@@ -6,7 +6,6 @@ import {
6
6
  COMPACT_TYPE_SEMANTIC,
7
7
  COMPACT_TYPE_RECALL_FASTTRACK,
8
8
  CONTEXT_SHARE_RATIO,
9
- DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT,
10
9
  COMPACT_TARGET_MIN_TOKENS,
11
10
  } from '../compact.mjs';
12
11
  import { isAgentOwner } from '../../agent-owner.mjs';
@@ -116,13 +115,13 @@ function compactTargetTokensForBoundary(boundaryTokens) {
116
115
  return Math.max(1, Math.min(boundary, Math.max(minTarget, byRatio)));
117
116
  }
118
117
  function defaultEffectiveContextWindowPercent(provider) {
119
- // Gateway/statusline route metadata reserves a universal 10% headroom from
120
- // the raw catalog window. Keep session compaction on the same effective
121
- // capacity so /context, the TUI statusline, and gateway telemetry agree.
118
+ // The session boundary is the model's full raw window. Headroom is applied
119
+ // by resolveSessionCompactPolicy instead: agent semantic sessions compact at
120
+ // the buffered trigger (default 90%), while main/user recall-fasttrack
121
+ // sessions compact on the boundary itself (100%).
122
122
  void provider;
123
- return DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT;
123
+ return 100;
124
124
  }
125
- const PROVIDER_SYNTHETIC_CONTEXT_DEFAULT = 1_000_000;
126
125
  function providerRawContextWindow(info, catalogInfo) {
127
126
  if (!info || typeof info !== 'object') return null;
128
127
  const fromApiFields = positiveContextWindow(info.context_window)
@@ -130,17 +129,18 @@ function providerRawContextWindow(info, catalogInfo) {
130
129
  if (fromApiFields) return fromApiFields;
131
130
  const fromCache = positiveContextWindow(info.contextWindow)
132
131
  || positiveContextWindow(info.maxContextWindow);
133
- if (!fromCache) return null;
134
132
  const catalogWindow = positiveContextWindow(catalogInfo?.contextWindow)
135
133
  || positiveContextWindow(catalogInfo?.maxContextWindow)
136
134
  || positiveContextWindow(catalogInfo?.context_window)
137
135
  || positiveContextWindow(catalogInfo?.max_context_window);
138
- if (fromCache === PROVIDER_SYNTHETIC_CONTEXT_DEFAULT
139
- && catalogWindow
140
- && fromCache !== catalogWindow) {
141
- return null;
142
- }
143
- return fromCache;
136
+ // Catalog/known metadata is authoritative for models present in the
137
+ // catalog. A stale provider cache can hold an outdated window (e.g. Opus
138
+ // 4.8 cached at 272k after its window grew to the catalog's 1M, or a
139
+ // synthetic 1M placeholder for a smaller real model); whenever the catalog
140
+ // disagrees with the cached snapshot, trust the catalog value rather than
141
+ // the cache. Only live API fields (handled above) outrank the catalog.
142
+ if (catalogWindow && fromCache !== catalogWindow) return catalogWindow;
143
+ return fromCache || null;
144
144
  }
145
145
  export function resolveSessionContextMeta(provider, model, seed = {}) {
146
146
  const info = typeof provider?.getCachedModelInfo === 'function'
@@ -382,10 +382,18 @@ export function drainPendingMessages(sessionId) {
382
382
  // first. Reversing this order (buffer before disk) delivered newer
383
383
  // buffered sends ahead of older persisted ones after a restart.
384
384
  const persisted = [...drainPersistedPendingMessages(sessionId), ...takeBufferedPendingMessages(sessionId)];
385
- // Discard deferred completion/task notifications instead of injecting them
386
- // out-of-order into a future turn on resume. Genuine user/steering messages
387
- // (plain strings / content entries) carry no marker and are kept in order.
388
- const memoryKept = memory.filter((m) => !isCompletionNotificationEntry(m));
385
+ // Deferred completion/task notifications are dropped ONLY from the persisted
386
+ // (disk/buffer) path. Those are the entries a later session resume/restart
387
+ // would replay out-of-order into a future turn, once the in-memory queue is
388
+ // gone discarding them there is the deliberate owner decision.
389
+ // Live in-memory completions (fallback/headless enqueues delivered within
390
+ // THIS process, e.g. the idle-resume kick that surfaces the model-visible
391
+ // body) are the intended payload and MUST survive the drain — filtering
392
+ // them here dropped the notification entirely. On a genuine resume the
393
+ // in-memory queue is empty, so keeping it only ever delivers live entries.
394
+ // Genuine user/steering messages carry no marker and are kept in order in
395
+ // both paths.
396
+ const memoryKept = memory;
389
397
  const persistedKept = persisted.filter((m) => !isCompletionNotificationEntry(m));
390
398
  const memoryVisible = modelVisiblePendingMessages(memoryKept);
391
399
  const persistedVisible = modelVisiblePendingMessages(persistedKept);
@@ -394,9 +402,15 @@ export function drainPendingMessages(sessionId) {
394
402
  const persistedTexts = persistedVisible.map(pendingMessageText);
395
403
  const prefixMatches = memoryVisible.every((m, i) => persistedTexts[i] === pendingMessageText(m));
396
404
  if (prefixMatches) return [...memoryVisible, ...persistedVisible.slice(memoryVisible.length)];
397
- const out = persistedVisible.slice();
398
- const seen = new Set(persistedTexts);
399
- for (const m of memoryVisible) {
405
+ // The live in-memory queue holds the authoritative order — including the
406
+ // completion entries that were filtered out of the persisted path — so an
407
+ // interleaved [user, completion, user] must stay in that order and never be
408
+ // flattened to [user, user, completion] by rebuilding from persisted first.
409
+ // Start from the memory order and append only genuinely persisted-only
410
+ // entries; buffered duplicates of live sends are skipped by text.
411
+ const out = memoryVisible.slice();
412
+ const seen = new Set(out.map(pendingMessageText).filter(Boolean));
413
+ for (const m of persistedVisible) {
400
414
  const text = pendingMessageText(m);
401
415
  if (!text || seen.has(text)) continue;
402
416
  out.push(m);
@@ -41,21 +41,17 @@ export function injectionBlockTargets(cmd) {
41
41
  return targets;
42
42
  }
43
43
 
44
- const _unquoteSpansForPolicy = (s) => s.replace(/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/g, (m) => m.slice(1, -1));
45
-
46
44
  export function buildBashPolicyScanTargets(command) {
47
45
  const cmd = String(command || '');
48
46
  if (!cmd) return [];
49
47
  const targets = [
50
48
  cmd,
51
49
  stripQuotedAndHeredoc(cmd),
52
- _unquoteSpansForPolicy(cmd),
53
50
  ...extractShellCInner(cmd).map(stripQuotedAndHeredoc),
54
- ...extractShellCInner(cmd).map(_unquoteSpansForPolicy),
55
51
  ...injectionBlockTargets(cmd),
56
52
  ];
57
53
  for (const inner of extractPowerShellCommandInner(cmd)) {
58
- targets.push(inner, stripQuotedAndHeredoc(inner), _unquoteSpansForPolicy(inner));
54
+ targets.push(inner, stripQuotedAndHeredoc(inner));
59
55
  }
60
56
  const psDecoded = decodePowerShellEncodedCommand(cmd);
61
57
  if (psDecoded) targets.push(psDecoded);