mixdog 1.0.8 → 1.0.9

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 (109) hide show
  1. package/package.json +2 -1
  2. package/scripts/lib/run-node-tests.mjs +13 -0
  3. package/scripts/session-efficiency-diag.mjs +283 -0
  4. package/src/defaults/skills/computer-use/SKILL.md +3 -3
  5. package/src/defaults/skills/local-provider/SKILL.md +2 -1
  6. package/src/defaults/skills/setup/SKILL.md +4 -4
  7. package/src/defaults/skills/setup/references/actions.md +1 -3
  8. package/src/defaults/skills/setup/references/surfaces.md +2 -3
  9. package/src/headless-exec.mjs +1 -0
  10. package/src/rules/shared/70-delivery.md +2 -2
  11. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +10 -0
  12. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +1 -1
  13. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  14. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +8 -4
  15. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +10 -1
  16. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +18 -3
  18. package/src/runtime/agent/orchestrator/providers/client-version-store.mjs +50 -0
  19. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +27 -4
  20. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +9 -5
  21. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +18 -4
  22. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +2 -2
  23. package/src/runtime/agent/orchestrator/providers/lib/anthropic-models.mjs +7 -1
  24. package/src/runtime/agent/orchestrator/providers/npm-cli-version.mjs +25 -2
  25. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +7 -1
  26. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +4 -0
  27. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +8 -0
  28. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +17 -3
  29. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +7 -1
  30. package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +16 -0
  31. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +0 -20
  32. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +12 -10
  33. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +3 -1
  34. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +1 -1
  35. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +15 -3
  36. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +11 -2
  37. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +5 -1
  38. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +23 -9
  39. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +15 -6
  40. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -4
  41. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +7 -0
  42. package/src/runtime/channels/lib/voice-transcription.mjs +30 -4
  43. package/src/runtime/computer-bridge/action-schema.mjs +3 -3
  44. package/src/runtime/computer-bridge/actions.mjs +4 -0
  45. package/src/runtime/computer-bridge/bridge-env.test-support.mjs +22 -0
  46. package/src/runtime/computer-bridge/client.mjs +10 -183
  47. package/src/runtime/computer-bridge/core-actions.mjs +2 -1
  48. package/src/runtime/computer-bridge/error-recovery.mjs +46 -34
  49. package/src/runtime/computer-bridge/result-canonical.mjs +181 -0
  50. package/src/runtime/local-provider/asset-installer.mjs +20 -10
  51. package/src/runtime/local-provider/catalog.mjs +4 -7
  52. package/src/runtime/local-provider/data/manifest.json +14 -1
  53. package/src/runtime/local-provider/hardware.mjs +36 -5
  54. package/src/runtime/local-provider/resumable-installations.mjs +2 -2
  55. package/src/runtime/local-provider/server.mjs +1 -1
  56. package/src/runtime/memory/index.mjs +2 -4
  57. package/src/runtime/memory/lib/core-memory-file.mjs +3 -8
  58. package/src/runtime/memory/lib/core-memory-store.mjs +14 -61
  59. package/src/runtime/memory/lib/cycle-scheduler/backlog-probe.mjs +5 -9
  60. package/src/runtime/memory/lib/cycle1/cycle1-plan.mjs +7 -0
  61. package/src/runtime/memory/lib/cycle1/cycle1-rows.mjs +43 -16
  62. package/src/runtime/memory/lib/cycle1/cycle1-window.mjs +11 -2
  63. package/src/runtime/memory/lib/embedding-provider.mjs +2 -5
  64. package/src/runtime/memory/lib/embedding-reindex.mjs +1 -5
  65. package/src/runtime/memory/lib/http-router/lifecycle-routes.mjs +0 -1
  66. package/src/runtime/memory/lib/memory-action-handlers/maintenance-actions.mjs +1 -1
  67. package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +4 -22
  68. package/src/runtime/memory/lib/memory.mjs +79 -32
  69. package/src/runtime/memory/lib/query-maintenance-handlers.mjs +0 -2
  70. package/src/runtime/office/com/com-adapter.mjs +3 -0
  71. package/src/runtime/office/pdf/pdf-adapter.mjs +50 -14
  72. package/src/runtime/office/pdf/pdf-render.mjs +2 -6
  73. package/src/runtime/office/pdf/pdf-security.mjs +8 -2
  74. package/src/runtime/office/portable/font-provisioner.mjs +28 -0
  75. package/src/runtime/office/portable/portable-docx-edits.mjs +28 -1
  76. package/src/runtime/office/portable/portable-soffice.mjs +44 -7
  77. package/src/runtime/shared/llm/anthropic-betas.mjs +7 -1
  78. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +1 -1
  79. package/src/runtime/shared/llm/default-context-window.mjs +42 -0
  80. package/src/runtime/shared/llm/model-catalog-projection.mjs +2 -0
  81. package/src/runtime/shared/llm/model-catalog.mjs +2 -0
  82. package/src/runtime/shared/plugin-paths.mjs +9 -2
  83. package/src/runtime/shared/worker-exec-argv.mjs +25 -0
  84. package/src/runtime/web-search/lib/formatter.mjs +43 -18
  85. package/src/session-runtime/automation-agents.mjs +88 -0
  86. package/src/session-runtime/automation-workflow.mjs +19 -6
  87. package/src/session-runtime/boot/apis.mjs +0 -1
  88. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +14 -12
  89. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +4 -25
  90. package/src/session-runtime/model-recency.mjs +10 -2
  91. package/src/session-runtime/model-route/set-route.mjs +7 -1
  92. package/src/session-runtime/resource-api.mjs +8 -2
  93. package/src/session-runtime/resource-plugins-api.mjs +14 -2
  94. package/src/session-runtime/route-state.mjs +6 -2
  95. package/src/session-runtime/schedule-session-run.mjs +7 -1
  96. package/src/session-runtime/services/agent-tool/render.mjs +0 -5
  97. package/src/session-runtime/settings-builtin-tools-api.mjs +0 -16
  98. package/src/session-runtime/setup-tool/executor.mjs +7 -13
  99. package/src/session-runtime/setup-tool/tool-defs.mjs +2 -3
  100. package/src/session-runtime/tool-policy-refresh.mjs +0 -2
  101. package/src/session-runtime/webhook-session-run.mjs +10 -11
  102. package/src/session-runtime/workflow-agents-api/agent-route.mjs +5 -0
  103. package/src/standalone/daemon-agent-control.mjs +8 -1
  104. package/src/standalone/daemon.mjs +4 -0
  105. package/src/tui/dist/index.mjs +3 -2
  106. package/src/tui/session/agent-job-feed/notification-router.mjs +7 -0
  107. package/src/tui/session/live-share.mjs +16 -1
  108. package/src/tui/session/notification-plan.mjs +4 -0
  109. package/src/session-runtime/bridge-first-use-gate.mjs +0 -72
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Open-source coding agent that matches Codex on Terminal-Bench 2.1 with 63% fewer tokens.",
@@ -106,6 +106,7 @@
106
106
  "bench:office:quality:live": "node src/runtime/office/bench/quality-live-benchmark.mjs",
