mixdog 1.0.1 → 1.0.3

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 (58) hide show
  1. package/README.md +8 -0
  2. package/package.json +2 -2
  3. package/src/defaults/skills/browser-use/SKILL.md +14 -6
  4. package/src/defaults/skills/setup/references/actions.md +6 -3
  5. package/src/defaults/skills/setup/references/surfaces.md +8 -4
  6. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +0 -1
  7. package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +10 -1
  8. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +40 -99
  9. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +6 -1
  10. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +2 -0
  11. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +7 -3
  12. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +1 -1
  13. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +18 -38
  14. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-path-fanout.mjs +59 -32
  15. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +15 -10
  16. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-request.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/tools/builtin/lib/shared-native-scan.mjs +12 -0
  18. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +14 -0
  19. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  20. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +3 -1
  21. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +17 -9
  22. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -0
  23. package/src/runtime/agent/orchestrator/tools/patch/wave.mjs +4 -0
  24. package/src/runtime/browser-bridge/input-fields.mjs +1 -1
  25. package/src/runtime/browser-bridge/tool-defs.mjs +1 -1
  26. package/src/runtime/computer-bridge/actions.mjs +0 -13
  27. package/src/runtime/media/adapters/antigravity-image.mjs +7 -3
  28. package/src/runtime/media/adapters/codex-image.mjs +12 -1
  29. package/src/runtime/media/adapters/gemini-image.mjs +17 -6
  30. package/src/runtime/media/adapters/gemini-video.mjs +19 -9
  31. package/src/runtime/media/adapters/xai-media.mjs +20 -7
  32. package/src/runtime/media/jobs.mjs +35 -3
  33. package/src/runtime/media/media-usage.mjs +126 -0
  34. package/src/runtime/media/tool.mjs +16 -4
  35. package/src/runtime/shared/llm/cost.mjs +50 -3
  36. package/src/runtime/shared/llm/model-catalog-projection.mjs +7 -2
  37. package/src/runtime/shared/llm/model-catalog.mjs +2 -1
  38. package/src/runtime/shared/llm/model-pricing-rates.mjs +21 -0
  39. package/src/runtime/shared/llm/usage-accounting.mjs +25 -5
  40. package/src/runtime/shared/llm/usage-context.mjs +6 -0
  41. package/src/runtime/shared/llm/usage-ledger-import.mjs +1 -0
  42. package/src/session-runtime/cwd-plugins/plugin-status.mjs +24 -4
  43. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +9 -1
  44. package/src/session-runtime/services/agent-tool/index-write-queue.mjs +38 -6
  45. package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +9 -2
  46. package/src/session-runtime/services/agent-tool/worker-index.mjs +6 -3
  47. package/src/session-runtime/services/hook-bus/event-runner.mjs +10 -2
  48. package/src/session-runtime/services/hook-bus/tool-gate.mjs +5 -2
  49. package/src/session-runtime/setup-tool/tool-defs.mjs +1 -1
  50. package/src/session-runtime/workflow-agents-api/agent-route.mjs +8 -5
  51. package/src/tui/app/doctor.mjs +78 -2
  52. package/src/tui/dist/index.mjs +7 -3
  53. package/scripts/lib/bench-sandbox.test.mjs +0 -54
  54. package/scripts/lib/cli-args.test.mjs +0 -64
  55. package/scripts/lib/embedding-model-bench-core.test.mjs +0 -47
  56. package/scripts/lib/smoke-loop-shared.test.mjs +0 -22
  57. package/scripts/lib/trace-row.test.mjs +0 -47
  58. package/scripts/lib/trace-stats.test.mjs +0 -62
@@ -14,11 +14,13 @@ import { isUncOrSmbPath, resolveSearchScope } from '../search-path-diagnostics.m
14
14
  import { buildGrepRgArgs } from '../search-builders.mjs';
15
15
  import { runRgWindowedLines } from '../native-search-runner.mjs';
16
16
  import { statReachable } from '../fs-reachability.mjs';
17
+ import { runSharedNativeScan } from './shared-native-scan.mjs';
17
18
  import { markScopedCacheIncomplete } from '../../../session/cache/scoped-cache-outcome.mjs';
18
19
  import { GREP_CONTEXT_MAX, hasUnsupportedRipgrepRegex } from '../arg-guard.mjs';
19
20
  import { resolveSearchWindow } from './search-input-helpers.mjs';
20
21
  import { formatGrepFanoutSections, formatGrepOutput, grepNoMatchesBody } from './grep-output.mjs';
21
22
  import { expandGrepAnchorContextOutput } from './grep-context-expander.mjs';
23
+ import { resolveGrepScope } from './grep-request.mjs';
22
24
 
23
25
  // Case-insensitive path keys on Windows, where rg echoes the operand path
24
26
  // in whatever case the caller wrote it.
