mixdog 1.0.2 → 1.0.4

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 (46) hide show
  1. package/README.md +8 -0
  2. package/package.json +1 -1
  3. package/src/defaults/skills/browser-use/SKILL.md +14 -6
  4. package/src/defaults/skills/goal-management/SKILL.md +2 -2
  5. package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +10 -1
  6. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +1 -1
  7. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +40 -99
  8. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +6 -1
  9. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +2 -0
  10. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +7 -3
  11. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +18 -38
  12. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-path-fanout.mjs +59 -32
  13. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +15 -10
  14. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-request.mjs +1 -1
  15. package/src/runtime/agent/orchestrator/tools/builtin/lib/shared-native-scan.mjs +12 -0
  16. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +14 -0
  17. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  18. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +3 -1
  19. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +17 -9
  20. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -0
  21. package/src/runtime/agent/orchestrator/tools/patch/wave.mjs +4 -0
  22. package/src/runtime/browser-bridge/input-fields.mjs +1 -1
  23. package/src/runtime/browser-bridge/tool-defs.mjs +1 -1
  24. package/src/runtime/computer-bridge/actions.mjs +0 -13
  25. package/src/runtime/media/adapters/antigravity-image.mjs +7 -3
  26. package/src/runtime/media/adapters/codex-image.mjs +12 -1
  27. package/src/runtime/media/adapters/gemini-image.mjs +17 -6
  28. package/src/runtime/media/adapters/gemini-video.mjs +19 -9
  29. package/src/runtime/media/adapters/xai-media.mjs +20 -7
  30. package/src/runtime/media/jobs.mjs +35 -3
  31. package/src/runtime/media/media-usage.mjs +126 -0
  32. package/src/runtime/media/tool.mjs +16 -4
  33. package/src/runtime/shared/llm/cost.mjs +50 -3
  34. package/src/runtime/shared/llm/model-catalog-projection.mjs +7 -2
  35. package/src/runtime/shared/llm/model-catalog.mjs +2 -1
  36. package/src/runtime/shared/llm/model-pricing-rates.mjs +21 -0
  37. package/src/runtime/shared/llm/usage-accounting.mjs +25 -5
  38. package/src/runtime/shared/llm/usage-context.mjs +6 -0
  39. package/src/runtime/shared/llm/usage-ledger-import.mjs +1 -0
  40. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +9 -1
  41. package/src/session-runtime/services/agent-tool/index-write-queue.mjs +38 -6
  42. package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +9 -2
  43. package/src/session-runtime/services/agent-tool/worker-index.mjs +6 -3
  44. package/src/session-runtime/services/hook-bus/event-runner.mjs +10 -2
  45. package/src/session-runtime/services/hook-bus/tool-gate.mjs +5 -2
  46. package/src/tui/dist/index.mjs +7 -3
@@ -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
  }
@@ -12,7 +12,9 @@ import { boundedSignal } from '../bounded-signal.mjs';
12
12
  import { decodeBase64Media, downloadGeminiMedia } from '../download.mjs';
13
13
  import { mediaError } from '../lanes.mjs';
14
14
  import { upstreamError } from '../upstream-error.mjs';
15
+ import { interactionsUsage, withReportedUsage } from '../media-usage.mjs';
15
16
 
17
+ const VEO_DEFAULT_SECONDS = 8;
16
18
  const BASE_URL = 'https://generativelanguage.googleapis.com/v1beta';
17
19
  const OMNI_TIMEOUT_MS = 600_000;
18
20
  const POLL_INTERVAL_MS = 8_000;
@@ -54,14 +56,15 @@ async function generateViaOmni({ model, prompt, options, references = [], signal
54
56
  });
55
57
  if (!res.ok) throw upstreamError('Gemini Omni video', res.status, await res.text().catch(() => ''));
56
58
  const data = await res.json();
59
+ const usage = interactionsUsage(data?.usage);
57
60
  for (const step of data?.steps || []) {
58
61
  const content = Array.isArray(step?.content) ? step.content : [step?.content].filter(Boolean);
59
62
  const video = content.find((item) => item?.type === 'video' && typeof item?.data === 'string');
60
63
  if (video) {
61
- return { bytes: decodeBase64Media(video.data, 'Gemini Omni video'), mime: video.mime_type || 'video/mp4' };
64
+ return { bytes: decodeBase64Media(video.data, 'Gemini Omni video'), mime: video.mime_type || 'video/mp4', usage };
62
65
  }
63
66
  }
64
- throw mediaError('Gemini Omni returned no video data', 'MEDIA_EMPTY_RESULT', 502);
67
+ throw withReportedUsage(mediaError('Gemini Omni returned no video data', 'MEDIA_EMPTY_RESULT', 502), usage);
65
68
  }
66
69
 