107
107
  "diag:sessions": "node scripts/session-diag.mjs",
108
108
  "diag:tools": "node scripts/tool-efficiency-diag.mjs",
109
+ "diag:efficiency": "node scripts/session-efficiency-diag.mjs",
109
110
  "diag:sweep": "node scripts/session-sweep.mjs",
110
111
  "bench:session": "node scripts/session-bench.mjs",
111
112
  "bench:task": "node scripts/task-bench.mjs",
@@ -13,6 +13,8 @@ const SUMMARY_REPORTER = new URL('./test-summary-reporter.mjs', import.meta.url)
13
13
  // instead of dying with spawn ENAMETOOLONG before any test runs.
14
14
  export const ARG_BUDGET = process.platform === 'win32' ? 30_000 : 1_000_000;
15
15
 
16
+ const HEAP_FLAG = /^--(?:max-old-space-size|max-semi-space-size)=\d+$/;
17
+
16
18
  // An argument costs its own characters plus the separator and its quotes.
17
19
  const argCost = (arg) => arg.length + 3;
18
20
 
@@ -145,6 +147,12 @@ export async function runNodeTests(
145
147
  argCost('.999'); // the per-batch log suffix
146
148
  const batches = chunkFileArgs(fileArgs, Math.max(argBudget - fixedCost, 1));
147
149
  const coverageDir = coverage ? await mkdtemp(join(tmpdir(), 'mixdog-v8-coverage-')) : '';
150
+ // Node 24's test runner rebuilds each test process's execArgv from its option
151
+ // table and drops V8 flags on the way, so a heap cap passed to the runner no
152
+ // longer reached a test (a 128 MB cap ran with the default 4 GB). The
153
+ // environment still reaches every test process.
154
+ const heapOptions = nodeArgs.filter((arg) => HEAP_FLAG.test(arg));
155
+ const nodeOptions = [process.env.NODE_OPTIONS, ...heapOptions].filter(Boolean).join(' ');
148
156
  // Failure records travel by environment, not argv: the command line is
149
157
  // already budgeted, and a third reporter would warn about listeners.
150
158
  const childEnv = (recordsPath) => ({
@@ -152,11 +160,16 @@ export async function runNodeTests(
152
160
  TEMP: scratchDir,
153
161
  TMP: scratchDir,
154
162
  TMPDIR: scratchDir,
163
+ // A fixture must never write into the signed-in app's data (~/.mixdog):
164
+ // schedule/webhook logs, design-library bindings and the like resolve the
165
+ // default data dir. The desktop preload already does this per process.
166
+ MIXDOG_DATA_DIR: join(scratchDir, 'data'),
155
167
  // Only the immutable validator binary is shared; fixture data stays private.
156
168
  // A gate supplies one directory across lanes; a standalone run owns its own.
157
169
  MIXDOG_TEST_OOXML_CACHE_DIR: process.env.MIXDOG_TEST_OOXML_CACHE_DIR || join(scratchDir, 'ooxml-cache'),
158
170
  [FAILURE_RECORDS_ENV]: recordsPath,
159
171
  ...(coverageDir ? { NODE_V8_COVERAGE: coverageDir } : {}),
172
+ ...(nodeOptions ? { NODE_OPTIONS: nodeOptions } : {}),
160
173
  });
161
174
  let status = 0;
162
175
  const failures = [];
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env node
2
+ // Token-efficiency diag over the stored session transcripts. The trace-based
3
+ // diags (session-diag, tool-efficiency-diag) need agent-trace.jsonl, which a
4
+ // shipped build does not write; the session store is always there.
5
+ // Usage: node scripts/session-efficiency-diag.mjs [--since 5d] [--data-dir <dir>] [--agent lead] [--json]
6
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
7
+ import { homedir } from 'node:os';
8
+ import { join, resolve } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { argValue, hasFlag } from './lib/cli-args.mjs';
11
+ import { parseSince } from './lib/parse-since.mjs';
12
+ import { DEFAULT_SHELL_AUTO_BACKGROUND_MS } from '../src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs';
13
+
14
+ const CONTEXT_BUCKETS = [50_000, 100_000, 200_000, 300_000, 500_000, Infinity];
15
+ // Fallback for a session that never recorded a provider context size.
16
+ const FALLBACK_TOKENS_PER_BYTE = 0.3;
17
+
18
+ const size = (value) => {
19
+ if (value == null) return 0;
20
+ return typeof value === 'string' ? value.length : JSON.stringify(value).length;
21
+ };
22
+
23
+ function replayBytes(message) {
24
+ let bytes = 0;
25
+ for (const item of message.providerReplay?.items || []) {
26
+ if (item?.type !== 'thinking' && item?.type !== 'redacted_thinking' && item?.type !== 'reasoning') continue;
27
+ bytes += size(item.thinking) + size(item.signature) + size(item.data) + size(item.encrypted_content);
28
+ }
29
+ return bytes;
30
+ }
31
+
32
+ function userCategory(message) {
33
+ return message.meta === 'skill' ? 'skills' : 'user';
34
+ }
35
+
36
+ function add(map, key, fields) {
37
+ const row = map.get(key) || {};
38
+ for (const [name, value] of Object.entries(fields)) row[name] = (row[name] || 0) + value;
39
+ map.set(key, row);
40
+ return row;
41
+ }
42
+
43
+ export function createSummary() {
44
+ return {
45
+ sessions: 0,
46
+ requests: 0,
47
+ groups: new Map(),
48
+ carry: new Map(),
49
+ tools: new Map(),
50
+ buckets: new Map(),
51
+ shell: {},
52
+ };
53
+ }
54
+
55
+ // Folds one stored session into the summary. A message's carry is its size
56
+ // times the requests sent after it: every later request re-sends it.
57
+ export function addSession(summary, session) {
58
+ const messages = Array.isArray(session?.messages) ? session.messages : [];
59
+ const requests = messages.filter((message) => message.role === 'assistant').length;
60
+ if (!requests) return;
61
+ summary.sessions++;
62
+ summary.requests += requests;
63
+ const group = add(summary.groups, `${session.agent || '-'} @${session.model || '-'}`, { sessions: 1, requests });
64
+ const shell = summary.shell;
65
+ const callsById = new Map();
66
+ const promoted = new Map();
67
+ const prefixes = [];
68
+ let prefix = size(session.tools);
69
+ let later = requests;
70
+ for (const message of messages) {
71
+ if (message.role === 'assistant') {
72
+ later--;
73
+ prefixes.push(prefix);
74
+ const calls = Array.isArray(message.toolCalls) ? message.toolCalls : [];
75
+ let argBytes = 0;
76
+ for (const call of calls) {
77
+ argBytes += size(call.arguments) + size(call.name);
78
+ callsById.set(call.id, { ...call, alone: calls.length === 1 });
79
+ }
80
+ const thinking = replayBytes(message);
81
+ const text = size(message.content);
82
+ add(summary.carry, 'tool args', { bytes: argBytes * later });
83
+ add(summary.carry, 'thinking', { bytes: thinking * later });
84
+ add(summary.carry, 'assistant text', { bytes: text * later });
85
+ group.calls = (group.calls || 0) + calls.length;
86
+ if (calls.length) group.toolRequests = (group.toolRequests || 0) + 1;
87
+ if (calls.length === 1) group.singleCall = (group.singleCall || 0) + 1;
88
+ if (calls.length && calls.every((call) => call.name === 'task' && call.arguments?.action === 'wait')) {
89
+ shell.taskWaitOnlyRequests = (shell.taskWaitOnlyRequests || 0) + 1;
90
+ }
91
+ prefix += argBytes + thinking + text;
92
+ continue;
93
+ }
94
+ const bytes = size(message.content);
95
+ prefix += bytes;
96
+ if (message.role === 'system') add(summary.carry, 'system', { bytes: bytes * later });
97
+ else if (message.role === 'user') add(summary.carry, userCategory(message), { bytes: bytes * later });
98
+ else if (message.role === 'tool') {
99
+ add(summary.carry, 'tool results', { bytes: bytes * later });
100
+ const call = callsById.get(message.toolCallId);
101
+ if (!call) continue;
102
+ add(summary.tools, call.name, { calls: 1, bytes, carry: bytes * later });
103
+ const text = typeof message.content === 'string' ? message.content : '';
104
+ if (call.name === 'shell') recordShellCall(shell, promoted, call, text);
105
+ else if (call.name === 'task') recordTaskResult(shell, promoted, call, text);
106
+ }
107
+ }
108
+ const lastContext = Number(session.lastContextTokens) || 0;
109
+ const tokensPerByte = lastContext > 0 && prefix > 0 ? lastContext / prefix : FALLBACK_TOKENS_PER_BYTE;
110
+ for (const bytes of prefixes) {
111
+ const context = bytes * tokensPerByte;
112
+ group.tokenRequests = (group.tokenRequests || 0) + context;
113
+ add(
114
+ summary.buckets,
115
+ CONTEXT_BUCKETS.find((limit) => context < limit),
116
+ { requests: 1, tokenRequests: context }
117
+ );
118
+ }
119
+ }
120
+
121
+ function recordShellCall(shell, promoted, call, text) {
122
+ shell.calls = (shell.calls || 0) + 1;
123
+ const waitMs = call.arguments?.wait_ms;
124
+ const shortWait = Number.isFinite(waitMs) && waitMs < DEFAULT_SHELL_AUTO_BACKGROUND_MS;
125
+ if (shortWait) shell.shortWait = (shell.shortWait || 0) + 1;
126
+ if (!text.startsWith('background task')) return;
127
+ shell.promoted = (shell.promoted || 0) + 1;
128
+ if (shortWait) shell.shortWaitPromoted = (shell.shortWaitPromoted || 0) + 1;
129
+ const taskId = /^task_id: (\S+)/m.exec(text)?.[1];
130
+ if (taskId) promoted.set(taskId, { shortWait, durationKnown: false });
131
+ }
132
+
133
+ // The first task result that reports both timestamps settles how long the
134
+ // promoted command really ran.
135
+ function recordTaskResult(shell, promoted, call, text) {
136
+ const job = promoted.get(call.arguments?.task_id);
137
+ if (!job || job.durationKnown) return;
138
+ const started = Date.parse(/^started: (\S+)/m.exec(text)?.[1] || '');
139
+ const finished = Date.parse(/^finished: (\S+)/m.exec(text)?.[1] || '');
140
+ if (!Number.isFinite(started) || !Number.isFinite(finished)) return;
141
+ job.durationKnown = true;
142
+ if (!job.shortWait) return;
143
+ shell.shortWaitTimed = (shell.shortWaitTimed || 0) + 1;
144
+ if (finished - started <= DEFAULT_SHELL_AUTO_BACKGROUND_MS) {
145
+ shell.shortWaitInsideDefault = (shell.shortWaitInsideDefault || 0) + 1;
146
+ }
147
+ }
148
+
149
+ const share = (part, whole) => (whole > 0 ? Math.round((part / whole) * 1000) / 10 : 0);
150
+
151
+ export function buildReport(summary) {
152
+ const carryTotal = [...summary.carry.values()].reduce((sum, row) => sum + row.bytes, 0);
153
+ const toolCarryTotal = [...summary.tools.values()].reduce((sum, row) => sum + row.carry, 0);
154
+ const tokenRequestTotal = [...summary.buckets.values()].reduce((sum, row) => sum + row.tokenRequests, 0);
155
+ return {
156
+ sessions: summary.sessions,
157
+ requests: summary.requests,
158
+ groups: [...summary.groups.entries()]
159
+ .map(([name, row]) => ({
160
+ name,
161
+ sessions: row.sessions,
162
+ requests: row.requests,
163
+ callsPerToolRequest: row.toolRequests ? Math.round((row.calls / row.toolRequests) * 100) / 100 : 0,
164
+ singleCallPct: share(row.singleCall || 0, row.toolRequests || 0),
165
+ avgContextTokens: Math.round((row.tokenRequests || 0) / row.requests),
166
+ tokenRequests: Math.round(row.tokenRequests || 0),
167
+ }))
168
+ .sort((a, b) => b.tokenRequests - a.tokenRequests),
169
+ carry: [...summary.carry.entries()]
170
+ .map(([name, row]) => ({ name, pct: share(row.bytes, carryTotal) }))
171
+ .sort((a, b) => b.pct - a.pct),
172
+ tools: [...summary.tools.entries()]
173
+ .map(([name, row]) => ({
174
+ name,
175
+ calls: row.calls,
176
+ avgResultBytes: Math.round(row.bytes / row.calls),
177
+ carryPct: share(row.carry, toolCarryTotal),
178
+ }))
179
+ .sort((a, b) => b.carryPct - a.carryPct),
180
+ requestContext: CONTEXT_BUCKETS.map((limit) => {
181
+ const row = summary.buckets.get(limit) || { requests: 0, tokenRequests: 0 };
182
+ return {
183
+ under: Number.isFinite(limit) ? limit : null,
184
+ requestPct: share(row.requests, summary.requests),
185
+ tokenRequestPct: share(row.tokenRequests, tokenRequestTotal),
186
+ };
187
+ }),
188
+ shell: {
189
+ calls: summary.shell.calls || 0,
190
+ promoted: summary.shell.promoted || 0,
191
+ waitUnderDefault: summary.shell.shortWait || 0,
192
+ waitUnderDefaultPromoted: summary.shell.shortWaitPromoted || 0,
193
+ waitUnderDefaultTimed: summary.shell.shortWaitTimed || 0,
194
+ waitUnderDefaultFinishedInsideDefault: summary.shell.shortWaitInsideDefault || 0,
195
+ taskWaitOnlyRequests: summary.shell.taskWaitOnlyRequests || 0,
196
+ taskWaitOnlyPct: share(summary.shell.taskWaitOnlyRequests || 0, summary.requests),
197
+ },
198
+ };
199
+ }
200
+
201
+ const compact = (value) => {
202
+ if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
203
+ return value >= 1e3 ? `${(value / 1e3).toFixed(1)}k` : String(Math.round(value));
204
+ };
205
+
206
+ function table(rows) {
207
+ const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => String(row[column]).length)));
208
+ return rows.map((row) =>
209
+ row
210
+ .map((cell, column) => String(cell).padEnd(widths[column]))
211
+ .join(' ')
212
+ .trimEnd()
213
+ );
214
+ }
215
+
216
+ function renderReport(report, sinceTs) {
217
+ const lines = [
218
+ `session efficiency: ${report.sessions} sessions, ${report.requests} requests${sinceTs ? ` since ${new Date(sinceTs).toISOString()}` : ''}`,
219
+ '',
220
+ ...table([
221
+ ['agent @model', 'sessions', 'requests', 'calls/req', 'single-call%', 'avgContext', 'tokenRequests'],
222
+ ...report.groups
223
+ .slice(0, 12)
224
+ .map((row) => [
225
+ row.name,
226
+ row.sessions,
227
+ row.requests,
228
+ row.callsPerToolRequest,
229
+ row.singleCallPct,
230
+ compact(row.avgContextTokens),
231
+ compact(row.tokenRequests),
232
+ ]),
233
+ ]),
234
+ '',
235
+ `carried context (size x later requests): ${report.carry.map((row) => `${row.name} ${row.pct}%`).join(', ')}`,
236
+ `tool results by carry: ${report.tools
237
+ .slice(0, 10)
238
+ .map((row) => `${row.name} ${row.carryPct}% (${row.calls} calls, avg ${compact(row.avgResultBytes)}B)`)
239
+ .join(', ')}`,
240
+ `request context: ${report.requestContext
241
+ .map(
242
+ (row) =>
243
+ `${row.under ? `<${compact(row.under)}` : 'larger'} ${row.requestPct}% of requests / ${row.tokenRequestPct}% of token-requests`
244
+ )
245
+ .join(', ')}`,
246
+ `shell foreground window: ${report.shell.calls} calls, ${report.shell.promoted} promoted to background; wait_ms under the ${DEFAULT_SHELL_AUTO_BACKGROUND_MS}ms default on ${report.shell.waitUnderDefault} calls (${report.shell.waitUnderDefaultPromoted} promoted, ${report.shell.waitUnderDefaultFinishedInsideDefault} of ${report.shell.waitUnderDefaultTimed} timed ones finished inside the default); task-wait-only requests: ${report.shell.taskWaitOnlyRequests} (${report.shell.taskWaitOnlyPct}%)`,
247
+ ];
248
+ return `${lines.join('\n')}\n`;
249
+ }
250
+
251
+ function main() {
252
+ const mixdogHome = process.env.MIXDOG_HOME || resolve(homedir(), '.mixdog');
253
+ const dataDir = resolve(argValue('--data-dir', process.env.MIXDOG_DATA_DIR || join(mixdogHome, 'data')));
254
+ const sinceTs = parseSince(argValue('--since', '5d'));
255
+ const agentFilter = argValue('--agent', null);
256
+ const sessionsDir = join(dataDir, 'sessions');
257
+ const summary = createSummary();
258
+ let names;
259
+ try {
260
+ names = readdirSync(sessionsDir).filter((name) => name.endsWith('.json'));
261
+ } catch {
262
+ process.stderr.write(`no sessions directory at ${sessionsDir}\n`);
263
+ return 1;
264
+ }
265
+ for (const name of names) {
266
+ const file = join(sessionsDir, name);
267
+ try {
268
+ if (sinceTs != null && statSync(file).mtimeMs < sinceTs) continue;
269
+ const session = JSON.parse(readFileSync(file, 'utf8'));
270
+ if (agentFilter && String(session.agent || '') !== agentFilter) continue;
271
+ addSession(summary, session);
272
+ } catch {
273
+ /* a session mid-write or unreadable: skip it */
274
+ }
275
+ }
276
+ const report = buildReport(summary);
277
+ process.stdout.write(hasFlag('--json') ? `${JSON.stringify(report, null, 2)}\n` : renderReport(report, sinceTs));
278
+ return 0;
279
+ }
280
+
281
+ if (resolve(process.argv[1] || '') === fileURLToPath(import.meta.url)) {
282
+ process.exitCode = main();
283
+ }
@@ -166,9 +166,9 @@ GNOME with the Window Calls extension); prefer accessibility refs there.
166
166
  `mouse_down` holds a button past the end of its command and is
