mixdog 0.9.175 → 1.0.1

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 (70) hide show
  1. package/package.json +1 -1
  2. package/scripts/context-renewal-live-check.mjs +2 -1
  3. package/scripts/plan-runtime-release-tag.mjs +57 -0
  4. package/src/defaults/skills/computer-use/SKILL.md +13 -12
  5. package/src/defaults/skills/setup/SKILL.md +1 -4
  6. package/src/defaults/skills/setup/references/actions.md +0 -2
  7. package/src/defaults/skills/setup/references/surfaces.md +2 -3
  8. package/src/defaults/skills/xlsx/SKILL.md +1 -0
  9. package/src/rules/shared/70-delivery.md +3 -0
  10. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -12
  11. package/src/runtime/agent/orchestrator/config.mjs +8 -1
  12. package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +6 -1
  13. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +2 -0
  14. package/src/runtime/agent/orchestrator/providers/cursor.mjs +20 -2
  15. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +3 -0
  16. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -0
  17. package/src/runtime/agent/orchestrator/providers/registry-errors.mjs +9 -0
  18. package/src/runtime/agent/orchestrator/providers/registry.mjs +3 -2
  19. package/src/runtime/agent/orchestrator/runtime-core/goal-tasks.mjs +25 -6
  20. package/src/runtime/agent/orchestrator/runtime-core/goal-text.mjs +3 -2
  21. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +1 -1
  22. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -0
  23. package/src/runtime/agent/orchestrator/session/result-classification.mjs +1 -1
  24. package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +7 -3
  25. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +27 -4
  26. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/benign-exit.mjs +7 -3
  27. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -0
  28. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +39 -3
  29. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +3 -1
  30. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
  31. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +13 -5
  32. package/src/runtime/computer-bridge/error-recovery.mjs +1 -1
  33. package/src/runtime/memory/lib/runtime-fetcher.mjs +94 -23
  34. package/src/runtime/office/capabilities-catalog.mjs +2 -0
  35. package/src/runtime/office/capabilities-signatures.mjs +12 -6
  36. package/src/runtime/office/capabilities.mjs +30 -10
  37. package/src/runtime/office/core/office-actions-open.mjs +1 -6
  38. package/src/runtime/office/core/office-sessions.mjs +9 -0
  39. package/src/runtime/office/portable/portable-xlsx-operations.mjs +2 -49
  40. package/src/runtime/office/portable/xlsx-contract.mjs +37 -3
  41. package/src/runtime/office/tool-defs.mjs +2 -2
  42. package/src/runtime/shared/config-patch.mjs +2 -0
  43. package/src/runtime/shared/llm/cost.mjs +16 -4
  44. package/src/runtime/shared/llm/model-catalog-projection.mjs +3 -2
  45. package/src/runtime/shared/llm/model-catalog.mjs +48 -0
  46. package/src/runtime/shared/llm/model-pricing-rates.mjs +11 -4
  47. package/src/runtime/shared/llm/usage-accounting.mjs +12 -11
  48. package/src/runtime/shared/llm/usage-context.mjs +7 -0
  49. package/src/runtime/shared/llm/usage-ledger-repair.mjs +3 -1
  50. package/src/runtime/shared/llm/usage-ledger.mjs +1 -1
  51. package/src/runtime/shared/tool-card-model/card-status.mjs +7 -5
  52. package/src/runtime/shared/tool-card-model/git-result.mjs +10 -0
  53. package/src/runtime/shared/tool-card-model/terminal-status.mjs +6 -2
  54. package/src/runtime/shared/tool-card-model.golden-cases.mjs +2 -1
  55. package/src/runtime/shared/tool-result-summary.mjs +1 -0
  56. package/src/runtime/shared/tool-status.mjs +35 -2
  57. package/src/runtime/shared/user-data-guard.mjs +1 -1
  58. package/src/runtime/tidy/report.mjs +2 -2
  59. package/src/runtime/tidy/tool.mjs +6 -1
  60. package/src/session-runtime/config-lifecycle.mjs +11 -1
  61. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +1 -1
  62. package/src/session-runtime/native-web-search/tool-shape.mjs +16 -4
  63. package/src/session-runtime/provider-models/catalog-load.mjs +18 -3
  64. package/src/session-runtime/setup-tool/executor.mjs +0 -1
  65. package/src/session-runtime/setup-tool/settings-contract.mjs +2 -13
  66. package/src/tui/dist/index.mjs +89 -38
  67. package/src/tui/session/session-api/settings.mjs +16 -10
  68. package/src/vendor/statusline/src/gateway/route-meta.mjs +7 -2
  69. package/src/workflows/default/WORKFLOW.md +0 -2
  70. package/src/workflows/headless/WORKFLOW.md +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.175",
