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.
- package/README.md +8 -0
- package/package.json +2 -2
- package/src/defaults/skills/browser-use/SKILL.md +14 -6
- package/src/defaults/skills/setup/references/actions.md +6 -3
- package/src/defaults/skills/setup/references/surfaces.md +8 -4
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +40 -99
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +6 -1
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +7 -3
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +18 -38
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-path-fanout.mjs +59 -32
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +15 -10
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-request.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shared-native-scan.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +17 -9
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/patch/wave.mjs +4 -0
- package/src/runtime/browser-bridge/input-fields.mjs +1 -1
- package/src/runtime/browser-bridge/tool-defs.mjs +1 -1
- package/src/runtime/computer-bridge/actions.mjs +0 -13
- package/src/runtime/media/adapters/antigravity-image.mjs +7 -3
- package/src/runtime/media/adapters/codex-image.mjs +12 -1
- package/src/runtime/media/adapters/gemini-image.mjs +17 -6
- package/src/runtime/media/adapters/gemini-video.mjs +19 -9
- package/src/runtime/media/adapters/xai-media.mjs +20 -7
- package/src/runtime/media/jobs.mjs +35 -3
- package/src/runtime/media/media-usage.mjs +126 -0
- package/src/runtime/media/tool.mjs +16 -4
- package/src/runtime/shared/llm/cost.mjs +50 -3
- package/src/runtime/shared/llm/model-catalog-projection.mjs +7 -2
- package/src/runtime/shared/llm/model-catalog.mjs +2 -1
- package/src/runtime/shared/llm/model-pricing-rates.mjs +21 -0
- package/src/runtime/shared/llm/usage-accounting.mjs +25 -5
- package/src/runtime/shared/llm/usage-context.mjs +6 -0
- package/src/runtime/shared/llm/usage-ledger-import.mjs +1 -0
- package/src/session-runtime/cwd-plugins/plugin-status.mjs +24 -4
- package/src/session-runtime/internal-tool-executor/feature-tools.mjs +9 -1
- package/src/session-runtime/services/agent-tool/index-write-queue.mjs +38 -6
- package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +9 -2
- package/src/session-runtime/services/agent-tool/worker-index.mjs +6 -3
- package/src/session-runtime/services/hook-bus/event-runner.mjs +10 -2
- package/src/session-runtime/services/hook-bus/tool-gate.mjs +5 -2
- package/src/session-runtime/setup-tool/tool-defs.mjs +1 -1
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +8 -5
- package/src/tui/app/doctor.mjs +78 -2
- package/src/tui/dist/index.mjs +7 -3
- package/scripts/lib/bench-sandbox.test.mjs +0 -54
- package/scripts/lib/cli-args.test.mjs +0 -64
- package/scripts/lib/embedding-model-bench-core.test.mjs +0 -47
- package/scripts/lib/smoke-loop-shared.test.mjs +0 -22
- package/scripts/lib/trace-row.test.mjs +0 -47
- 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
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
250
|
-
|
|
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.
|
|
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.
|
|
7
|
-
"sha256": "
|
|
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.
|
|
11
|
-
"sha256": "
|
|
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.
|
|
15
|
-
"sha256": "
|
|
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.
|
|
19
|
-
"sha256": "
|
|
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.
|
|
23
|
-
"sha256": "
|
|
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.
|
|
27
|
-
"sha256": "
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
|
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
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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(
|
|
85
|
+
return pickGeminiImagePart(
|
|
86
|
+
data?.candidates?.[0]?.content?.parts || [],
|
|
87
|
+
'Gemini',
|
|
88
|
+
null,
|
|
89
|
+
geminiUsage(data?.usageMetadata)
|
|
90
|
+
);
|
|
80
91
|
}
|