167
167
  background-only; pair it with `mouse_up` on a fresh observation.
168
168
  `key_down` holds a key the same way and is foreground-only, because no window
169
- message can leave a key physically down; pair it with `key_up`. Any session
170
- exit releases what the session pressed, and a failed release is reported
171
- rather than assumed.
169
+ message can leave a key physically down; pair it with `key_up`. The end of
170
+ the turn, like any session exit, releases what the session pressed, and a
171
+ failed release is reported rather than assumed.
172
172
  Execution stops at the first failure or when the target transitions
173
173
  (popup, dialog, window change) and returns one fresh observation. An act
174
174
  whose first action uses a `ref` returns accessibility only (`ax`) unless
@@ -87,7 +87,8 @@ available before Local Provider is installed or enabled.
87
87
  - For startup failure, use `starting`, `lastError`, and `lastExit` diagnostics.
88
88
  Do not print authentication keys or attempt unmanaged server commands.
89
89
  - Hardware status may initially be `checking`; inspect again after detection
90
- completes. All NVIDIA GPUs are checked without blocking the app. Loading
90
+ completes. All NVIDIA GPUs on Windows, or the Apple Silicon GPU on a Mac
91
+ (Metal, on unified memory), are checked without blocking the app. Loading
91
92
  chooses a compatible GPU with enough free memory for the catalog's estimate;