3
+ "version": "1.0.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -125,7 +125,8 @@ try {
125
125
  for (const spec of specs) {
126
126
  const dataDir = path.join(sandbox, spec.name);
127
127
  const db = path.join(dataDir, 'pgdata');
128
- const ctl = path.join(dataDir, 'runtime', 'runtime-pg16.4+pgvector-0.8.2', 'bin', 'pg_ctl.exe');
128
+ const runtimeKey = fs.readFileSync(path.join(dataDir, 'runtime', 'active-version'), 'utf8').trim();
129
+ const ctl = path.join(dataDir, 'runtime', `runtime-${runtimeKey}`, 'bin', 'pg_ctl.exe');
129
130
  if (process.platform === 'win32' && fs.existsSync(ctl) && fs.existsSync(path.join(db, 'postmaster.pid'))) {
130
131
  const stopped = spawnSync(ctl, ['-D', db, '-m', 'fast', '-w', 'stop'], { stdio: 'inherit' });
131
132
  if (stopped.status !== 0) cleanupFailed = true;
@@ -0,0 +1,57 @@
1
+ // Decides the release tag a memory runtime build publishes under.
2
+ //
3
+ // Published runtime assets are immutable: every app that shipped a manifest
4
+ // naming a tag verifies that tag's assets by size and sha256, so replacing an
5
+ // asset under a shipped tag breaks the next runtime install of all of those
6
+ // apps. A build that would replace an asset of a shipped tag (any tag at or
7
+ // below the bundled manifest's release_tag) moves to the next patch tag; the
8
+ // manifest sync then points new app builds at it.
9
+ //
10
+ // CLI: node scripts/plan-runtime-release-tag.mjs <requested-tag> <shipped-tag> <needed-assets-file> <published-assets-file>
11
+ // prints the tag to build. The asset files hold one asset name per line.
12
+ import { readFileSync } from 'node:fs';
13
+ import { pathToFileURL } from 'node:url';
14
+
15
+ const TAG_RE = /^runtime-v(\d+)\.(\d+)\.(\d+)$/;
16
+
17
+ function parseTag(tag) {
18
+ const match = TAG_RE.exec(String(tag ?? '').trim());
19
+ if (!match) throw new Error(`invalid runtime release tag: ${tag}`);
20
+ return match.slice(1).map(Number);
21
+ }
22
+
23
+ function compareTags(left, right) {
24
+ const a = parseTag(left);
25
+ const b = parseTag(right);
26
+ for (let index = 0; index < 3; index += 1) {
27
+ if (a[index] !== b[index]) return a[index] - b[index];
28
+ }
29
+ return 0;
30
+ }
31
+
32
+ export function planRuntimeReleaseTag({ requestedTag, shippedTag, neededAssets, publishedAssets }) {
33
+ const published = new Set(publishedAssets);
34
+ const replacesPublished = neededAssets.some((name) => published.has(name));
35
+ if (!replacesPublished || compareTags(requestedTag, shippedTag) > 0) return requestedTag;
36
+ const [major, minor, patch] = parseTag(shippedTag);
37
+ return `runtime-v${major}.${minor}.${patch + 1}`;
38
+ }
39
+
40
+ function readNames(path) {
41
+ return readFileSync(path, 'utf8')
42
+ .split(/\r?\n/)
43
+ .map((line) => line.trim())
44
+ .filter(Boolean);
45
+ }
46
+
47
+ if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
48
+ const [requestedTag, shippedTag, neededPath, publishedPath] = process.argv.slice(2);
49
+ process.stdout.write(
50
+ `${planRuntimeReleaseTag({
51
+ requestedTag,
52
+ shippedTag,
53
+ neededAssets: readNames(neededPath),
54
+ publishedAssets: readNames(publishedPath),
55
+ })}\n`
56
+ );
57
+ }
@@ -115,18 +115,19 @@ GNOME with the Window Calls extension); prefer accessibility refs there.
115
115
  of the shared desktop. If that surface is unavailable, use semantic refs or
116
116
  report `pixel_unavailable`; do not substitute a screen grab.
117
117
  - If the user intervenes, preserve their cursor and focus. Worker termination
118
- and input cleanup must finish before resuming; a failed cleanup is not cleared
119
- by a resume request. Ask the user to press `Ctrl+Alt+Esc`, or the overlay's
120
- Stop control, for verified cleanup recovery.
121
- If target-local release remains unconfirmed, the user must inspect/recover
122
- that window; restarting the host alone does not prove release. Obtain a new
123
- observation after an interruption.
124
- Use `wait_for_user` to keep the task waiting without sending input. The host
125
- may resume ordinary physical-input interruptions after its configured quiet
126
- interval; renewed input resets that interval. Explicit pauses/stops and
127
- uncertain observation/cleanup never auto-resume. The overlay's only control
128
- is Stop, which cancels the task. Never operate it or the recovery controls,
129
- or change the idle policy, on the user's behalf.
118
+ and input cleanup must finish before resuming. The user's Resume on the
119
+ overlay continues a pause; an ordinary physical-input pause may also resume
120
+ after the host's configured quiet interval (renewed input resets it), while
121
+ explicit pauses, stops and uncertain cleanup never auto-resume. Stop (or
122
+ `Ctrl+Alt+Esc`) cancels the task, which then reports the stop as a failure.
123
+ A cleanup that could not be confirmed is retried before the next input
124
+ command; if it still fails, that command returns the reason — relay it to
125
+ the user. If target-local release remains unconfirmed, the user must
126
+ inspect/recover that window; restarting the host alone does not prove
127
+ release. Obtain a new observation after an interruption.
128
+ Use `wait_for_user` to keep the task waiting without sending input. Never
129
+ operate Resume, Stop or the recovery controls, or change the idle policy, on
130
+ the user's behalf.
130
131
  While paused, only `list`, `diagnose` and `wait_for_user` are available.
131
132
  Chat text alone does not clear the host. After `resumed`, capture fresh state;
132
133
  after `timeout` or `cancelled`, no input is authorized. Never replay the