67
70
  async function generateViaVeo({ model, prompt, options, references = [], signal, onProgress, key }) {
@@ -114,13 +117,20 @@ async function generateViaVeo({ model, prompt, options, references = [], signal,
114
117
  const sample = data?.response?.generateVideoResponse?.generatedSamples?.[0] || data?.response?.generatedVideos?.[0];
115
118
  const uri = sample?.video?.uri || sample?.video?.fileUri;
116
119
  if (!uri) throw mediaError('Veo finished without a video URI', 'MEDIA_EMPTY_RESULT', 502);
117
- return {
118
- // The download is part of the generation budget: unbounded, it held an
119
- // active job slot (and the upstream connection) open indefinitely after
120
- // the poll loop finished.
121
- bytes: await downloadGeminiMedia(uri, { key, signal: boundedSignal(signal, deadline, TOTAL_TIMEOUT_MS) }),
122
- mime: 'video/mp4',
123
- };
120
+ // Veo reports no usage; it bills per generated second. The response does
121
+ // not echo the length, so the requested one (default 8s) is the billed one.
122
+ // A failed download still leaves the generated seconds billed.
123
+ const usage = { seconds: parameters.durationSeconds || VEO_DEFAULT_SECONDS, resolution: parameters.resolution || '720p' };
124
+ // The download is part of the generation budget: unbounded, it held an
125
+ // active job slot (and the upstream connection) open indefinitely after
126
+ // the poll loop finished.
127
+ const bytes = await downloadGeminiMedia(uri, {
128
+ key,
129
+ signal: boundedSignal(signal, deadline, TOTAL_TIMEOUT_MS),
130
+ }).catch((error) => {
131
+ throw withReportedUsage(error, usage);
132
+ });
133
+ return { bytes, mime: 'video/mp4', usage };
124
134
  }
125
135
  }
126
136
 
@@ -10,6 +10,7 @@ import { boundedSignal, timeoutSignal } from '../bounded-signal.mjs';
10
10
  import { decodeBase64Media, downloadPublicMedia } from '../download.mjs';
11
11
  import { mediaError } from '../lanes.mjs';
12
12
  import { upstreamError } from '../upstream-error.mjs';
13
+ import { withReportedUsage, xaiUsage } from '../media-usage.mjs';
13
14
 
14
15
  const POLL_INTERVAL_MS = 4_000;
15
16
  const START_TIMEOUT_MS = 60_000;
@@ -72,6 +73,7 @@ export async function generateImage({ lane, model, prompt, options = {}, referen
72
73
  bytes: decodeBase64Media(entry.b64_json, 'xAI image'),
73
74
  mime: entry.mime_type || 'image/png',
74
75
  revisedPrompt: entry.revised_prompt || null,
76
+ usage: xaiUsage(data?.usage),
75
77
  };
76
78
  }
77
79
 
@@ -114,18 +116,29 @@ export async function generateVideo({ lane, model, prompt, options = {}, referen
114
116
  if (typeof data?.progress === 'number' && typeof onProgress === 'function') onProgress(data.progress);
115
117
  if (data?.status === 'done') {
116
118
  const url = data?.video?.url;
117
- if (!url) throw mediaError('xAI video finished without a URL', 'MEDIA_EMPTY_RESULT', 502);
119
+ // The billed cost (when reported) is the price; seconds + resolution
120
+ // let the catalog's per-second rate price it otherwise.
121
+ const seconds = Number(data?.video?.duration) || duration;
122
+ const usage = { ...xaiUsage(data?.usage), seconds, resolution };
123
+ if (!url) throw withReportedUsage(mediaError('xAI video finished without a URL', 'MEDIA_EMPTY_RESULT', 502), usage);
124
+ const bytes = await downloadPublicMedia(url, { signal, label: 'xAI video' }).catch((error) => {
125
+ throw withReportedUsage(error, usage);
126
+ });
118
127
  return {
119
- bytes: await downloadPublicMedia(url, { signal, label: 'xAI video' }),
128
+ bytes,
120
129
  mime: 'video/mp4',
121
- durationSeconds: Number(data?.video?.duration) || duration,
130
+ durationSeconds: seconds,
131
+ usage,
122
132
  };
123
133
  }
124
134
  if (data?.status === 'failed' || data?.status === 'expired') {
125
- throw mediaError(
126
- `xAI video ${data.status}${data?.error?.code ? `: ${data.error.code}` : ''}`,
127
- 'MEDIA_UPSTREAM_FAILED',
128
- 502
135
+ throw withReportedUsage(
136
+ mediaError(
137
+ `xAI video ${data.status}${data?.error?.code ? `: ${data.error.code}` : ''}`,
138
+ 'MEDIA_UPSTREAM_FAILED',
139
+ 502
140
+ ),
141
+ xaiUsage(data?.usage)
129
142
  );
130
143
  }
131
144
  }