92
93
  that estimate is a guard, not a guarantee against driver/runtime allocation
93
94
  failure. Never bypass a failed resource check.
@@ -65,13 +65,13 @@ that a call returned without transport error.
65
65
  provider restores Main inheritance; omission never resets it. Disabling an
66
66
  agent preserves its route for re-enabling.
67
67
  - Web Search model selection and Web Search tool exposure are separate.
68
- - Memory master state controls tools, core injection, and background cycles;
69
- `set_recap_enabled` changes only the background cycles. Core Memory entries
68
+ - Memory master state controls tools and background cycles; Core Memory
69
+ injection stays on regardless. `set_recap_enabled` changes only the
70
+ background cycles. Core Memory entries
70
71
  are read and changed with the `memory` tool, not setup.
71
72
  - Built-ins have installation state separate from enabled state. Browser Use,
72
73
  Computer Use, and voice actions require this conversation open in the local
73
- Desktop window. `set_first_use_approval` controls first-use approval for
74
- Browser Use and Computer Use only, not voice.
74
+ Desktop window.
75
75
  - Desktop appearance changes affect the Desktop host, not a paired phone.
76
76
  Display language is independent of profile response language. A required
77
77
  reload is reported, never performed automatically.
@@ -27,7 +27,6 @@ Read this when choosing a `setup` mutation.
27
27
  | Background memory cycles | `set_recap_enabled` |