@@ -25,7 +25,7 @@ supported `setup` action, and read its result as the change receipt.
25
25
  arguments and the transcript. Open the Providers surface and let the user
26
26
  enter them.
27
27
  - A mutation follows the active workflow's approval rules. Status, catalog,
28
- definition, MCP-config and Instructions reads do not change persisted settings.
28
+ definition and MCP-config reads do not change persisted settings.
29
29
  - Most changes affect new sessions. Report `appliedToCurrentSession` and
30
30
  `appliesTo` exactly when the result supplies them.
31
31
  - This skill does not own source edits, builds, installation deployment,
@@ -78,9 +78,6 @@ that a call returned without transport error.
78
78
  - Read definitions before editing them; use skill-creator when authoring a
79
79
  skill. Setup persists the supported definition fields. Workflow and agent
80
80
  ids are stable identities, distinct from their display names.
81
- - Read Instructions before replacing them and pass the returned text as
82
- `expectedContent`. `projectPath:null` means Common Instructions; otherwise
83
- use an exact registered Project root. Keep the returned backup.
84
81
  - MCP servers and plugins are machine-global integrations whose visibility may
85
82
  be scoped to selected Projects.
86
83
  - User skills are machine-global. Enabled plugin skills and built-in skills are
@@ -68,8 +68,6 @@ Read this when choosing a `setup` mutation.
68
68
  | Desktop theme, display language, side panels, or zoom | `set_appearance` |
69
69
  | Register a Project and optionally set its display alias | `save_project` |
70
70
  | Unregister a Project without deleting its files | `remove_project` |
71
- | Read Project/Common Instructions before editing | `get_instructions` |
72
- | Replace unchanged Project/Common Instructions and retain a backup | `set_instructions` |
73
71
  | Revoke one linked device; re-pairing restores access | `revoke_linked_device` |
74
72
  | Managed local model context allocation; null restores automatic | `set_local_context` |
75
73
 
@@ -38,8 +38,7 @@ An attached Desktop or TUI may navigate and return `opened:true`.
38
38
  - Extensions → Plugin → Git & GitHub: Git/GitHub CLI installation and connection.
39
39
  - Settings → Connection: web-app pairing and linked devices.
40
40
  - Settings → System: update, keep-awake, and Doctor.
41
- - Projects: Project registration, name, Instructions, Common Instructions, and
42
- Core Memories.
41
+ - Projects: Project registration, name, Project Memories, and Common Memory.
43
42
  - Workflows: workflow packs, Main and Web Search defaults, and agent editing.
44
43
  - Extensions → Plugin: Built-in cards and plugins.
45
44
  - Extensions → Skill: skills and MCP servers.
@@ -51,7 +50,7 @@ the user to Desktop.
51
50
 
52
51
  Setup can read and change Desktop appearance, keep-awake, usage pin,
53
52
  Computer observe-only, Browser/Computer/voice installation and toggles,
54
- Projects, Instructions, and linked-device revocation through the attached
53
+ Projects, and linked-device revocation through the attached
55
54
  Desktop. A receipt identifies `scope:desktop-host`; appearance is not applied
56
55
  to the paired browser. No Desktop claimant means no confirmed change.
57
56
 
@@ -37,6 +37,7 @@ Excel proves the numbers and makes their meaning readable. Keep sourced inputs a
37
37
  1. Settle the table first: headers, one row per record, consistent units, numeric cells as numbers (not text), dates as ISO strings (both backends store `2026-09-30` as the Excel date under `yyyy-mm-dd`, so date arithmetic is `=D5-DATE(2026,9,22)`, not `DATEVALUE`), percentages as fractions (`0.15`, formatted `0.0%`). A figure written as text is stored as the number Excel types from it, on both backends — `"1,234"` under `#,##0`, `"15%"` as 0.15 under `0%` — and any other text stays exactly as written (`"1-2"`, `"007"`, `"1e3"`, `"TRUE"` never become a date, a 7, a 1000, or a truth value). Keep the same `design.content` model as the deck or document in the same package.
38
38
  2. For a report, design the reading order, sheet roles, column proportions, chart placement and intended screen/print use. Author with native cell, style, merge and chart operations by default. `compose_sheet` is an opt-in preset only when its table-plus-panel structure already fits; do not select it merely to save calls. Batch all known operations. Use `describe format:'xlsx' operation:<op>` when fields are unknown; cell styles belong in `properties`.
39
39
  3. Add sheets with `add_sheet`, then native ranges and formulas targeting `sheet`. Use `set_formula` for derived cells and `define_name` for reused inputs. Check representative formulas against the intended ranges before extending a model; clean recalculation proves evaluation, not correct modelling. A design specimen is optional, not required for every workbook.
40
+ If `create` fails, stop dependent `open`, `render`, and `finalize` calls: a failed creation is not an artifact. Correct the reported inputs and resume only from a successful `create` result.
40
41
  4. Raw tabular files (`.csv`, `.tsv`) take `set_range` with a 2D `values` array and `append_row`; they carry no styles, formulas, or multiple sheets.
41
42
  5. `finalize` recalculates and returns current rendered pages for review; add `auditProfile:'financial-model'` for a decision model. Read the actual images as in `report-design.md`, with concrete keep/fix observations about hierarchy, charts and print layout. Correct material issues and rerender, rather than filling a pass checklist. Complete with `design:{ reviewed:true, reviewToken:<current token>, critique:[{ page:1, verdict:'pass', note:<page-specific observations> }, ...] }`. This records agent review, not user approval. CSV/TSV needs no visual approval. Resolve validation failures against their original baseline, never by reopening as a new baseline.
