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
package/README.md
CHANGED
|
@@ -487,6 +487,14 @@ npm install
|
|
|
487
487
|
npm run dev
|
|
488
488
|
```
|
|
489
489
|
|
|
490
|
+
Desktop development uses a fresh, isolated test profile on each launch, including
|
|
491
|
+
its data, daemon and tool connections. It does not copy the installed app's
|
|
492
|
+
settings or sign-ins. The profile path is printed and retained in the system
|
|
493
|
+
temporary directory for diagnosis. CDP uses port `9342`; if it is occupied, reuse
|
|
494
|
+
the running test app or choose another port with `npm run dev -- --port 9343`.
|
|
495
|
+
On Windows, `npm run e2e:direct` and `npm run e2e:direct:source` also use isolated
|
|
496
|
+
profiles and port `9342` (override with `-- -Port 9343`).
|
|
497
|
+
|
|
490
498
|
Both packages discover `*.test.mjs` and `*-test.mjs` under their `src/` and
|
|
491
499
|
`scripts/` directories. Fast, slow, and live tests run in separate lanes;
|
|
492
500
|
live checks need their corresponding built artifacts or services. See
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone mixdog coding-agent CLI/TUI workspace.",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"!scripts/bench/",
|
|
32
32
|
"!scripts/recall-bench-*.txt",
|
|
33
33
|
"!scripts/*-test.mjs",
|
|
34
|
-
"!scripts
|
|
34
|
+
"!scripts/**/*.test.mjs",
|
|
35
35
|
"!scripts/tool-contracts/",
|
|
36
36
|
"!scripts/provider-toolcall/",
|
|
37
37
|
"!scripts/session-transport/",
|
|
@@ -159,12 +159,20 @@ not as two blocks joined.
|
|
|
159
159
|
|
|
160
160
|
## Foreground vs background
|
|
161
161
|
|
|
162
|
-
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
162
|
+
- Default to named `background:true` pages; the panel stays closed and the
|
|
163
|
+
user's current page stays intact. If the answer can be given in the
|
|
164
|
+
conversation, keep the page hidden even when it produced the answer;
|
|
165
|
+
screenshots, reports, and result-only actions never need a reveal.
|
|
166
|
+
- Reveal a page only with `open` and its `tab`, and only when the page itself
|
|
167
|
+
is what the user needs after the turn:
|
|
168
|
+
- **Deliverable** — the user asked to see, show, or keep a page open, or the
|
|
169
|
+
page is the output: an edited document or dashboard, a cart or checkout,
|
|
170
|
+
a submitted form's result.
|
|
171
|
+
- **Handoff** — the user must act on the page: sign-in, CAPTCHA, 2FA,
|
|
172
|
+
payment, approval, or an unfinished workflow they continue.
|
|
173
|
+
- When the user refers to the page already visible in this session's panel,
|
|
174
|
+
act on that page instead of opening another.
|
|
175
|
+
- Later calls may omit `background`: naming a support page does not promote it.
|
|
168
176
|
- Background pages run concurrently, so independent background work can share
|
|
169
177
|
one assistant turn.
|
|
170
178
|
- Pages, tabs, URLs, and targets are session-local; sign-in state, cookies,
|
|
@@ -64,11 +64,11 @@ Read this when choosing a `setup` mutation.
|
|
|
64
64
|
| Delete an existing schedule/webhook | `delete_automation` |
|
|
65
65
|
| Enable/disable a schedule/webhook | `set_automation_enabled` |
|
|
66
66
|
| Webhook listener enabled state, port, or domain | `set_webhook_config` |
|
|
67
|
-
| Desktop keep-awake, usage pin, or Computer observe-only state | `set_desktop_settings` |
|
|
67
|
+
| Desktop keep-awake, run in background after window close, usage pin, or Computer observe-only state | `set_desktop_settings` |
|
|
68
68
|
| Desktop theme, display language, side panels, or zoom | `set_appearance` |
|
|
69
69
|
| Register a Project and optionally set its display alias | `save_project` |
|
|
70
70
|
| Unregister a Project without deleting its files | `remove_project` |
|
|
71
|
-
| Revoke one linked device
|
|
71
|
+
| Revoke one linked device by its client id from `status connection`; re-pairing restores access | `revoke_linked_device` |
|
|
72
72
|
| Managed local model context allocation; null restores automatic | `set_local_context` |
|
|
73
73
|
|
|
74
74
|
## Routes and workflow
|
|
@@ -116,7 +116,10 @@ values through chat.
|
|
|
116
116
|
## Built-in features
|
|
117
117
|
|
|
118
118
|
Read `status features` and distinguish installation, enabled state, and live
|
|
119
|
-
bridge state.
|
|
119
|
+
bridge state. `status features` covers Git, Memory, Office, Code Tidy, Local
|
|
120
|
+
Provider and Web Search plus Browser/Computer bridge activity; voice status and
|
|
121
|
+
the Browser Use / Computer Use installed and enabled markers are read with
|
|
122
|
+
`status desktop`.
|
|
120
123
|
|
|
121
124
|
- Git, Memory, Office, and Local Provider are install-first capabilities.
|
|
122
125
|
- Git and Office installation run on the Desktop when this conversation is
|
|
@@ -37,7 +37,8 @@ An attached Desktop or TUI may navigate and return `opened:true`.
|
|
|
37
37
|
- Settings → Providers: API-key, OAuth, local providers, and usage sign-in.
|
|
38
38
|
- Extensions → Plugin → Git & GitHub: Git/GitHub CLI installation and connection.
|
|
39
39
|
- Settings → Connection: web-app pairing and linked devices.
|
|
40
|
-
- Settings → System: update, keep-awake,
|
|
40
|
+
- Settings → System: update, keep-awake, run in background after the window
|
|
41
|
+
closes, and Doctor.
|
|
41
42
|
- Projects: Project registration, name, Project Memories, and Common Memory.
|
|
42
43
|
- Workflows: workflow packs, Main and Web Search defaults, and agent editing.
|
|
43
44
|
- Extensions → Plugin: Built-in cards and plugins.
|
|
@@ -48,12 +49,15 @@ the user to Desktop.
|
|
|
48
49
|
|
|
49
50
|
## Desktop-backed settings
|
|
50
51
|
|
|
51
|
-
Setup can read and change Desktop appearance, keep-awake,
|
|
52
|
-
Computer observe-only, Browser/Computer/voice installation and
|
|
53
|
-
Projects, and linked-device revocation through the attached
|
|
52
|
+
Setup can read and change Desktop appearance, keep-awake, run in background,
|
|
53
|
+
usage pin, Computer observe-only, Browser/Computer/voice installation and
|
|
54
|
+
toggles, Projects, and linked-device revocation through the attached
|
|
54
55
|
Desktop. A receipt identifies `scope:desktop-host`; appearance is not applied
|
|
55
56
|
to the paired browser. No Desktop claimant means no confirmed change.
|
|
56
57
|
|
|
58
|
+
Computer observe-only has no Desktop settings control; setup is its only
|
|
59
|
+
supported route. Usage pin is the pin toggle on the activity rail usage card.
|
|
60
|
+
|
|
57
61
|
Schedules/webhooks and workflow/agent/skill definitions use their runtime
|
|
58
62
|
actions directly. These do not require clicking their Desktop rails.
|
|
59
63
|
|
|
@@ -281,7 +281,6 @@ export function composeSystemPrompt(opts) {
|
|
|
281
281
|
// pre-tool preamble toward English.
|
|
282
282
|
const environmentParts = [
|
|
283
283
|
opts.sessionStartContext,
|
|
284
|
-
opts.projectInstructionsContext,
|
|
285
284
|
opts.environmentContext,
|
|
286
285
|
opts.languageContext,
|
|
287
286
|
].map(trimmedPromptSlice);
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from './retry-classifier.mjs';
|
|
25
25
|
import { _classifyMidstreamError, _midstreamSleepWithAbort, stampAnthropicStreamOutcome } from './anthropic-sse.mjs';
|
|
26
26
|
import { notifyCurrentAnthropicRateLimit } from './admission-scheduler.mjs';
|
|
27
|
+
import { noteAbandonedUsage } from '../../../shared/llm/usage-context.mjs';
|
|
27
28
|
|
|
28
29
|
export function createAnthropicMidState(attemptIndex) {
|
|
29
30
|
return {
|
|
@@ -144,7 +145,7 @@ export function createAnthropicMidstreamRecovery({
|
|
|
144
145
|
* `{ retry: false, value }` when a non-streaming replay produced the turn,
|
|
145
146
|
* and throws the (stamped) error when the turn is over.
|
|
146
147
|
*/
|
|
147
|
-
const
|
|
148
|
+
const decideStreamError = async ({ err, midState, controller, response, attemptIndex }) => {
|
|
148
149
|
// Canonical stream-outcome contract: stamp before ANY safety
|
|
149
150
|
// decision below. The parser's stamped verdict (when present)
|
|
150
151
|
// is authoritative — coarse midState.partialToolCall must not
|
|
@@ -309,6 +310,14 @@ export function createAnthropicMidstreamRecovery({
|
|
|
309
310
|
throw err;
|
|
310
311
|
};
|
|
311
312
|
|
|
313
|
+
// A thrown error carries its partial usage to the send's accounting; an
|
|
314
|
+
// attempt replaced by a retry or a non-streaming replay is noted here.
|
|
315
|
+
const onStreamError = async (args) => {
|
|
316
|
+
const decision = await decideStreamError(args);
|
|
317
|
+
noteAbandonedUsage(args.err?.partialUsage, args.err?.partialModel);
|
|
318
|
+
return decision;
|
|
319
|
+
};
|
|
320
|
+
|
|
312
321
|
const exhaustedError = () => firstAttemptError || new Error(unreachableMessage);
|
|
313
322
|
|
|
314
323
|
return { onStreamError, exhaustedError };
|
|
@@ -20,9 +20,6 @@ export function scopedCacheGeneration() {
|
|
|
20
20
|
// sessionId -> Map<key, { content, ts, firstToolUseId, depRoots }>
|
|
21
21
|
const _scopedBySession = new Map();
|
|
22
22
|
|
|
23
|
-
// sessionId -> Map<absPath, Set<cacheKey>> — reverse index for O(1) path-targeted invalidation
|
|
24
|
-
const _scopedReverseIdx = new Map();
|
|
25
|
-
|
|
26
23
|
function _canonicalArgs(args) {
|
|
27
24
|
if (args === null || args === undefined) return '';
|
|
28
25
|
if (typeof args !== 'object') return String(args);
|
|
@@ -243,16 +240,6 @@ function _pathTouchesRoot(absPath, root) {
|
|
|
243
240
|
);
|
|
244
241
|
}
|
|
245
242
|
|
|
246
|
-
function _dropScopedEntry(sessionId, key) {
|
|
247
|
-
_scopedBySession.get(sessionId)?.delete(key);
|
|
248
|
-
const index = _scopedReverseIdx.get(sessionId);
|
|
249
|
-
if (!index) return;
|
|
250
|
-
for (const [path, keys] of index) {
|
|
251
|
-
keys.delete(key);
|
|
252
|
-
if (keys.size === 0) index.delete(path);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
243
|
/**
|
|
257
244
|
* Look up a cached result for a deterministic multi-file-scope tool. Returns
|
|
258
245
|
* null on miss. On hit returns the full entry
|
|
@@ -265,7 +252,7 @@ export function tryScopedToolCached({ sessionId, toolName, args, cwd, touch = tr
|
|
|
265
252
|
const key = _scopedKey(toolName, args, cwd);
|
|
266
253
|
const entry = map.get(key);
|
|
267
254
|
if (!entry || Date.now() - entry.ts >= SCOPED_CACHE_TTL_MS) {
|
|
268
|
-
if (entry)
|
|
255
|
+
if (entry) map.delete(key);
|
|
269
256
|
return null;
|
|
270
257
|
}
|
|
271
258
|
if (touch) {
|
|
@@ -301,33 +288,12 @@ export function setScopedToolCached({
|
|
|
301
288
|
_scopedBySession.set(sessionId, map);
|
|
302
289
|
}
|
|
303
290
|
const depRoots = _scopedDependencyRoots(toolName, args, cwd);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
)
|
|
311
|
-
)
|
|
312
|
-
return;
|
|
313
|
-
// Register key in reverse index for dependency roots. Exact root hits use
|
|
314
|
-
// O(1) lookup; touched files under a root are caught by the small prefix scan
|
|
315
|
-
// in clearScopedToolsForSessionPaths (MAX_PER_SESSION is 100).
|
|
316
|
-
let ridx = _scopedReverseIdx.get(sessionId);
|
|
317
|
-
if (!ridx) {
|
|
318
|
-
ridx = new Map();
|
|
319
|
-
_scopedReverseIdx.set(sessionId, ridx);
|
|
320
|
-
}
|
|
321
|
-
const _registerAbs = (abs) => {
|
|
322
|
-
if (!abs || typeof abs !== 'string') return;
|
|
323
|
-
let s = ridx.get(abs);
|
|
324
|
-
if (!s) {
|
|
325
|
-
s = new Set();
|
|
326
|
-
ridx.set(abs, s);
|
|
327
|
-
}
|
|
328
|
-
s.add(key);
|
|
329
|
-
};
|
|
330
|
-
for (const dep of depRoots) _registerAbs(dep);
|
|
291
|
+
setBoundedTextCacheEntry(
|
|
292
|
+
map,
|
|
293
|
+
key,
|
|
294
|
+
{ content, ts: Date.now(), firstToolUseId: toolUseId || null, depRoots },
|
|
295
|
+
{ maxEntries: MAX_PER_SESSION }
|
|
296
|
+
);
|
|
331
297
|
}
|
|
332
298
|
|
|
333
299
|
/**
|
|
@@ -338,78 +304,51 @@ export function clearScopedToolsForSession(sessionId) {
|
|
|
338
304
|
if (!sessionId) return;
|
|
339
305
|
mutationGeneration += 1;
|
|
340
306
|
_scopedBySession.delete(sessionId);
|
|
341
|
-
_scopedReverseIdx.delete(sessionId);
|
|
342
307
|
}
|
|
343
308
|
|
|
344
309
|
/**
|
|
345
|
-
* Targeted scoped-cache invalidation: evict only entries whose
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
* root-prefix scan for files nested under cached directories/globs. Full wipe
|
|
310
|
+
* Targeted scoped-cache invalidation: evict only entries whose registered
|
|
311
|
+
* dependency roots contain, or are contained by, at least one of the given
|
|
312
|
+
* touched paths (single bounded pass; MAX_PER_SESSION entries). Full wipe
|
|
349
313
|
* when paths cannot be resolved.
|
|
350
314
|
*/
|
|
351
315
|
export function clearScopedToolsForSessionPaths(sessionId, touchedPaths, cwd) {
|
|
352
316
|
if (!sessionId || !Array.isArray(touchedPaths) || touchedPaths.length === 0) return;
|
|
353
317
|
mutationGeneration += 1;
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const absPaths = touchedCacheKeys(touchedPaths, cwd);
|
|
357
|
-
if (absPaths.length === 0) {
|
|
358
|
-
// Fallback: can't resolve — full wipe.
|
|
359
|
-
_scopedBySession.delete(sessionId);
|
|
360
|
-
_scopedReverseIdx.delete(sessionId);
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
const ridx = _scopedReverseIdx.get(sessionId);
|
|
364
|
-
const evictedKeys = new Set();
|
|
365
|
-
for (const abs of absPaths) evictScopedEntriesForPath(map, ridx, abs, evictedKeys);
|
|
366
|
-
// Remove evicted keys from any other reverse-index sets they appeared in; prune empty Sets.
|
|
367
|
-
if (ridx && evictedKeys.size > 0) {
|
|
368
|
-
for (const [absKey, keySet] of ridx) {
|
|
369
|
-
for (const k of evictedKeys) keySet.delete(k);
|
|
370
|
-
if (keySet.size === 0) ridx.delete(absKey);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
318
|
+
if (!_scopedBySession.has(sessionId)) return;
|
|
319
|
+
clearSessionForAbsPaths(sessionId, touchedCacheKeys(touchedPaths, cwd));
|
|
373
320
|
}
|
|
374
321
|
|
|
375
|
-
//
|
|
376
|
-
// entries are dropped.
|
|
322
|
+
// Normalized, deduplicated absolute cache keys of the touched paths;
|
|
323
|
+
// unresolvable entries are dropped.
|
|
377
324
|
function touchedCacheKeys(touchedPaths, cwd) {
|
|
378
325
|
const base = cwd && typeof cwd === 'string' ? cwd : process.cwd();
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
326
|
+
const out = new Set();
|
|
327
|
+
for (const p of touchedPaths) {
|
|
328
|
+
if (typeof p !== 'string' || p.length === 0) continue;
|
|
329
|
+
try {
|
|
330
|
+
out.add(_normalizeCacheKey(_pathNorm(_pathIsAbs(p) ? p : _pathResolve(base, p))));
|
|
331
|
+
} catch {
|
|
332
|
+
/* unresolvable: dropped */
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
out.delete('');
|
|
336
|
+
return [...out];
|
|
389
337
|
}
|
|
390
338
|
|
|
391
|
-
// Evicts
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
for (const key of keys) {
|
|
399
|
-
if (map.has(key)) {
|
|
400
|
-
map.delete(key);
|
|
401
|
-
evictedKeys.add(key);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
keys.clear();
|
|
405
|
-
ridx.delete(abs);
|
|
339
|
+
// Evicts the session's entries whose dependency roots touch any of the
|
|
340
|
+
// already-normalized `absPaths` in a single pass. Empty `absPaths` means the
|
|
341
|
+
// paths were unresolvable: full wipe.
|
|
342
|
+
function clearSessionForAbsPaths(sessionId, absPaths) {
|
|
343
|
+
if (absPaths.length === 0) {
|
|
344
|
+
_scopedBySession.delete(sessionId);
|
|
345
|
+
return;
|
|
406
346
|
}
|
|
347
|
+
const map = _scopedBySession.get(sessionId);
|
|
348
|
+
if (!map) return;
|
|
407
349
|
for (const [key, entry] of map) {
|
|
408
350
|
const roots = Array.isArray(entry?.depRoots) ? entry.depRoots : [];
|
|
409
|
-
if (roots.some((root) => _pathTouchesRoot(abs, root)))
|
|
410
|
-
map.delete(key);
|
|
411
|
-
evictedKeys.add(key);
|
|
412
|
-
}
|
|
351
|
+
if (roots.some((root) => absPaths.some((abs) => _pathTouchesRoot(abs, root)))) map.delete(key);
|
|
413
352
|
}
|
|
414
353
|
}
|
|
415
354
|
|
|
@@ -418,8 +357,10 @@ function evictScopedEntriesForPath(map, ridx, abs, evictedKeys) {
|
|
|
418
357
|
// that no watcher reports.
|
|
419
358
|
registerCacheInvalidationListener((paths) => {
|
|
420
359
|
mutationGeneration += 1;
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
360
|
+
if (paths?.length) {
|
|
361
|
+
const absPaths = touchedCacheKeys(paths);
|
|
362
|
+
for (const sessionId of [..._scopedBySession.keys()]) clearSessionForAbsPaths(sessionId, absPaths);
|
|
363
|
+
} else {
|
|
364
|
+
for (const sessionId of [..._scopedBySession.keys()]) clearScopedToolsForSession(sessionId);
|
|
424
365
|
}
|
|
425
366
|
});
|
|
@@ -133,7 +133,12 @@ function summarySendOpts(opts) {
|
|
|
133
133
|
onSteerMessage: undefined,
|
|
134
134
|
signal: combinedSignal(opts.signal || opts.sendOpts?.signal || null, opts.timeoutMs || HANDOFF_TIMEOUT_MAX_MS),
|
|
135
135
|
};
|
|
136
|
-
|
|
136
|
+
// The `:compact` id isolates provider state (cache, socket pool); the
|
|
137
|
+
// spend still belongs to the source session.
|
|
138
|
+
if (opts.sessionId) {
|
|
139
|
+
sendOpts.sessionId = `${opts.sessionId}:compact`;
|
|
140
|
+
sendOpts.usageSessionId = opts.sessionId;
|
|
141
|
+
}
|
|
137
142
|
if (opts.promptCacheKey || opts.sendOpts?.promptCacheKey) {
|
|
138
143
|
sendOpts.promptCacheKey = `${opts.promptCacheKey || opts.sendOpts.promptCacheKey}:compact`;
|
|
139
144
|
}
|
|
@@ -78,6 +78,8 @@ function summarizeConversation(target, ctx) {
|
|
|
78
78
|
);
|
|
79
79
|
const summarySession = {
|
|
80
80
|
id: `${sessionId || 'unknown'}:compact`,
|
|
81
|
+
// Usage attribution: background sessions stay background.
|
|
82
|
+
sourceType: sessionRef?.sourceType,
|
|
81
83
|
provider: target.providerName,
|
|
82
84
|
model: target.model,
|
|
83
85
|
cwd: sessionRef?.cwd,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* ask for approval before the tool runs.
|
|
4
4
|
*/
|
|
5
5
|
import { resolvePreToolAskApproval } from '../tool-helpers.mjs';
|
|
6
|
-
import { runAbortable } from '../../../../../shared/abort-race.mjs';
|
|
6
|
+
import { runAbortable, throwIfAborted } from '../../../../../shared/abort-race.mjs';
|
|
7
7
|
|
|
8
8
|
const isArgsObject = (value) => value && typeof value === 'object' && !Array.isArray(value);
|
|
9
9
|
|
|
@@ -67,8 +67,12 @@ export async function applyBeforeToolHook({
|
|
|
67
67
|
const denied = await askApproval(call, { callerSessionId, executeOpts, toolApprovalHook, decision });
|
|
68
68
|
if (denied) return denied;
|
|
69
69
|
}
|
|
70
|
-
} catch {
|
|
71
|
-
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throwIfAborted(executeOpts.signal);
|
|
72
|
+
// A failed policy check fails closed: the tool must not run.
|
|
73
|
+
return {
|
|
74
|
+
denial: `Error: tool "${name}" denied: PreToolUse policy check failed: ${error?.message || String(error)}`,
|
|
75
|
+
};
|
|
72
76
|
}
|
|
73
77
|
return call;
|
|
74
78
|
}
|
|
@@ -86,7 +86,7 @@ export function composeSessionSystem(opts, { profile, providerName, modelName, s
|
|
|
86
86
|
const languageContext = leadOnly ? _buildLeadLanguageContext() : '';
|
|
87
87
|
const shellEnvironmentContext = buildShellEnvironmentContext(opts, ownerIsAgent, surface.toolsForRouting);
|
|
88
88
|
// Persisted env tail: refreshSessionBp3Environment rebuilds the env block
|
|
89
|
-
// as [session,
|
|
89
|
+
// as [session, bp3EnvironmentContext], so the language block must
|
|
90
90
|
// already sit at the end of this string to stay last after a refresh.
|
|
91
91
|
const environmentTailContext = [shellEnvironmentContext, languageContext].filter(Boolean).join('\n\n---\n\n');
|
|
92
92
|
const composed = composeSystemPrompt({
|
|
@@ -46,16 +46,13 @@ const STAT_CACHE = new Map(); // fullPath → { ts, stat }
|
|
|
46
46
|
const STAT_CACHE_TTL_MS = 5_000;
|
|
47
47
|
const STAT_CACHE_MAX_ENTRIES = 2_000;
|
|
48
48
|
const RAW_CONTENT_CACHE = new Map(); // fullPath → { ts, mtimeMs, ctimeMs, size, rawBuf }
|
|
49
|
-
const RAW_CONTENT_INFLIGHT = new Map(); // canonical path → {
|
|
49
|
+
const RAW_CONTENT_INFLIGHT = new Map(); // canonical path → { path, promise }
|
|
50
50
|
const RAW_CONTENT_CACHE_TTL_MS = 30_000;
|
|
51
51
|
// 64: a parallel read batch fanning out over a component directory easily
|
|
52
52
|
// tops 16 files; at 16 the batch thrashed its own cache before re-reads hit.
|
|
53
53
|
// The 64MB byte cap below still bounds memory.
|
|
54
54
|
const RAW_CONTENT_CACHE_MAX_ENTRIES = 64;
|
|
55
|
-
const
|
|
56
|
-
const PATH_MUTATION_GENERATION_MAX_ENTRIES = 4096;
|
|
57
|
-
let PATH_MUTATION_GLOBAL_GENERATION = 0;
|
|
58
|
-
const READ_ONLY_STAT_INFLIGHT = new Map(); // kind + canonical path → { generation, promise }
|
|
55
|
+
const READ_ONLY_STAT_INFLIGHT = new Map(); // kind + canonical path → { path, promise }
|
|
59
56
|
const RAW_CONTENT_CACHE_MAX_BYTES = envByteBudget(
|
|
60
57
|
'MIXDOG_RAW_CONTENT_CACHE_MAX_BYTES',
|
|
61
58
|
'MIXDOG_RAW_CONTENT_CACHE_MAX_MB',
|
|
@@ -74,25 +71,6 @@ function cachePathsOverlap(a, b) {
|
|
|
74
71
|
return a.startsWith(b.endsWith(sep) ? b : `${b}${sep}`) || b.startsWith(a.endsWith(sep) ? a : `${a}${sep}`);
|
|
75
72
|
}
|
|
76
73
|
|
|
77
|
-
function bumpPathMutationGeneration(path) {
|
|
78
|
-
const key = canonicalCachePath(path);
|
|
79
|
-
PATH_MUTATION_GENERATIONS.set(key, (PATH_MUTATION_GENERATIONS.get(key) || 0) + 1);
|
|
80
|
-
while (PATH_MUTATION_GENERATIONS.size > PATH_MUTATION_GENERATION_MAX_ENTRIES) {
|
|
81
|
-
const oldest = PATH_MUTATION_GENERATIONS.keys().next().value;
|
|
82
|
-
if (!oldest) break;
|
|
83
|
-
PATH_MUTATION_GENERATIONS.delete(oldest);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function getPathMutationGeneration(path) {
|
|
88
|
-
const key = canonicalCachePath(path);
|
|
89
|
-
let generation = PATH_MUTATION_GLOBAL_GENERATION;
|
|
90
|
-
for (const [changedPath, value] of PATH_MUTATION_GENERATIONS) {
|
|
91
|
-
if (cachePathsOverlap(key, changedPath)) generation += value;
|
|
92
|
-
}
|
|
93
|
-
return generation;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
74
|
function cacheEntryOverlapsPaths(entry, affectedPaths) {
|
|
97
75
|
const entryPaths = Array.isArray(entry?.paths) ? entry.paths : [];
|
|
98
76
|
const entryScopes = Array.isArray(entry?.scopes) ? entry.scopes : [];
|
|
@@ -237,12 +215,12 @@ export async function runResultCacheInFlight(key, compute, options = {}) {
|
|
|
237
215
|
return subscribeResultCacheInFlight(entry, subscriberSignal);
|
|
238
216
|
}
|
|
239
217
|
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
async function
|
|
218
|
+
// Invalidation detaches only affected in-flight entries. New readers never
|
|
219
|
+
// join a pre-mutation operation, while its existing subscribers still finish.
|
|
220
|
+
// Lookups need no scan of the project's historical mutations on every read.
|
|
221
|
+
async function runPathInFlight(inFlight, key, path, start) {
|
|
244
222
|
const existing = inFlight.get(key);
|
|
245
|
-
if (existing
|
|
223
|
+
if (existing) return await existing.promise;
|
|
246
224
|
const promise = Promise.resolve()
|
|
247
225
|
.then(start)
|
|
248
226
|
.finally(() => {
|
|
@@ -250,23 +228,21 @@ async function runInFlightForGeneration(inFlight, key, generation, start) {
|
|
|
250
228
|
inFlight.delete(key);
|
|
251
229
|
}
|
|
252
230
|
});
|
|
253
|
-
inFlight.set(key, {
|
|
231
|
+
inFlight.set(key, { path, promise });
|
|
254
232
|
return await promise;
|
|
255
233
|
}
|
|
256
234
|
|
|
257
235
|
export async function runRawContentInFlight(fullPath, loader = fsPromises.readFile) {
|
|
258
236
|
const key = canonicalCachePath(fullPath);
|
|
259
|
-
return await
|
|
260
|
-
loader(fullPath)
|
|
261
|
-
);
|
|
237
|
+
return await runPathInFlight(RAW_CONTENT_INFLIGHT, key, key, () => loader(fullPath));
|
|
262
238
|
}
|
|
263
239
|
|
|
264
240
|
export async function runReadOnlyStatInFlight(fullPath, loader = fsPromises.stat, kind = 'stat') {
|
|
265
241
|
const canonicalPath = canonicalCachePath(fullPath);
|
|
266
|
-
return await
|
|
242
|
+
return await runPathInFlight(
|
|
267
243
|
READ_ONLY_STAT_INFLIGHT,
|
|
268
244
|
`${kind}|${canonicalPath}`,
|
|
269
|
-
|
|
245
|
+
canonicalPath,
|
|
270
246
|
() => loader(fullPath)
|
|
271
247
|
);
|
|
272
248
|
}
|
|
@@ -484,8 +460,8 @@ function cacheInvalidateAll() {
|
|
|
484
460
|
STAT_CACHE.clear();
|
|
485
461
|
RAW_CONTENT_CACHE.clear();
|
|
486
462
|
RAW_CONTENT_CACHE_BYTES = 0;
|
|
487
|
-
|
|
488
|
-
|
|
463
|
+
RAW_CONTENT_INFLIGHT.clear();
|
|
464
|
+
READ_ONLY_STAT_INFLIGHT.clear();
|
|
489
465
|
runExtraInvalidationListeners();
|
|
490
466
|
}
|
|
491
467
|
|
|
@@ -495,6 +471,11 @@ function cacheInvalidatePaths(paths) {
|
|
|
495
471
|
cacheInvalidateAll();
|
|
496
472
|
return;
|
|
497
473
|
}
|
|
474
|
+
for (const inFlight of [RAW_CONTENT_INFLIGHT, READ_ONLY_STAT_INFLIGHT]) {
|
|
475
|
+
for (const [key, entry] of inFlight) {
|
|
476
|
+
if (affectedPaths.some((affected) => cachePathsOverlap(entry.path, affected))) inFlight.delete(key);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
498
479
|
for (const [key, entry] of RESULT_CACHE_INFLIGHT) {
|
|
499
480
|
const scopes = Array.isArray(entry.scopes) ? entry.scopes : [];
|
|
500
481
|
if (
|
|
@@ -522,7 +503,6 @@ function cacheInvalidatePaths(paths) {
|
|
|
522
503
|
}
|
|
523
504
|
for (const affected of affectedPaths) {
|
|
524
505
|
deleteReadRangeIndexForPath(affected);
|
|
525
|
-
bumpPathMutationGeneration(affected);
|
|
526
506
|
}
|
|
527
507
|
runExtraInvalidationListeners(affectedPaths);
|
|
528
508
|
}
|