28
28
  | Web Search tool exposure | `set_web_search_enabled` |
29
29
  | Git, Office, Code Tidy, Local Provider, Browser, Computer, or voice enabled state | `set_builtin_enabled` |
30
- | One approval per session before the first Browser Use or Computer Use call (`name`: browser or computer; on by default) | `set_first_use_approval` |
31
30
  | Git, Memory, Office, Code Tidy, Local Provider, Browser, Computer, or voice preparation | `install_builtin` |
32
31
  | Managed local model installation (use the local-provider skill) | `install_local_model` |
33
32
  | Start or resume a background runtime/model download | `start_local_installation` |
@@ -150,8 +149,7 @@ the Browser Use / Computer Use installed and enabled markers are read with
150
149
  - Browser Use, Computer Use, and voice installation/toggles use the same
151
150
  `install_builtin` and `set_builtin_enabled` actions through a live Desktop
152
151
  receipt. Computer Use runs on Windows, macOS, and Linux desktops. These
153
- actions do not approve OS permission dialogs or first-use tool approval on
154
- the user's behalf.
152
+ actions do not approve OS permission dialogs on the user's behalf.
155
153
  - Environment feature overrides are diagnostics for headless or benchmark
156
154
  environments, not ordinary user settings.
157
155
 
@@ -59,9 +59,8 @@ toggles, Projects, and linked-device revocation through the attached
59
59
  Desktop. A receipt identifies `scope:desktop-host`; appearance is not applied