42
43
 
@@ -7,6 +7,9 @@
7
7
  absolute path, optionally `:line[:column]` (`deliverables/report.docx`,
8
8
  `src/app.ts:42`); never a bare name, a path relative to another folder, or a
9
9
  `file://` URI.
10
+ - Deliver requested visual artifacts (SVG, diagrams, HTML pages) as saved files
11
+ and give their path; never paste their source as the answer unless the user
12
+ asks for the code.
10
13
 
11
14
  <!-- tools: git -->
12
15
  - A commit request includes selecting and staging its changes; a stage-only
@@ -4,6 +4,7 @@ import { countJsonNextCalls } from './tools/next-call-utils.mjs';
4
4
  import { parseGrepContextHeader, splitGrepLinePrefix } from './tools/builtin/grep-formatting.mjs';
5
5
  import { isReadOnlyNavigationMiss } from './session/result-classification.mjs';
6
6
  import { nonNegativeInt } from '../../shared/numbers.mjs';
7
+ import { isUserControlCancellation } from '../../shared/tool-status.mjs';
7
8
  import {
8
9
  appendAgentTrace,
9
10
  normalizeSessionId,
@@ -128,7 +129,7 @@ function traceAgentCompact({
128
129
  }
129
130
 
130
131
  const TOOL_ARG_KEYS = {
131
- read: ['path', 'offset', 'limit', 'line', 'context'],
132
+ read: ['file_path', 'path', 'offset', 'limit', 'line', 'context'],
132
133
  grep: ['pattern', 'path', 'glob', 'output_mode', 'head_limit', 'offset'],
133
134
  glob: ['pattern', 'path', 'head_limit', 'offset', 'sort'],
134
135
  find: ['query', 'path', 'head_limit'],
@@ -211,12 +212,11 @@ function summarizeToolArgs(toolName, args) {
211
212
  for (const key of keys) {
212
213
  if (Object.hasOwn(args, key)) out[key] = compactTraceArgValue(args[key], key);
213
214
  }
214
- for (const countKey of ['edits', 'writes']) {
215
+ // Arrays are clipped to 6 entries above; the count keeps batch-limit
216
+ // failures (more than 10 read/grep targets) diagnosable.
217
+ for (const countKey of ['edits', 'writes', 'file_path', 'path']) {
215
218
  if (Array.isArray(args[countKey])) out[`${countKey}_count`] = args[countKey].length;
216
219
  }
217
- if (toolName === 'read' && Array.isArray(args.path)) {
218
- out.path_count = args.path.length;
219
- }
220
220
  return Object.keys(out).length ? out : null;
221
221
  }
222
222
 
@@ -410,21 +410,21 @@ function classifyToolFailure(resultText, toolName) {
410
410
  if (/\[signal:\s*[^\]]+/i.test(leading)) return 'process/signal';
411
411
  return 'command-exit';
412
412
  }
413
+ // edit quotes the file around a miss (the nearest line and a current-file
414
+ // excerpt). That quoted text is file content, so a word in it such as
415
+ // "unreadable", "timeout" or "permission" must not pick the category.
416
+ if (toolName === 'edit' && /^edit failed \(old_string (?:not found|found \d+ times)\b/i.test(leading))
417
+ return 'runtime/failure';
413
418
  if (/compacted-history placeholder/.test(text)) return 'expected-preflight';
414
419
  if (/\[tool-input-validation\]/.test(text)) return 'schema/args';
415
420
  // These are emitted validation/cancellation contracts, not keywords found
416
421
  // somewhere in a quoted document, command output, or error preview.
417
- if (
418
- /^(?:Browser command interrupted by local user input\.|computer_session_aborted: queued command was cancelled before execution|computer_user_control_active:)/i.test(
419
- leading
420
- )
421
- )
422
- return 'expected-cancellation';
422
+ if (isUserControlCancellation(leading)) return 'expected-cancellation';
423
423
  if (
424
424
  /^The arguments provided to `[^`]+` are invalid JSON and could not be parsed:/i.test(leading) ||
425
425
  /^new Goal tasks must omit ids; use updates for existing tasks$/i.test(leading) ||
426
426
  /^goal task exceeds \d+ characters$/i.test(leading) ||
427
- /^goal (?:tasks|updates) support at most \d+ entries$/i.test(leading) ||
427
+ /^goal (?:tasks|updates) support at most \d+ entries(?:$|;)/i.test(leading) ||
428
428
  /^goal arguments contain unknown fields:/i.test(leading) ||
429
429
  /^Unsupported field for GitHub [\w.]+\.$/i.test(leading) ||
430
430
  /^browser action "[^"]+" (?:does not accept input field|requires input\.|accepts only one input target form|fields must be inside input:|input\.[^\n]*(?:must be|requires|accepts at most))/i.test(
@@ -100,7 +100,9 @@ export function buildDefaultConfig(options = {}) {
100
100
  providers['mixdog-local'] = { enabled: false };
101
101
  return {
102
102
  providers,
103
- disabledAgents: [...DEFAULT_DISABLED_AGENT_IDS],
103
+ // The Maintainer also starts off: background upkeep spends model calls, so
104
+ // it runs only once the user turns it on and picks its model.
105
+ disabledAgents: [...DEFAULT_DISABLED_AGENT_IDS, 'maintainer'],
104
106
  workflow: { active: 'default' },
105
107
  orchestrationMode: 'none',
106
108
  };
@@ -272,6 +274,11 @@ export function loadConfig(options = {}) {
272
274
  recap: { enabled: true },
273
275
  developer: {},
274
276
  modules: {},
277
+ // No stored section at all is a brand-new profile: its built-ins start
278
+ // uninstalled. The defaults above carry presets/recap/modules, which
279
+ // would otherwise read as a pre-section profile and be grandfathered.
280
+ // A stored section that failed to parse proves nothing and keeps that path.
281
+ ...(hasKeys(sectionRaw) ? {} : { builtins: {} }),
275
282
  };
276
283
  }
277
284
  /** In-lock patch of `skills.disabled` only (avoids whole-config lost-update). */
@@ -21,6 +21,7 @@ import {
21
21
  } from './retry-classifier.mjs';
22
22
  import { PROVIDER_FIRST_BYTE_TIMEOUT_MS, createTimeoutSignal } from '../stall-policy.mjs';
23
23
  import { noteFastModeCapacityError } from './anthropic-fast-mode.mjs';
24
+ import { noteRequestServiceTier } from '../../../shared/llm/usage-context.mjs';
24
25
  import { notifyCurrentAnthropicRateLimit } from './admission-scheduler.mjs';
25
26
 
26
27
  /**
@@ -103,6 +104,7 @@ export function createAnthropicApiTransport({ client, label, opts, useModel, par
103
104
  const requestStreamingResponse = () =>
104
105
  withRetry(
105
106
  async ({ signal: attemptSignal }) => {
107
+ noteRequestServiceTier(params.speed);
106
108
  const res = await client.messages.create(params, requestOptions(attemptSignal)).asResponse();
107
109
  if (!res.ok) {
108
110
  const text = await res.text().catch(() => '');
@@ -156,7 +158,10 @@ export function createAnthropicApiTransport({ client, label, opts, useModel, par
156
158
 
157
159
  const requestNonStreamingMessage = (nonStreamingParams, signal) =>
158
160
  withRetry(
159
- async ({ signal: attemptSignal }) => client.messages.create(nonStreamingParams, requestOptions(attemptSignal)),
161
+ async ({ signal: attemptSignal }) => {
162
+ noteRequestServiceTier(nonStreamingParams.speed);
163
+ return client.messages.create(nonStreamingParams, requestOptions(attemptSignal));
164
+ },
160
165
  {
161
166
  ...retryPolicy(signal, `${label} Anthropic non-streaming fallback`, `${label}-nonstreaming-request`),
162
167
  onRetry: ({ attempt, maxAttempts, lastErr, delayMs }) =>
@@ -12,6 +12,7 @@ import { traceAgentFetch } from '../agent-trace.mjs';
12
12
  import { createAbortController } from '../../../shared/abort-controller.mjs';
13
13
  import { notifyCurrentAnthropicRateLimit } from './admission-scheduler.mjs';
14
14
  import { noteFastModeCapacityError } from './anthropic-fast-mode.mjs';
15
+ import { noteRequestServiceTier } from '../../../shared/llm/usage-context.mjs';
15
16
  import {
16
17
  ANTHROPIC_RETRY_BACKOFF_MS,
17
18
  ANTHROPIC_RETRY_JITTER_RATIO,
@@ -66,6 +67,7 @@ export function createAnthropicOAuthRequest({
66
67
  // provider-visible cache breakpoint off the cached one — the
67
68
  // exact COLD-turn bug this change fixes. Order is fixed:
68
69
  // build → sanitize (once) → mark → prepare image bytes → JSON.stringify.
70
+ noteRequestServiceTier(requestBody?.speed);
69
71
  const response = await postMessages({
70
72
  accessToken,
71
73
  requestBody,
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { getAgentApiKey } from '../../../shared/config.mjs';
3
+ import { noteRequestServiceTier } from '../../../shared/llm/usage-context.mjs';
3
4
  import {
4
5
  knownToolNamesFromOpenAITools,
5
6
  parseToolCalls,
@@ -510,6 +511,15 @@ function selectParameterizedCursorVariant(group, requested, sendOpts = {}) {
510
511
  };
511
512
  }
512
513
 
514
+ // Fast is a RequestedModel parameter on parameterized models and an id
515
+ // suffix on raw variants.
516
+ function cursorSelectionIsFast(selection) {
517
+ if (selection.parameters?.length) {
518
+ return selection.parameters.some((parameter) => parameter.id === 'fast' && parameter.value === 'true');
519
+ }
520
+ return parseCursorVariantId(selection.modelId).fast;
521
+ }
522
+
513
523
  function selectCursorVariant(group, { effort = null, fast = false } = {}) {
514
524
  if (!Array.isArray(group) || group.length === 0) return null;
515
525
  const normalizedEffort =
@@ -680,11 +690,13 @@ class CursorProviderBase {
680
690
  sendOpts.sessionId || sendOpts.providerCacheKey || sendOpts.promptCacheKey || `cursor-call:${randomUUID()}`
681
691
  );
682
692
  const openAiTools = tools?.length ? toOpenAITools(tools) : undefined;
683
- const dispatch = () =>
684
- this._dispatchChat(
693
+ const dispatch = () => {
694
+ noteRequestServiceTier(cursorSelectionIsFast(cursorSelection) ? 'fast' : '');
695
+ return this._dispatchChat(
685
696
  this._chatBody(messages, { cursorSelection, sessionScope, openAiTools, toolChoice: sendOpts.toolChoice }),
686
697
  { runtime, accessToken, signal, sendOpts, openAiTools }
687
698
  );
699
+ };
688
700
  let assembled;
689
701
  try {
690
702
  assembled = await dispatch();
@@ -702,9 +714,15 @@ class CursorProviderBase {
702
714
  assembled = await dispatch();
703
715
  }
704
716
  const rawUsage = assembled.rawUsage;
717
+ // A pinned raw variant (effort/Fast suffix) bills as its catalog model;
718
+ // the Fast part is carried by the request's tier, not the id.
719
+ const pinned = parseCursorVariantId(canonicalCursorModelId(String(model || '').trim()));
720
+ const pricingModel =
721
+ pinned.baseId !== pinned.id && this._cursorCatalog?.groups?.has(pinned.baseId) ? pinned.baseId : undefined;
705
722
  return {
706
723
  content: assembled.content || '',
707
724
  model: model || assembled.model || 'auto',
725
+ ...(pricingModel ? { pricingModel } : {}),
708
726
  toolCalls: assembled.toolCalls,
709
727
  stopReason: assembled.stopReason,
710
728
  ...(assembled.reasoningContent ? { reasoningContent: assembled.reasoningContent } : {}),
@@ -39,6 +39,7 @@ import {
39
39
  resolveProviderCacheKey,
40
40
  } from '../agent-runtime/cache-strategy.mjs';
41
41
  import { preconnect } from '../../../shared/llm/http-agent.mjs';
42
+ import { noteRequestServiceTier } from '../../../shared/llm/usage-context.mjs';
42
43
  import { sendViaHttpSse, _envFlag } from './openai-oauth-http-sse.mjs';
43
44
  import { warmCodexClientVersion } from './codex-client-meta.mjs';
44
45
  import { CODEX_BACKEND_ORIGIN } from './openai-codex-endpoints.mjs';
@@ -209,6 +210,8 @@ export class OpenAIOAuthProvider {
209
210
  const authState = { tokens: await _authP };
210
211
  await _verP;
211
212
  const body = await _bodyP;
213
+ // Every WS/HTTP attempt of this send carries this body's tier unchanged.
214
+ noteRequestServiceTier(body.service_tier);
212
215
  // poolKey != cacheKey by design. poolKey isolates socket/delta state per
213
216
  // session. cacheKey is body.prompt_cache_key and affects prompt-cache
214
217
  // routing only; Codex handshake identity comes from sendOpts and remains
@@ -23,6 +23,7 @@ import { applyOpenAIDirectCachePolicy, openAiDirectSupportsFast } from './openai
23
23
  import { streamCallbacks } from './lib/send-callbacks.mjs';
24
24
  import { getAgentApiKey } from '../../../shared/provider-api-key.mjs';
25
25
  import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
26
+ import { noteRequestServiceTier } from '../../../shared/llm/usage-context.mjs';
26
27
  import { resolveProviderCacheKey, resolveProviderPromptCacheLane } from '../agent-runtime/cache-strategy.mjs';
27
28
 
28
29
  function applyOpenAIDirectFastTier(body, model, opts) {
@@ -122,6 +123,7 @@ export class OpenAIDirectProvider {
122
123
  // so a model can opt into Fast even when the OAuth catalog does not
123
124
  // advertise a Fast tier for its OAuth endpoint.
124
125
  applyOpenAIDirectFastTier(body, useModel, opts);
126
+ noteRequestServiceTier(body.service_tier);
125
127
  // Keep public response storage and model-specific cache options out of
126
128
  // the shared OAuth payload. Storage opt-out still forces full frames in
127
129
  // _computeDelta; retained output items make those frames self-contained.
@@ -0,0 +1,9 @@
1
+ // Error code for a provider init that has no enabled provider to register.
2
+ // The registry refuses such an init to protect its live instances; readers
3
+ // that legitimately run before any provider exists (the model catalog on a
4
+ // fresh install) treat it as "nothing to load" instead of a failure.
5
+ export const NO_ENABLED_PROVIDERS = 'PROVIDERS_NONE_ENABLED';
6
+
7
+ export function noEnabledProvidersError(message) {
8
+ return Object.assign(new Error(message), { code: NO_ENABLED_PROVIDERS });
9
+ }
@@ -4,6 +4,7 @@ import { refreshCatalog as refreshMetadataCatalog } from './model-catalog.mjs';
4
4
  import { wrapProviderAdmission } from './admission-scheduler.mjs';
5
5
  import { createAccountPoolProvider } from './account-pool.mjs';
6
6
  import { isInclusiveProvider } from '../../../shared/llm/cost.mjs';
7
+ import { noEnabledProvidersError } from './registry-errors.mjs';
7
8
  // OpenAI-compat provider names are self-declared by openai-compat-presets.mjs via
8
9
  // OPENAI_COMPAT_PRESETS. No parallel list maintained here.
9
10
  const providers = new Map();
@@ -223,7 +224,7 @@ async function _initProvidersUnsynchronized(config, signal = null) {
223
224
  // here preserves whatever was already registered.
224
225
  const entries = Object.entries(config || {});
225
226
  if (entries.length === 0) {
226
- throw new Error('[provider] initProviders called with empty config — refusing to clear registry');
227
+ throw noEnabledProvidersError('[provider] initProviders called with empty config — refusing to clear registry');
227
228
  }
228
229
  const next = new Map();
229
230
  const nextSignatures = new Map();
@@ -258,7 +259,7 @@ async function _initProvidersUnsynchronized(config, signal = null) {
258
259
  // Second guard: every entry was disabled. Same reasoning — keep the
259
260
  // existing registry rather than going dark.
260
261
  if (next.size === 0) {
261
- throw new Error('[provider] all providers disabled in config — refusing to clear registry');
262
+ throw noEnabledProvidersError('[provider] all providers disabled in config — refusing to clear registry');
262
263
  }
263
264
  // OAuth preservation guard. anthropic-oauth / openai-oauth are NOT stored
264
265
  // in mixdog-config.json — buildDefaultConfig injects them at runtime by
@@ -6,11 +6,13 @@ import { clean } from '../../../shared/clean.mjs';
6
6
  // dropped on normalize rather than rejected.
7
7
  const TASK_ENTRY_FIELDS = ['id', 'text', 'status', 'kind'];
8
8
 
9
- // Dropped rows remain in the record but are no longer requested work.
9
+ // Dropped rows remain in the record but are no longer requested work. Every
10
+ // row, completed and dropped included, still fills one of the capped slots.
10
11
  export function goalTaskProgress(tasks) {
11
12
  return {
12
13
  tasksCompleted: tasks.filter((task) => task?.status === 'completed').length,
13
14
  tasksTotal: tasks.filter((task) => task?.status !== 'dropped').length,
15
+ taskSlotsUsed: tasks.length,
14
16
  };
15
17
  }
16
18
 
@@ -34,7 +36,18 @@ export function optionalGoalTaskChanges({ tasks, updates } = {}) {
34
36
  export function normalizeGoalTasks(input, previous = [], { strict = false } = {}) {
35
37
  if (input == null) return previous.map((task) => ({ ...task }));
36
38
  if (!Array.isArray(input)) throw new Error('goal tasks must be an array');
37
- if (input.length > MAX_GOAL_TASKS) throw new Error(`goal tasks support at most ${MAX_GOAL_TASKS} entries`);
39
+ if (input.length > MAX_GOAL_TASKS) {
40
+ // Settled rows count toward the cap; set_tasks may omit them (only
41
+ // unfinished tasks must be retained), which is how a long Goal makes room.
42
+ const settled = previous.some((task) => GOAL_TASK_SETTLED.includes(task?.status));
43
+ throw new Error(
44
+ `goal tasks support at most ${MAX_GOAL_TASKS} entries${
45
+ settled
46
+ ? '; completed and dropped tasks count, so to add more send set_tasks with only the unfinished tasks plus the new ones'
47
+ : ''
48
+ }`
49
+ );
50
+ }
38
51
  const previousByText = new Map(previous.map((task) => [clean(task?.text), task]));
39
52
  const reservedIds = new Set([...previous, ...input].map((task) => clean(task?.id)).filter(Boolean));
40
53
  let nextId = 1;
@@ -78,12 +91,18 @@ function taskInputRetains(entry, task) {
78
91
  }
79
92
 
80
93
  export function patchGoalTasks(previous, { updates, tasks } = {}) {
81
- const patches = updates ?? [];
82
- const additions = tasks ?? [];
83
- if (!Array.isArray(patches) || !Array.isArray(additions)) throw new Error('goal updates and tasks must be arrays');
94
+ const requested = updates ?? [];
95
+ const entries = tasks ?? [];
96
+ if (!Array.isArray(requested) || !Array.isArray(entries)) throw new Error('goal updates and tasks must be arrays');
97
+ const byId = new Map(previous.map((task) => [task.id, { ...task }]));
98
+ // A new-task entry naming an id this Goal already has can only mean that
99
+ // task, so it is applied as the task's update. An id the Goal does not have
100
+ // is still refused below.
101
+ const addressesExisting = (entry) => byId.has(clean(entry?.id));
102
+ const patches = [...requested, ...entries.filter(addressesExisting)];
103
+ const additions = entries.filter((entry) => !addressesExisting(entry));
84
104
  if (!patches.length && !additions.length) throw new Error('goal update_tasks requires updates or new tasks');
85
105
  if (patches.length > MAX_GOAL_TASKS) throw new Error(`goal updates support at most ${MAX_GOAL_TASKS} entries`);
86
- const byId = new Map(previous.map((task) => [task.id, { ...task }]));
87
106
  const seen = new Set();
88
107
  for (const patch of patches) {
89
108
  const id = clean(patch?.id);
@@ -3,6 +3,7 @@
3
3
  // No filesystem, no runtime coupling — values in, text out.
4
4
 
5
5
  import { goalTaskProgress } from './goal-tasks.mjs';
6
+ import { MAX_GOAL_TASKS } from './goal-tool-defs.mjs';
6
7
 
7
8
  function escapeGoalPromptText(value) {
8
9
  return String(value ?? '')
@@ -214,7 +215,7 @@ export function goalStateReminder(goal, { reason = '' } = {}) {
214
215
  }
215
216
  if (reason === 'deadline-reached' && goal.status === 'active') return '';
216
217
  const tasks = Array.isArray(goal.tasks) ? goal.tasks : [];
217
- const { tasksCompleted, tasksTotal } = goalTaskProgress(tasks);
218
+ const { tasksCompleted, tasksTotal, taskSlotsUsed } = goalTaskProgress(tasks);
218
219
  // Event-specific steering: what the model could not have learned from its own
219
220
  // tool results. Standing rules stay in the cached tool description.
220
221
  let lead = 'Current durable Goal snapshot:';
@@ -233,7 +234,7 @@ export function goalStateReminder(goal, { reason = '' } = {}) {
233
234
  ...(goal.status === 'blocked' ? [BLOCKED_LINE] : []),
234
235
  '',
235
236
  `Objective: ${escapeGoalPromptText(goal.objective)}`,
236
- `Status: ${escapeGoalPromptText(goal.status)} · tasks ${tasksCompleted}/${tasksTotal}`,
237
+ `Status: ${escapeGoalPromptText(goal.status)} · tasks ${tasksCompleted}/${tasksTotal} · task slots used ${taskSlotsUsed}/${MAX_GOAL_TASKS}`,
237
238
  ...(goal.blocker ? [`Waiting or stop reason: ${escapeGoalPromptText(goal.blocker)}`] : []),
238
239
  ...(goal.revision ? [`Revision: ${goal.revision}`] : []),
239
240
  ...goalTimeLines(goal),
@@ -16,7 +16,7 @@ const taskFields = {
16
16
  type: 'string',
17
17
  minLength: 1,
18
18
  maxLength: MAX_GOAL_TASK_TEXT_LENGTH,
19
- description: 'Required work or verification outcome.',
19
+ description: 'Brief work/check; results go in reply.',
20
20
  },
21
21
  status: {
22
22
  type: 'string',
@@ -22,6 +22,7 @@ function toolReply(ctx, id, goal, { full = false, previousIds = null } = {}) {
22
22
  status: goal.status,
23
23
  tasksCompleted: goal.tasksCompleted,
24
24
  tasksTotal: goal.tasksTotal,
25
+ taskSlotsUsed: goal.taskSlotsUsed,
25
26
  tasksUpdatedAt: goal.tasksUpdatedAt,
26
27
  timeUsedMs: goal.timeUsedMs,
27
28
  };
@@ -61,7 +61,7 @@ const ZERO_MATCH_PREFIXES = [
61
61
  const READ_ONLY_NAVIGATION_TOOLS = new Set(['find', 'glob', 'grep', 'list', 'read', 'code_graph']);
62
62
 
63
63
  const NAVIGATION_MISS_RE =
64
- /\b(?:enoent|enotdir)\b|path does not exist|directory does not exist|no such (?:file|path)|not found at this path|file not found in graph/i;
64
+ /\b(?:enoent|enotdir)\b|path does not exist|directory does not exist|no such (?:file|path)|not found at this path|file not found\b|symbol "[^"\n]*" not found in /i;
65
65
 
66
66
  export function isReadOnlyNavigationMiss(toolName, result) {
67
67
  if (!READ_ONLY_NAVIGATION_TOOLS.has(String(toolName || '').toLowerCase())) return false;
@@ -3,7 +3,11 @@
3
3
  * session runtime.
4
4
  */
5
5
  import { stripShellExitHeader, toolErrorDisplay } from './tool-result-text.mjs';
6
- import { normalizeToolTerminalStatus, toolResultTerminalStatus } from '../../../../shared/tool-status.mjs';
6
+ import {
7
+ hasBenignExitOutcome,
8
+ normalizeToolTerminalStatus,
9
+ toolResultTerminalStatus,
10
+ } from '../../../../shared/tool-status.mjs';
7
11
  import { isReadOnlyNavigationMiss } from '../result-classification.mjs';
8
12
  import { formatAggregateDetail, summarizeToolResult, toolLoadingTargets } from '../../../../shared/tool-surface.mjs';
9
13
  import { normalizeToolName } from '../../../../shared/tool-primitives.mjs';
@@ -43,7 +47,7 @@ export function shellCommandExitCode(text) {
43
47
  export function toolCallOutcome(message, rawText) {
44
48
  if (normalizeToolName(message?.toolName || message?.name) === 'git') {
45
49
  const exitCode = gitResultExitCode(rawText);
46
- if (exitCode !== null) return { isCallError: false, isExitError: true, exitCode };
50
+ if (exitCode !== null) return { isCallError: false, isExitError: !hasBenignExitOutcome(rawText), exitCode };
47
51
  if (gitResultError(rawText)) return { isCallError: true, isExitError: false, exitCode: null };
48
52
  }
49
53
  const exitCode = shellCommandExitCode(rawText);
@@ -51,7 +55,7 @@ export function toolCallOutcome(message, rawText) {
51
55
  // Every completed shell result carries `[exit code: N]` — including 0.
52
56
  // A non-zero code is a command failure unless execution marked a known
53
57
  // successful no-match/no-change outcome.
54
- const benignExit = exitCode !== 0 && /^\[outcome:\s*(?:no-match|no-change)\]\s*$/im.test(String(rawText || ''));
58
+ const benignExit = exitCode !== 0 && hasBenignExitOutcome(rawText);
55
59
  return { isCallError: false, isExitError: exitCode !== 0 && !benignExit, exitCode };
56
60
  }
57
61
  if (isReadOnlyNavigationMiss(message?.toolName || message?.name, rawText)) {