@@ -30,12 +32,11 @@ function pathKey(value) {
30
32
  // The single-pattern request the combined path[] pass can answer, parsed
31
33
  // the way the canonical single-path parsing in search-grep-tool.mjs does
32
34
  // (keep in sync); null when the arguments fall outside its narrow
33
- // eligibility: single pattern, no glob/type, default flags, content-ish or
35
+ // eligibility: single pattern, no type, default flags, content-ish or
34
36
  // files_with_matches mode. MIXDOG_GREP_PATH_COMBINED=0 disables.
35
37
  function combinedPathRequest(args, defaultHeadLimit) {
36
38
  if (
37
39
  process.env.MIXDOG_GREP_PATH_COMBINED === '0' ||
38
- args.glob ||
39
40
  args.type ||
40
41
  args.multiline === true ||
41
42
  args['-o'] === true ||
@@ -81,8 +82,7 @@ function combinedPathRequest(args, defaultHeadLimit) {
81
82
  // order; null when one is a glob, unreachable, or on a share — the legacy
82
83
  // fan-out owns those diagnostics.
83
84
  async function resolvePathRoots(list, workDir) {
84
- const roots = [];
85
- for (const p of list) {
85
+ const roots = await Promise.all(list.map(async (p) => {
86
86
  const cleaned = normalizeInputPath(p);
87
87
  if (hasGlobMagic(cleaned)) return null;
88
88
  const resolved = resolveSearchScope(cleaned, workDir);
@@ -93,11 +93,21 @@ async function resolvePathRoots(list, workDir) {
93
93
  } catch {
94
94
  return null;
95
95
  }
96
- roots.push({
96
+ return {
97
97
  arg: p,
98
98
  abs: normalizeOutputPath(isAbsolute(resolved) ? await trueCasePath(resolved) : resolved),
99
99
  isDir: st.isDirectory(),
100
- });
100
+ };
101
+ }));
102
+ if (roots.some((root) => !root)) return null;
103
+ // An overlapping operand would scan a file twice and then attribute both
104
+ // copies to each containing section. Keep independent scope semantics.
105
+ for (let i = 0; i < roots.length; i++) {
106
+ const left = pathKey(roots[i].abs).replace(/\/+$/, '');
107
+ for (let j = i + 1; j < roots.length; j++) {
108
+ const right = pathKey(roots[j].abs).replace(/\/+$/, '');
109
+ if (left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`)) return null;
110
+ }
101
111
  }
102
112
  return roots;
103
113
  }
@@ -190,17 +200,23 @@ async function runCombinedPathFanout({ args, list, workDir, options, callContext
190
200
  const roots = await resolvePathRoots(list, workDir);
191
201
  if (!roots) return null;
192
202
  const { headLimit, offset } = request;
193
- const rgArgs = combinedPathRgArgs(args, request, roots, workDir);
203
+ const rgArgs = await combinedPathRgArgs(args, request, roots, workDir);
204
+ if (!rgArgs) return null;
194
205
  const perWindow = headLimit === Infinity ? 300 : offset + headLimit + 4;
195
206
  const cap = Math.min(4000, Math.max(400, perWindow * roots.length));
196
207
  let streamed;
197
208
  try {
198
- streamed = await runRgWindowedLines(
199
- rgArgs,
200
- { cwd: workDir, signal: options.signal },
201
- { offset: 0, limit: cap, summaryLimit: 0 }
202
- );
209
+ const runWindowedLines =
210
+ typeof options.__runRgWindowedLines === 'function' ? options.__runRgWindowedLines : runRgWindowedLines;
211
+ streamed = await runSharedNativeScan(runWindowedLines, {
212
+ cwd: workDir,
213
+ argv: rgArgs,
214
+ window: { offset: 0, limit: cap, summaryLimit: 0 },
215
+ scopes: roots.map((root) => root.abs),
216
+ signal: options.signal,
217
+ });
203
218
  } catch {
219
+ options.signal?.throwIfAborted();
204
220
  return null;
205
221
  }
206
222
  if (!streamed.complete || streamed.partial) return null;
@@ -211,26 +227,36 @@ async function runCombinedPathFanout({ args, list, workDir, options, callContext
211
227
  }
212
228
 
213
229
  // One rg invocation over every root's files at once.
214
- function combinedPathRgArgs(args, request, roots, workDir) {
215
- return buildGrepRgArgs({
216
- patterns: [request.pattern],
217
- includeNoise: args.include_noise === true,
218
- text: args.text === true,
219
- searchPath: workDir,
220
- globPatterns: [],
221
- outputMode: request.outMode,
222
- caseInsensitive: request.caseInsensitive,
223
- showLineNumbers: true,
224
- beforeN: null,
225
- afterN: null,
226
- contextN: null,
227
- multilineMode: false,
228
- fileType: '',
229
- onlyMatching: false,
230
- pcre2: false,
231
- withFilename: true,
232
- candidateFiles: roots.map((r) => r.abs),
233
- });
230
+ async function combinedPathRgArgs(args, request, roots, workDir) {
231
+ const perRoot = await Promise.all(roots.map(async (root) => {
232
+ const scope = await resolveGrepScope({ ...args, path: root.arg }, workDir);
233
+ if (scope.result !== undefined) return null;
234
+ return buildGrepRgArgs({
235
+ patterns: [request.pattern],
236
+ includeNoise: args.include_noise === true,
237
+ text: args.text === true,
238
+ searchPath: isAbsolute(scope.searchPath) ? root.abs : scope.searchPath,
239
+ // Match buildGrepScope: an explicit file operand wins over its glob.
240
+ globPatterns: root.isDir ? scope.normalizedGlobPatterns : [],
241
+ outputMode: request.outMode,
242
+ caseInsensitive: request.caseInsensitive,
243
+ showLineNumbers: true,
244
+ beforeN: null,
245
+ afterN: null,
246
+ contextN: null,
247
+ multilineMode: false,
248
+ fileType: '',
249
+ onlyMatching: false,
250
+ pcre2: false,
251
+ withFilename: true,
252
+ }).slice(0, -1);
253
+ }));
254
+ if (perRoot.some((argv) => !argv)) return null;
255
+ const first = JSON.stringify(perRoot[0]);
256
+ // Native filters are rooted at each operand. Only identical normalized
257
+ // rules (including explicit noise-directory exceptions) may share a scan.
258
+ if (perRoot.some((argv) => JSON.stringify(argv) !== first)) return null;
259
+ return [...perRoot[0], ...roots.map((root) => root.abs)];
234
260
  }
235
261
 
236
262
  // The per-root sections in argument order; roots without a hit collapse
@@ -252,6 +278,7 @@ async function renderCombinedPathSections(request, roots, byRoot, bodyContext) {
252
278
  // recursion bottoms out after one level. Results retain input order even
253
279
  // though every path starts immediately.
254
280
  export async function runGrepPathFanout(input) {
281
+ input.options.signal?.throwIfAborted();
255
282
  const combined = await runCombinedPathFanout(input);
256
283
  if (combined !== null) return combined;
257
284
  const { args, list, workDir, executeChildBuiltinTool, readStateScope, options, callContextCharBudget } = input;
@@ -9,6 +9,7 @@ import { GREP_AUTO_CONTEXT_LINES, trueCasePath } from '../path-utils.mjs';
9
9
  import { buildGrepRgArgs } from '../search-builders.mjs';
10
10
  import { runRgWindowedLines } from '../native-search-runner.mjs';
11
11
  import { statReachable } from '../fs-reachability.mjs';
12
+ import { runSharedNativeScan } from './shared-native-scan.mjs';
12
13
  import {
13
14
  dedupeFanoutMatchLines,
14
15
  formatGrepFanoutSections,
@@ -67,13 +68,15 @@ async function fanoutPrefilterCandidates(request) {
67
68
  multilineMode,
68
69
  withFilename: false,
69
70
  });
70
- const pre = await request.runWindowedLines(
71
- prefilterArgs,
72
- { cwd: scope.cwd, signal: options.signal },
71
+ const pre = await runSharedNativeScan(request.runWindowedLines, {
72
+ cwd: scope.cwd,
73
+ argv: prefilterArgs,
73
74
  // Whole-scope fallback pass: the broad admission lane keeps it
74
75
  // from competing with interactive searches for disk bandwidth.
75
- { offset: 0, limit: GREP_FANOUT_PREFILTER_FILE_CAP, summaryLimit: 0, bulkHint: true }
76
- );
76
+ window: { offset: 0, limit: GREP_FANOUT_PREFILTER_FILE_CAP, summaryLimit: 0, bulkHint: true },
77
+ scopes: [grepResolvedPath],
78
+ signal: options.signal,
79
+ });
77
80
  return pre.complete && !pre.partial ? pre.lines : null;
78
81
  } catch {
79
82
  options.signal?.throwIfAborted();
@@ -245,11 +248,13 @@ async function streamCombinedLines(request, scope) {
245
248
  const combinedCap = Math.min(4000, Math.max(400, perPatternWindow * patterns.length));
246
249
  const combinedBulkHint = request.normalizedGlobPatterns.length === 0 && !request.fileType;
247
250
  try {
248
- return await request.runWindowedLines(
249
- combinedArgs,
250
- { cwd: scope.cwd, signal: options.signal },
251
- { offset: 0, limit: combinedCap, summaryLimit: 0, bulkHint: combinedBulkHint }
252
- );
251
+ return await runSharedNativeScan(request.runWindowedLines, {
252
+ cwd: scope.cwd,
253
+ argv: combinedArgs,
254
+ window: { offset: 0, limit: combinedCap, summaryLimit: 0, bulkHint: combinedBulkHint },
255
+ scopes: [request.grepResolvedPath],
256
+ signal: options.signal,
257
+ });
253
258
  } catch {
254
259
  options.signal?.throwIfAborted();
255
260
  return null;
@@ -90,7 +90,7 @@ function rebaseAbsoluteGlob(glob, rootFwd) {
90
90
  };
91
91
  }
92
92
 
93
- async function resolveGrepScope(args, workDir) {
93
+ export async function resolveGrepScope(args, workDir) {
94
94
  let searchPath = args.path || '.';
95
95
  const rawGlobs = uniqueStrings(stringList(args.glob).map(normalizeInputPath));
96
96
  if (hasGlobMagic(searchPath)) {
@@ -0,0 +1,12 @@
1
+ // Shares only identical in-flight native scans (no persistent result). The
2
+ // existing single-flight layer isolates subscriber cancellation and detaches
3
+ // a scan after a mutation without interrupting its current readers.
4
+ import { runResultCacheInFlight } from '../cache-layers.mjs';
5
+
6
+ export function runSharedNativeScan(runWindowedLines, { cwd, argv, window, scopes, signal }) {
7
+ return runResultCacheInFlight(
8
+ `grep-path-scan:${JSON.stringify([cwd, argv, window])}`,
9
+ ({ signal: sharedSignal }) => runWindowedLines(argv, { cwd, signal: sharedSignal }, window),
10
+ { signal, scopes }
11
+ );
12
+ }
@@ -1,6 +1,8 @@
1
1
  import { open } from 'node:fs/promises';
2
2
  import { hashText } from './hash-utils.mjs';
3
+ import { runResultCacheInFlight } from './cache-layers.mjs';
3
4
  import {
5
+ canonicalCachePath,
4
6
  getReadRangeIndex,
5
7
  nearestReadRangeAnchor,
6
8
  maybeRecordReadRangeAnchor,
@@ -13,6 +15,18 @@ import { READ_MAX_SCAN_BYTES, READ_STREAM_TIMEOUT_MS } from './read-constants.mj
13
15
  export async function readSourceWindows(path, intervals, { signal } = {}) {
14
16
  signal?.throwIfAborted();
15
17
  if (!intervals.length) return new Map();
18
+ const snapshot = intervals.map(({ start, end }) => ({ start, end }));
19
+ const key = `source-windows:${canonicalCachePath(path)}:${JSON.stringify(snapshot.map((w) => [w.start, w.end]))}`;
20
+ const shared = await runResultCacheInFlight(
21
+ key,
22
+ ({ signal: sharedSignal }) => scanSourceWindows(path, snapshot, sharedSignal),
23
+ { signal, scopes: [path] }
24
+ );
25
+ return new Map(shared);
26
+ }
27
+
28
+ async function scanSourceWindows(path, intervals, signal) {
29
+ signal?.throwIfAborted();
16
30
  const handle = await open(path, 'r');
17
31
  try {
18
32
  const st = await handle.stat();
@@ -1,30 +1,30 @@
1
1
  {
2
- "version": "0.1.30",
2
+ "version": "0.1.31",
3
3
  "_comment": "Synced from immutable graph-v release assets.",
4
4
  "assets": {
5
5
  "darwin-arm64": {
6
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.30/mixdog-graph-darwin-arm64",
7
- "sha256": "cf306f716eeacb46b4a6a99b94ceb97f20be6facc7e4c6aa3274124e38a13efc"
6
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.31/mixdog-graph-darwin-arm64",
7
+ "sha256": "7fbbb99fd98da1fee7d3cdb90c73c9b4ec663a8ea108bbc0f7ee30e194571ed1"
8
8
  },
9
9
  "darwin-x64": {
10
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.30/mixdog-graph-darwin-x64",
11
- "sha256": "2ed1d6d98bee1db750c5b9df86ee2affa59f0afce28e2753772dca1dcdf9dd6d"
10
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.31/mixdog-graph-darwin-x64",
11
+ "sha256": "8cdbcdf3e207ead5ba274b87363ba1e8acc2c68d5912d95a29cd89cbecdc3c46"
12
12
  },
13
13
  "linux-arm64": {
14
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.30/mixdog-graph-linux-arm64",
15
- "sha256": "9963d0fb086bcd99341436458b207b6e521e9750bf75f817f0e6e99ebf7a30db"
14
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.31/mixdog-graph-linux-arm64",
15
+ "sha256": "704361e56ee605d6dd5edcb014e3cc0c3e11b7909639d5d8cacc9d75ad1134d2"
16
16
  },
17
17
  "linux-x64": {
18
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.30/mixdog-graph-linux-x64",
19
- "sha256": "79c6811c6bb3e05f4e048ebbe98bf28429e4b82763106c488d929668a2326cba"
18
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.31/mixdog-graph-linux-x64",
19
+ "sha256": "ef80b2bb372e98e5e71f11cf720fc709ec79f187d95c00bcef3b3c8d23f22f47"
20
20
  },
21
21
  "win32-arm64": {
22
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.30/mixdog-graph-win32-arm64.exe",
23
- "sha256": "8a1c8d623ee6e54948da2c77f5a8def5c9eafc3c6f2866c0c46c91bdaae393d0"
22
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.31/mixdog-graph-win32-arm64.exe",
23
+ "sha256": "8fe669f56b6bd17705cdd24e16e705a40dc4ffe210fad0e9692d51c365a9a072"
24
24
  },
25
25
  "win32-x64": {
26
- "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.30/mixdog-graph-win32-x64.exe",
27
- "sha256": "8d123e5044aee4b41e820361bd0dd1012a09a2da93a0074d21774602505784f4"
26
+ "url": "https://github.com/tribgames/mixdog/releases/download/graph-v0.1.31/mixdog-graph-win32-x64.exe",
27
+ "sha256": "ca2e4045fd4757b4b4e39de9d4df7790d9bcccc083ffca96a6f0cffbb1130f1d"
28
28
  }
29
29
  }
30
30
  }
@@ -2,6 +2,7 @@
2
2
  // target), then apply it in one shot under path locks with all-or-nothing
3
3
  // rollback.
4
4
  import { parsePatch } from 'diff';
5
+ import { throwIfAborted } from '../../../../../shared/abort-race.mjs';
5
6
  import { withBuiltinPathLocks } from '../../builtin.mjs';
6
7
  import { withAdvisoryLocks } from '../../builtin/advisory-lock.mjs';
7
8
  import { wrapPatchMutationOutput } from '../mutation-output.mjs';
@@ -155,7 +156,7 @@ export async function runCodexBatch({ batch, basePath, v4aConvertOpts, rejectedV
155
156
  // it whenever the batch fails — by returned Error text OR by a thrown error
156
157
  // (V4A rename, persistence) — so mode:"atomic" really is all-or-nothing
157
158
  // instead of leaving an earlier commit in place.
158
- export function applyCodexBatchWithRollback({ batch, basePath, dryRun, readStateScope, options, runBatch }) {
159
+ export function applyCodexBatchWithRollback({ batch, basePath, dryRun, readStateScope, abortSignal, options, runBatch }) {
159
160
  const { lockPaths, v4aRenamePlan } = batch;
160
161
  const registerUiDiff = (rollbackSnapshots) =>
161
162
  registerCommittedPatchUiDiff({
@@ -168,6 +169,7 @@ export function applyCodexBatchWithRollback({ batch, basePath, dryRun, readState
168
169
  });
169
170
  return withBuiltinPathLocks(lockPaths, () =>
170
171
  withAdvisoryLocks(lockPaths, async () => {
172
+ throwIfAborted(abortSignal);
171
173
  let rollbackSnapshots = [];
172
174
  if (!dryRun) {
173
175
  try {
@@ -8,6 +8,7 @@ import { countLabel, formatNativeSummary, kindLabel, traceNativeApply } from './
8
8
  import { unlink } from 'node:fs/promises';
9
9
  import { dirname as pathDirname } from 'node:path';
10
10
  import { performance } from 'node:perf_hooks';
11
+ import { throwIfAborted } from '../../../../shared/abort-race.mjs';
11
12
  import {
12
13
  normalizeOutputPath,
13
14
  invalidateBuiltinResultCache,
@@ -154,6 +155,7 @@ export async function dispatchNativePatch({
154
155
  dryRun,
155
156
  fuzzy: fuzz > 0,
156
157
  readStateScope,
158
+ signal,
157
159
  });
158
160
  if (!/^Error:/i.test(String(recovered || '').trimStart())) {
159
161
  return `${recovered}\n[native transport unavailable; create-only patch completed with the JS engine]`;
@@ -326,19 +328,19 @@ function findParsedForRow(row, parsed, basePath) {
326
328
  return null;
327
329
  }
328
330
 
329
- async function applyJsParsedEntry(entry, basePath, { dryRun, fuzzy, readStateScope }) {
331
+ async function applyJsParsedEntry(entry, basePath, { dryRun, fuzzy, readStateScope, signal }) {
330
332
  const kind = classifyEntry(entry);
331
333
  const headerName = entryHeaderName(entry, kind);
332
334
  const fullPath = resolveEntryPath(basePath, headerName);
333
335
  const displayPath = normalizeOutputPath(stripDiffPrefix(headerName));
334
- if (kind === 'create') await applyJsCreateEntry(entry, fullPath, displayPath, { dryRun, readStateScope });
335
- else if (kind === 'delete') await applyJsDeleteEntry(fullPath, displayPath, { dryRun, readStateScope });
336
- else await applyJsUpdateEntry(entry, fullPath, displayPath, { dryRun, fuzzy, readStateScope });
336
+ if (kind === 'create') await applyJsCreateEntry(entry, fullPath, displayPath, { dryRun, readStateScope, signal });
337
+ else if (kind === 'delete') await applyJsDeleteEntry(fullPath, displayPath, { dryRun, readStateScope, signal });
338
+ else await applyJsUpdateEntry(entry, fullPath, displayPath, { dryRun, fuzzy, readStateScope, signal });
337
339
  const { added, removed } = countHunkChanges(entry.hunks);
338
340
  return { kind, fullPath, displayPath, added, removed };
339
341
  }
340
342
 
341
- async function applyJsCreateEntry(entry, fullPath, displayPath, { dryRun, readStateScope }) {
343
+ async function applyJsCreateEntry(entry, fullPath, displayPath, { dryRun, readStateScope, signal }) {
342
344
  // Add File is create-only. atomicWrite checks the absent-target
343
345
  // expectation again so a file appearing after preflight is not replaced.
344
346
  assertAddTargetAbsent(fullPath, displayPath);
@@ -350,9 +352,11 @@ async function applyJsCreateEntry(entry, fullPath, displayPath, { dryRun, readSt
350
352
  }
351
353
  const content = joinTextLinesForPatch(addedLines);
352
354
  if (dryRun) return;
355
+ throwIfAborted(signal);
353
356
  mkdirSync(pathDirname(fullPath), { recursive: true });
354
357
  try {
355
358
  await atomicWrite(fullPath, content, {
359
+ signal,
356
360
  sessionId: readStateScope,
357
361
  expectedTargetSnapshot: { exists: false },
358
362
  });
@@ -369,16 +373,17 @@ async function applyJsCreateEntry(entry, fullPath, displayPath, { dryRun, readSt
369
373
  recordReadSnapshotForPath(fullPath, readStateScope, { source: 'apply_patch_js', isPartialView: false });
370
374
  }
371
375
 
372
- async function applyJsDeleteEntry(fullPath, displayPath, { dryRun, readStateScope }) {
376
+ async function applyJsDeleteEntry(fullPath, displayPath, { dryRun, readStateScope, signal }) {
373
377
  lstatRegularPatchFile(fullPath, displayPath);
374
378
  if (dryRun) return;
379
+ throwIfAborted(signal);
375
380
  await unlink(fullPath);
376
381
  invalidateBuiltinResultCache([fullPath]);
377
382
  markCodeGraphDirtyPaths([fullPath]);
378
383
  clearReadSnapshotForPath(fullPath, readStateScope);
379
384
  }
380
385
 
381
- async function applyJsUpdateEntry(entry, fullPath, displayPath, { dryRun, fuzzy, readStateScope }) {
386
+ async function applyJsUpdateEntry(entry, fullPath, displayPath, { dryRun, fuzzy, readStateScope, signal }) {
382
387
  const preMutationStat = lstatRegularPatchFile(fullPath, displayPath);
383
388
  const { writePath, snapshotStat } = resolvePatchWriteTarget(fullPath, displayPath, preMutationStat);
384
389
  // Rewrite path: decode by BOM, re-encode into the same codec, and keep every
@@ -393,11 +398,13 @@ async function applyJsUpdateEntry(entry, fullPath, displayPath, { dryRun, fuzzy,
393
398
  });
394
399
  const content = encodePatchTargetContent(joinTextLinesForPatch(updatedLines), rawEnc);
395
400
  if (dryRun) return;
401
+ throwIfAborted(signal);
396
402
  // The expected-target snapshot closes the read→write window: if anything
397
403
  // rewrote this file after we read it, the write fails instead of silently
398
404
  // clobbering it (and instead of inheriting a stale "body delivered" claim).
399
405
  try {
400
406
  await atomicWrite(writePath, content, {
407
+ signal,
401
408
  sessionId: readStateScope,
402
409
  expectedTargetSnapshot: snapshotStat
403
410
  ? {
@@ -427,13 +434,14 @@ async function applyJsUpdateEntry(entry, fullPath, displayPath, { dryRun, fuzzy,
427
434
  });
428
435
  }
429
436
 
430
- export async function dispatchJsPatchEntries({ rows, parsed, basePath, dryRun, fuzzy, readStateScope }) {
437
+ export async function dispatchJsPatchEntries({ rows, parsed, basePath, dryRun, fuzzy, readStateScope, signal }) {
431
438
  const applied = [];
432
439
  for (const row of rows || []) {
433
440
  const entry = findParsedForRow(row, parsed, basePath);
434
441
  if (!entry) throw new Error(`apply_patch: missing parsed entry for ${row.displayPath}`);
435
442
  try {
436
- applied.push(await applyJsParsedEntry(entry, basePath, { dryRun, fuzzy, readStateScope }));
443
+ throwIfAborted(signal);
444
+ applied.push(await applyJsParsedEntry(entry, basePath, { dryRun, fuzzy, readStateScope, signal }));
437
445
  } catch (err) {
438
446
  return `Error: ${err?.message || String(err)}`;
439
447
  }
@@ -66,6 +66,7 @@ async function apply_patch(rawArgs, cwd, options = {}) {
66
66
  basePath,
67
67
  dryRun,
68
68
  readStateScope,
69
+ abortSignal,
69
70
  options,
70
71
  runBatch: () =>
71
72
  runCodexBatch({
@@ -2,6 +2,7 @@
2
2
  // JS writer split. Both the Codex batch and the ordered sequence share these
3
3
  // apply semantics.
4
4
  import { parsePatch } from 'diff';
5
+ import { throwIfAborted } from '../../../../shared/abort-race.mjs';
5
6
  import { symlinkWriteTarget } from '../builtin/atomic-write.mjs';
6
7
  import { dispatchNativePatch, dispatchJsPatchEntries } from './dispatch.mjs';
7
8
  import { patchTargetUsesUtf16 } from './matcher.mjs';
@@ -33,6 +34,7 @@ export function parseConvertedUnifiedPatch(unified) {
33
34
  // whether earlier waves already committed to disk.
34
35
  export async function applyParsedWave({ parsed: wparsed, entries: wentries, headerRewrites: whr }, basePath, opts) {
35
36
  const { fuzz, rejectPartial, dryRun, fuzzy, readStateScope, abortSignal } = opts;
37
+ throwIfAborted(abortSignal);
36
38
  // Create entries use the JS atomic writer even inside the base path. Its
37
39
  // expected-absent snapshot makes Add File create-only under external races;
38
40
  // the native patch engine intentionally supports overwrite-style additions.
@@ -47,6 +49,7 @@ export async function applyParsedWave({ parsed: wparsed, entries: wentries, head
47
49
  // binary that predates this build's byte-fidelity contract is not used at
48
50
  // all — everything takes the JS writer instead.
49
51
  const engineContractOk = await nativePatchSessionSatisfiesContract();
52
+ throwIfAborted(abortSignal);
50
53
  // Codec routing only concerns entries that REWRITE bytes; a delete does not.
51
54
  // An unverified engine, however, must receive no work at all — deletes
52
55
  // included — so the whole wave takes the JS writer.
@@ -117,6 +120,7 @@ export async function applyParsedWave({ parsed: wparsed, entries: wentries, head
117
120
  dryRun,
118
121
  fuzzy,
119
122
  readStateScope,
123
+ signal: abortSignal,
120
124
  });
121
125
  if (isPatchErrorText(jsResult)) return { executor, error: jsResult };
122
126
  resultParts.push(jsResult);
@@ -404,7 +404,7 @@ export const BROWSER_INPUT_FIELDS = {
404
404
  background: {
405
405
  type: 'boolean',
406
406
  description:
407
- 'true creates/targets a hidden support page; false reveals it; omitted never promotes a named one. open without true keeps it for user handoff.',
407
+ 'true creates/targets a hidden support page; false reveals it until task end; omitted never promotes a named one. open without true keeps it for user handoff.',
408
408
  },
409
409
  },
410
410
  };
@@ -22,7 +22,7 @@ import { BROWSER_INPUT_FIELDS } from './input-fields.mjs';
22
22
  const BROWSER_DESCRIPTION =
23
23
  "Drive Mixdog's in-app Chromium directly for rendered, signed-in or interactive pages; public page text uses web_fetch, external windows computer. " +
24
24
  'Load the browser-use skill before first use. ' +
25
- 'Background for results; open retains a handoff, background:false reveals temporarily. Respect user-closed panels. ' +
25
+ 'Background by default; reveal only via open for a deliverable or handoff. Respect user-closed panels. ' +
26
26
  'Task end cleans owned pages/restores temporary panels; user pages persist. ' +
27
27
  'hide folds the panel; close_tab closes named background pages only. ' +
28
28
  'Pages are session-local; sign-in/storage shared; never provide session_id. ' +
@@ -72,19 +72,6 @@ for (const name of [
72
72
  'key',
73
73
  ].includes(name),
74
74
  focusGuard: ['invoke', 'set_value', 'toggle'].includes(name),
75
- focusContinuation: [
76
- 'invoke',
77
- 'click',
78
- 'double_click',
79
- 'right_click',
80
- 'middle_click',
81
- 'triple_click',
82
- 'mouse_down',
83
- 'mouse_up',
84
- 'mouse_move',
85
- 'drag',
86
- 'scroll',
87
- ].includes(name),
88
75
  policy: 'act',
89
76
  });
90
77
  }
@@ -11,6 +11,7 @@ import { randomUUID } from 'node:crypto';
11
11
  import { resolveAntigravityAuth } from '../auth.mjs';
12
12
  import { timeoutSignal } from '../bounded-signal.mjs';
13
13
  import { upstreamError } from '../upstream-error.mjs';
14
+ import { geminiUsage } from '../media-usage.mjs';
14
15
  import { geminiImageRequestBody, pickGeminiImagePart } from './gemini-image.mjs';
15
16
  import { CONTENT_ENDPOINTS, antigravityHeaders } from '../../agent/orchestrator/providers/antigravity-oauth-tokens.mjs';
16
17
  import { frameAndParseSse } from '../../agent/orchestrator/providers/lib/sse-framing.mjs';
@@ -35,6 +36,7 @@ function antigravityImageRequestBody({ projectId, model, prompt, options = {}, r
35
36
  export function antigravityImageParts(sseText) {
36
37
  const parts = [];
37
38
  let failure = null;
39
+ let usageMetadata = null;
38
40
  for (const event of frameAndParseSse(sseText).events) {
39
41
  if (event.error) continue;
40
42
  const value = event.value;
@@ -43,10 +45,12 @@ export function antigravityImageParts(sseText) {
43
45
  const chunk = value?.response && typeof value.response === 'object' ? value.response : value;
44
46
  const candidate = chunk?.candidates?.[0];
45
47
  parts.push(...(candidate?.content?.parts || []));
48
+ // usageMetadata is cumulative; the last chunk that carries it is the total.
49
+ if (chunk?.usageMetadata) usageMetadata = chunk.usageMetadata;
46
50
  const blocked = chunk?.promptFeedback?.blockReason;
47
51
  if (blocked && !failure) failure = `prompt blocked (${blocked})`;
48
52
  }
49
- return { parts, failure };
53
+ return { parts, failure, usage: geminiUsage(usageMetadata) };
50
54
  }
51
55
 
52
56
  export async function generateImage(
@@ -68,6 +72,6 @@ export async function generateImage(
68
72
  signal: timeoutSignal(signal, REQUEST_TIMEOUT_MS),
69
73
  });
70
74
  if (!res.ok) throw upstreamError('Antigravity image', res.status, await res.text().catch(() => ''));
71
- const { parts, failure } = antigravityImageParts(await res.text());
72
- return pickGeminiImagePart(parts, 'Antigravity', failure);
75
+ const { parts, failure, usage } = antigravityImageParts(await res.text());
76
+ return pickGeminiImagePart(parts, 'Antigravity', failure, usage);
73
77
  }
@@ -12,6 +12,7 @@ import { timeoutSignal } from '../bounded-signal.mjs';
12
12
  import { decodeBase64Media } from '../download.mjs';
13
13
  import { mediaError } from '../lanes.mjs';
14
14
  import { upstreamError } from '../upstream-error.mjs';
15
+ import { responsesUsage, withReportedUsage } from '../media-usage.mjs';
15
16
  import { CODEX_OAUTH_ORIGINATOR, CODEX_RESPONSES_URL } from '../../agent/orchestrator/providers/openai-oauth.mjs';
16
17
  import {
17
18
  codexUserAgent,
@@ -32,6 +33,12 @@ function imageTool(options = {}) {
32
33
 
33
34
  function collectImage(event, state) {
34
35
  const item = event?.item;
36
+ // Terminal Responses events carry the orchestrator's token usage. It does
37
+ // not include the hosted image tool's own tokens.
38
+ if (['response.completed', 'response.failed', 'response.incomplete'].includes(event?.type)) {
39
+ state.usage = responsesUsage(event?.response?.usage) || state.usage;
40
+ return;
41
+ }
35
42
  if (event?.type === 'response.output_item.done' && item?.type === 'image_generation_call') {
36
43
  if (typeof item.result === 'string' && item.result.length > 0) state.final = item.result;
37
44
  if (typeof item.model === 'string') state.actualModel = item.model;
@@ -142,12 +149,16 @@ export async function generateImage(
142
149
 
143
150
  const b64 = state.final || state.partial;
144
151
  if (!b64) {
145
- throw mediaError(`ChatGPT returned no image data${failure ? `: ${failure}` : ''}`, 'MEDIA_EMPTY_RESULT', 502);
152
+ throw withReportedUsage(
153
+ mediaError(`ChatGPT returned no image data${failure ? `: ${failure}` : ''}`, 'MEDIA_EMPTY_RESULT', 502),
154
+ state.usage
155
+ );
146
156
  }
147
157
  return {
148
158
  bytes: decodeBase64Media(b64, 'ChatGPT image'),
149
159
  mime: 'image/png',
150
160
  revisedPrompt: state.revisedPrompt,
151
161
  actualModel: state.actualModel || null,
162
+ usage: state.usage || null,
152
163
  };
153
164
  }
@@ -11,6 +11,7 @@ import { timeoutSignal } from '../bounded-signal.mjs';
11
11
  import { decodeBase64Media } from '../download.mjs';
12
12
  import { mediaError } from '../lanes.mjs';
13
13
  import { upstreamError } from '../upstream-error.mjs';
14
+ import { geminiUsage, withReportedUsage } from '../media-usage.mjs';
14
15
 
15
16
  const BASE_URL = 'https://generativelanguage.googleapis.com/v1beta/models';
16
17
  const REQUEST_TIMEOUT_MS = 180_000;
@@ -51,20 +52,25 @@ async function post(model, key, body, signal, fetchFn) {
51
52
  * First inline image among a candidate's parts. A text-only answer is the
52
53
  * model's refusal and surfaces as such; it is never retried.
53
54
  */
54
- export function pickGeminiImagePart(parts, label, failure = null) {
55
+ export function pickGeminiImagePart(parts, label, failure = null, usage = null) {
55
56
  const image = parts.find((part) => part?.inlineData?.data);
56
57
  if (!image) {
57
58
  const refusal = failure || parts.find((part) => typeof part?.text === 'string' && part.text.trim())?.text || '';
58
- throw mediaError(
59
- `${label} returned no image data${refusal ? `: ${String(refusal).slice(0, 200)}` : ''}`,
60
- 'MEDIA_EMPTY_RESULT',
61
- 502
59
+ throw withReportedUsage(
60
+ mediaError(
61
+ `${label} returned no image data${refusal ? `: ${String(refusal).slice(0, 200)}` : ''}`,
62
+ 'MEDIA_EMPTY_RESULT',
63
+ 502
64
+ ),
65
+ usage
62
66
  );
63
67
  }
64
68
  return {
65
69
  bytes: decodeBase64Media(image.inlineData.data, `${label} image`),
66
70
  mime: image.inlineData.mimeType || 'image/png',
67
71
  revisedPrompt: null,
72
+ // Without reported tokens the one image is priced per image, if the catalog lists it.
73
+ usage: usage || { images: 1 },
68
74
  };
69
75
  }
70
76
 
@@ -76,5 +82,10 @@ export async function generateImage(
76
82
  const res = await post(model, key, geminiImageRequestBody(prompt, options, references), signal, fetchFn);
77
83
  if (!res.ok) throw upstreamError('Gemini image', res.status, await res.text().catch(() => ''));
78
84
  const data = await res.json();
79
- return pickGeminiImagePart(data?.candidates?.[0]?.content?.parts || [], 'Gemini');
85
+ return pickGeminiImagePart(
86
+ data?.candidates?.[0]?.content?.parts || [],
87
+ 'Gemini',
88
+ null,
89
+ geminiUsage(data?.usageMetadata)
90
+ );
80
91
  }