60
60
  to the paired browser. No Desktop claimant means no confirmed change.
61
61
 
62
- Computer observe-only, background Memory cycles (`set_recap_enabled`), and
63
- Browser/Computer first-use approval have no settings control in either app;
64
- setup is their only supported route. Usage pin is the pin toggle on the activity rail usage card.
62
+ Computer observe-only and background Memory cycles (`set_recap_enabled`)
63
+ have no settings control in either app; setup is their only supported route. Usage pin is the pin toggle on the activity rail usage card.
65
64
 
66
65
  Schedules/webhooks and workflow/agent/skill definitions use their runtime
67
66
  actions directly. These do not require clicking their Desktop rails.
@@ -396,6 +396,7 @@ async function createHeadlessSessionRuntime(boundary, { provider, model, cwd, we
396
396
  const createRuntime = runtimeFactory || (await import('./mixdog-session-runtime.mjs')).createMixdogSessionRuntime;
397
397
  process.env.MIXDOG_FEATURE_WEB_SEARCH = webSearch === true ? '1' : '0';
398
398
  process.env.MIXDOG_FEATURE_MEMORY = '0';
399
+ process.env.MIXDOG_BOOT_CORE_MEMORY = '0';
399
400
  return createRuntime({
400
401
  provider,
401
402
  model,
@@ -5,8 +5,8 @@
5
5
  results in the conversation instead.
6
6
  - Reference files and folders in inline code by a Project-root-relative or
7
7
  absolute path, optionally `:line[:column]` (`deliverables/report.docx`,
8
- `src/app.ts:42`); never a bare name, a path relative to another folder, or a
9
- `file://` URI.
8
+ `src/app.ts:42`); files outside the Project always by absolute path; never a
9
+ bare name, a path relative to another folder, or a `file://` URI.
10
10
  - Deliver requested visual artifacts (SVG, diagrams, HTML pages) as saved files
11
11
  and give their path; never paste their source as the answer unless the user
12
12
  asks for the code.
@@ -18,6 +18,10 @@ import { headerValue } from './retry-classification.mjs';
18
18
  // Server-declared reason header for a fast-mode rejection caused by missing
19
19
  // overage billing (matches the reference client's header name).
20
20
  const OVERAGE_DISABLED_HEADER = 'anthropic-ratelimit-unified-overage-disabled-reason';
21
+ // Body-only form of the same rejection ("Usage credits are required for fast
22
+ // mode."), sent as a 429 rate_limit_error without the header above.
23
+ const CREDITS_REQUIRED_PATTERN = /usage credits are required for fast mode/i;
24
+ const CREDITS_REQUIRED_REASON = 'usage_credits_required';
21
25
 
22
26
  // A Retry-After under this bound is short enough that waiting keeps fast mode
23
27
  // (and its cache prefix) worthwhile.
@@ -66,6 +70,12 @@ export function noteFastModeCapacityError(err, { fast = false, now = Date.now()
66
70
  _disabledReason = String(overageReason);
67
71
  return 'disabled';
68
72
  }
73
+ // Same terminal condition reported only in the error body: the account has
74
+ // no usage credits for fast mode, so no cooldown can make it succeed.
75
+ if (CREDITS_REQUIRED_PATTERN.test(String(err?.message || ''))) {
76
+ _disabledReason = CREDITS_REQUIRED_REASON;
77
+ return 'disabled';
78
+ }
69
79
 
70
80
  const retryAfterMs = Number(err?.retryAfterMs);
71
81
  const hasWindow = Number.isFinite(retryAfterMs) && retryAfterMs >= 0;
@@ -48,7 +48,7 @@ function fallbackAnthropicMaxTokens(model) {
48
48
  if (MAX_TOKENS[model]) return MAX_TOKENS[model];
49
49
  const id = String(model || '').toLowerCase();
50
50
  if (id.includes('opus')) return 65536;
51
- if (id.includes('fable')) return 65536;
51
+ if (id.includes('fable') || id.includes('mythos')) return 65536;
52
52
  const sonnetVersion = id.match(/^claude-sonnet-(\d+)/);
53
53
  if (sonnetVersion) return Number(sonnetVersion[1]) >= 5 ? 65536 : 16384;
54
54
  if (id.includes('sonnet')) return 16384;
@@ -17,7 +17,7 @@ const VERSION_GATE_PATTERN =
17
17
  /Claude Code\s+(\d{1,4}\.\d{1,4}\.\d{1,6})\s+does not support this model;\s*version\s+(\d{1,4}\.\d{1,4}\.\d{1,6})\s+or newer is required\b/i;
18
18
 
19
19
  // Effective version = max(floor, learned minimum, live @anthropic-ai/claude-code).
20
- const liveCliVersion = createNpmVersionSource('@anthropic-ai/claude-code');
20
+ const liveCliVersion = createNpmVersionSource('@anthropic-ai/claude-code', { persistKey: 'claude-cli' });
21
21
 
22
22
  let learnedCliVersion = null;
23
23
  let learnedCliVersionLoaded = false;
@@ -51,14 +51,18 @@ export async function judgeInitialStatus(result, requestBody, { provider, cleanu
51
51
  release();
52
52
  // Initial-response failure: nothing was sampled, so the typed retry
53
53
  // rules upstream own the decision. Subscription 429s never retry
54
- // in-loop, so the cooldown is what keeps the NEXT turn off the drained
55
- // fast pool.
56
- noteFastModeCapacityError(
57
- { httpStatus: status, headers: result?.response?.headers },
54
+ // in-loop; a drained or unavailable fast pool says nothing about
55
+ // standard-speed capacity, so `speed` is dropped and the error is
56
+ // flagged for a single standard-speed replay by the caller.
57
+ const fastDecision = noteFastModeCapacityError(
58
+ { httpStatus: status, headers: result?.response?.headers, message: quotaText },
58
59
  { fast: requestBody?.speed === 'fast' }
59
60
  );
61
+ const fastDowngraded = fastDecision === 'downgrade' || fastDecision === 'disabled';
62
+ if (fastDowngraded) delete requestBody.speed;
60
63
  throw Object.assign(anthropicQuotaError(status, result?.response?.headers, provider.scrubTokens(quotaText)), {
61
64
  initialResponseError: true,
65
+ fastDowngraded,
62
66
  });
63
67
  }
64
68
  const err = new Error(`Anthropic OAuth API ${status}`);
@@ -97,7 +97,16 @@ export function createAnthropicOAuthRequest({
97
97
  withRetry(
98
98
  async ({ signal: attemptSignal }) => {
99
99
  const result = await doRequestImpl(accessToken, attemptSignal, requestBody);
100
- await judgeInitialStatus(result, requestBody, { provider, cleanupCancelHandler });
100
+ try {
101
+ await judgeInitialStatus(result, requestBody, { provider, cleanupCancelHandler });
102
+ } catch (err) {
103
+ // Fast-pool 429 before any byte was sampled: `speed` is already
104
+ // dropped, so replay once at standard speed within this attempt.
105
+ if (err?.fastDowngraded !== true) throw err;
106
+ const replay = await doRequestImpl(accessToken, attemptSignal, requestBody);
107
+ await judgeInitialStatus(replay, requestBody, { provider, cleanupCancelHandler });
108
+ return replay;
109
+ }
101
110
  return result;
102
111
  },
103
112
  {
@@ -77,7 +77,7 @@ const ANTHROPIC_SSE_ERROR_TYPE_STATUS = new Map([
77
77
  ['request_too_large', 413],
78
78
  ['rate_limit_error', 429],
79
79
  ['api_error', 500],
80
- ['overloaded_error', 503],
80
+ ['overloaded_error', 529],
81
81
  ]);
82
82
 
83
83
  function _statusForAnthropicSseError(event, payload) {
@@ -20,6 +20,8 @@ import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
20
20
  import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs';
21
21
  import { normalizeExpiresAtMs as _normalizeExpiresAt, scrubOAuthSecrets } from './lib/oauth-token-utils.mjs';
22
22
  import { ANTIGRAVITY_MODELS } from '../../../shared/llm/provider-model-identities.mjs';
23
+ import { readLastKnownVersion, rememberLastKnownVersion } from './client-version-store.mjs';
24
+ import { maxSemver } from './npm-cli-version.mjs';
23
25
  export { ANTIGRAVITY_MODELS } from '../../../shared/llm/provider-model-identities.mjs';
24
26
 
25
27
  // The Antigravity IDE's installed-app OAuth client, which every copy of that
@@ -62,7 +64,7 @@ export const PROJECT_ENDPOINT = CONTENT_ENDPOINT;
62
64
 
63
65
  export const DEFAULT_ANTIGRAVITY_MODEL = ANTIGRAVITY_MODELS[0].id;
64
66
 
65
- const ANTIGRAVITY_VERSION_FALLBACK = '2.8.0';
67
+ const ANTIGRAVITY_VERSION_FALLBACK = '2.19.1';
66
68
  export const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
67
69
  export const TOKEN_TIMEOUT_MS = 30_000;
68
70
  export const PROJECT_TIMEOUT_MS = 30_000;
@@ -71,21 +73,31 @@ export const TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1000;
71
73
 
72
74
  // The backend gates models on the client version in the User-Agent, so the
73
75
  // version tracks the latest Antigravity release via its update manifest.
74
- // The pinned value is the offline fallback; the env override always wins.
76
+ // The last discovered version is kept on disk for offline starts; the pinned
77
+ // value is only the first-run fallback. The env override always wins.
75
78
  const VERSION_MANIFEST_URL =
76
79
  'https://antigravity-hub-auto-updater-974169037036.us-central1.run.app/manifest/latest-arm64-mac.yml';
77
80
  const VERSION_FETCH_TIMEOUT_MS = 5_000;
78
81
  const VERSION_RETRY_AFTER_MS = 10 * 60 * 1000;
82
+ const VERSION_LAST_KNOWN_KEY = 'antigravity-hub';
79
83
  let _discoveredVersion = null;
84
+ let _lastKnownVersion;
80
85
  let _versionFetch = null;
81
86
  let _versionFailedAt = 0;
82
87
 
83
88
  function antigravityVersion() {
84
89
  return (
85
- String(process.env.MIXDOG_ANTIGRAVITY_VERSION || '').trim() || _discoveredVersion || ANTIGRAVITY_VERSION_FALLBACK
90
+ String(process.env.MIXDOG_ANTIGRAVITY_VERSION || '').trim() ||
91
+ _discoveredVersion ||
92
+ maxSemver(lastKnownAntigravityVersion(), ANTIGRAVITY_VERSION_FALLBACK)
86
93
  );
87
94
  }
88
95
 
96
+ function lastKnownAntigravityVersion() {
97
+ if (_lastKnownVersion === undefined) _lastKnownVersion = readLastKnownVersion(VERSION_LAST_KNOWN_KEY);
98
+ return _lastKnownVersion;
99
+ }
100
+
89
101
  /** Version from an electron-builder update manifest, or null when absent. */
90
102
  export function parseAntigravityManifestVersion(text) {
91
103
  for (const line of String(text || '').split(/\r?\n/)) {
@@ -113,6 +125,7 @@ export function ensureAntigravityVersion({ fetchFn = fetch } = {}) {
113
125
  signal: AbortSignal.timeout(VERSION_FETCH_TIMEOUT_MS),
114
126
  });
115
127
  if (res.ok) _discoveredVersion = parseAntigravityManifestVersion(await res.text());
128
+ if (_discoveredVersion) rememberLastKnownVersion(VERSION_LAST_KNOWN_KEY, _discoveredVersion);
116
129
  } catch {
117
130
  // The pinned fallback stays valid when discovery fails.
118
131
  } finally {
@@ -125,6 +138,8 @@ export function ensureAntigravityVersion({ fetchFn = fetch } = {}) {
125
138
  }
126
139
 
127
140
  export function _resetAntigravityVersionForTest() {
141
+ rememberLastKnownVersion(VERSION_LAST_KNOWN_KEY, null);
142
+ _lastKnownVersion = undefined;
128
143
  _discoveredVersion = null;
129
144
  _versionFetch = null;
130
145
  _versionFailedAt = 0;
@@ -0,0 +1,50 @@
1
+ // Last live client version each provider saw, kept on disk under one key per
2
+ // client. A new or offline process starts from the newest version this machine
3
+ // has observed instead of a floor that only moves with a release. Skipped
4
+ // entirely when MIXDOG_DISABLE_LIVE_CLI_VERSIONS=1.
5
+
6
+ import { readFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { updateJsonAtomicSync } from '../../../shared/atomic-file.mjs';
9
+ import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
10
+
11
+ const LAST_KNOWN_FILE_NAME = 'client-versions.json';
12
+ const LAST_KNOWN_SCHEMA_VERSION = 1;
13
+ const LAST_KNOWN_VALUE = /^[\w.+-]{1,64}$/;
14
+
15
+ const liveLookupDisabled = () => process.env.MIXDOG_DISABLE_LIVE_CLI_VERSIONS === '1';
16
+ const lastKnownPath = () => join(resolvePluginData(), LAST_KNOWN_FILE_NAME);
17
+
18
+ /** The last live version remembered under `key`, or null. */
19
+ export function readLastKnownVersion(key) {
20
+ if (!key || liveLookupDisabled()) return null;
21
+ try {
22
+ const raw = JSON.parse(readFileSync(lastKnownPath(), 'utf-8'));
23
+ if (raw?.version !== LAST_KNOWN_SCHEMA_VERSION) return null;
24
+ const value = raw.entries?.[key]?.value;
25
+ return typeof value === 'string' && LAST_KNOWN_VALUE.test(value) ? value : null;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ /** Remember a live version; `null` forgets it. Best-effort: never throws. */
32
+ export function rememberLastKnownVersion(key, value) {
33
+ if (!key || liveLookupDisabled()) return;
34
+ if (value != null && !LAST_KNOWN_VALUE.test(String(value))) return;
35
+ try {
36
+ updateJsonAtomicSync(
37
+ lastKnownPath(),
38
+ (current) => {
39
+ const entries = current?.version === LAST_KNOWN_SCHEMA_VERSION ? { ...current.entries } : {};
40
+ if ((entries[key]?.value ?? null) === value) return undefined;
41
+ if (value == null) delete entries[key];
42
+ else entries[key] = { value, fetchedAt: Date.now() };
43
+ return { version: LAST_KNOWN_SCHEMA_VERSION, entries };
44
+ },
45
+ { lock: true }
46
+ );
47
+ } catch {
48
+ /* a read-only data directory must not break the lookup */
49
+ }
50
+ }