mixdog 0.9.45 → 0.9.47
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/package.json +5 -2
- package/scripts/agent-dispatch-abort-compose-test.mjs +31 -0
- package/scripts/agent-model-liveness-test.mjs +618 -0
- package/scripts/agent-trace-io-test.mjs +68 -4
- package/scripts/bench-run.mjs +30 -3
- package/scripts/compact-pressure-test.mjs +187 -4
- package/scripts/compact-prior-context-flatten-test.mjs +252 -0
- package/scripts/compact-trigger-migration-smoke.mjs +8 -6
- package/scripts/compacted-placeholder-scrub-test.mjs +20 -34
- package/scripts/context-mcp-metering-test.mjs +75 -0
- package/scripts/explore-prompt-policy-test.mjs +15 -16
- package/scripts/explore-timeout-cancel-test.mjs +345 -0
- package/scripts/headless-pristine-execution-test.mjs +614 -0
- package/scripts/internal-comms-smoke.mjs +15 -6
- package/scripts/live-worker-smoke.mjs +1 -1
- package/scripts/memory-core-input-test.mjs +137 -0
- package/scripts/memory-rule-contract-test.mjs +5 -5
- package/scripts/openai-oauth-refresh-race-test.mjs +120 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +26 -0
- package/scripts/parent-abort-link-test.mjs +22 -0
- package/scripts/provider-toolcall-test.mjs +22 -0
- package/scripts/reactive-compact-persist-smoke.mjs +8 -4
- package/scripts/session-bench.mjs +3 -70
- package/scripts/task-bench.mjs +0 -2
- package/scripts/terminal-bench-isolation-guards-test.mjs +102 -0
- package/scripts/tool-smoke.mjs +21 -21
- package/scripts/tool-tui-presentation-test.mjs +68 -0
- package/scripts/tui-transcript-jitter-harness-entry.jsx +91 -10
- package/src/app.mjs +28 -103
- package/src/cli.mjs +17 -13
- package/src/defaults/cycle3-review-prompt.md +10 -4
- package/src/defaults/memory-promote-prompt.md +4 -3
- package/src/headless-command.mjs +139 -0
- package/src/headless-role.mjs +121 -10
- package/src/help.mjs +4 -1
- package/src/rules/agent/00-common.md +3 -3
- package/src/rules/agent/00-core.md +8 -9
- package/src/rules/agent/20-skip-protocol.md +2 -3
- package/src/rules/agent/30-explorer.md +50 -56
- package/src/rules/agent/40-cycle1-agent.md +10 -12
- package/src/rules/agent/41-cycle2-agent.md +12 -9
- package/src/rules/agent/42-cycle3-agent.md +4 -6
- package/src/rules/lead/01-general.md +5 -6
- package/src/rules/lead/02-channels.md +1 -1
- package/src/rules/lead/lead-brief.md +14 -17
- package/src/rules/lead/lead-tool.md +3 -3
- package/src/rules/shared/01-tool.md +41 -43
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +46 -10
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +44 -31
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +18 -3
- package/src/runtime/agent/orchestrator/config.mjs +96 -30
- package/src/runtime/agent/orchestrator/context/collect.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +18 -5
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +123 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +148 -30
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +5 -7
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +62 -14
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +141 -19
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +85 -17
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -8
- package/src/runtime/agent/orchestrator/providers/registry.mjs +47 -17
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +159 -20
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +83 -10
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +75 -7
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +4 -374
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +0 -75
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +0 -5
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +20 -3
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +32 -16
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +86 -15
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +30 -27
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +1 -14
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +21 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -51
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +26 -0
- package/src/runtime/memory/index.mjs +0 -1
- package/src/runtime/memory/lib/core-memory-store.mjs +44 -24
- package/src/runtime/memory/lib/http-router.mjs +0 -193
- package/src/runtime/memory/lib/memory-action-handlers.mjs +37 -11
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +13 -5
- package/src/runtime/memory/lib/memory-cycle2.mjs +8 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +41 -6
- package/src/runtime/memory/lib/query-handlers.mjs +49 -6
- package/src/runtime/memory/lib/recall-format.mjs +21 -6
- package/src/runtime/memory/tool-defs.mjs +5 -6
- package/src/runtime/shared/config.mjs +11 -34
- package/src/runtime/shared/pristine-execution-contract.json +75 -0
- package/src/runtime/shared/pristine-execution.mjs +356 -0
- package/src/runtime/shared/provider-api-key.mjs +43 -0
- package/src/runtime/shared/provider-auth-binding.mjs +21 -0
- package/src/session-runtime/context-status.mjs +61 -13
- package/src/session-runtime/mcp-glue.mjs +29 -2
- package/src/session-runtime/plugin-mcp.mjs +7 -0
- package/src/session-runtime/resource-api.mjs +38 -5
- package/src/session-runtime/runtime-core.mjs +5 -1
- package/src/session-runtime/session-turn-api.mjs +14 -2
- package/src/session-runtime/settings-api.mjs +5 -0
- package/src/session-runtime/tool-catalog.mjs +13 -2
- package/src/session-runtime/tool-defs.mjs +1 -3
- package/src/standalone/agent-task-status.mjs +50 -11
- package/src/standalone/agent-tool/tool-def.mjs +1 -1
- package/src/standalone/explore-tool.mjs +257 -49
- package/src/standalone/seeds.mjs +1 -0
- package/src/tui/App.jsx +23 -10
- package/src/tui/app/use-transcript-scroll.mjs +4 -3
- package/src/tui/app/use-transcript-window.mjs +12 -21
- package/src/tui/components/ContextPanel.jsx +19 -25
- package/src/tui/dist/index.mjs +77 -65
- package/src/tui/engine/agent-envelope.mjs +16 -5
- package/src/tui/engine/labels.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +21 -51
- package/src/workflows/solo/WORKFLOW.md +12 -17
- package/src/workflows/solo-review/WORKFLOW.md +0 -47
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
# Lead Brief
|
|
2
2
|
|
|
3
|
-
-
|
|
4
|
-
required
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
the original request and official spec/test acceptance criteria.
|
|
18
|
-
- Full brief only for fresh spawn/`respawned: true`; live follow-ups are delta.
|
|
19
|
-
Dead-tag send is cold: re-supply anchors.
|
|
3
|
+
- Use one-line fragments. `Task:` is mandatory and lossless: preserve intent,
|
|
4
|
+
required and forbidden outcomes, completion/stop boundary, user-supplied
|
|
5
|
+
exact targets, and exact replacements/outputs. Never infer exactness from
|
|
6
|
+
task name, file count, or difficulty.
|
|
7
|
+
- Each role constructs its own `Task:` from the original request and official
|
|
8
|
+
spec/test acceptance criteria, preserving every requirement and boundary.
|
|
9
|
+
- Omit role-known rules, repeated context/facts, and padding; split scope
|
|
10
|
+
without discarding requirements.
|
|
11
|
+
- Other fields are task-specific deltas: `Anchors:`, `Allow/Forbid:`,
|
|
12
|
+
`Deliver:`. Omit empty fields. Anchors are `file:line` plus a one-line
|
|
13
|
+
conclusion, never log/code bodies. State outcomes, not methods, unless the
|
|
14
|
+
method is required. `Deliver:` sets handoff shape/size.
|
|
15
|
+
- Send a full brief only for a fresh spawn or `respawned: true`; live follow-ups
|
|
16
|
+
contain only the delta. A dead-tag send is cold and must re-supply anchors.
|
|
20
17
|
- Never `send` mid-run; batch one follow-up after completion; interrupt only to
|
|
21
18
|
cancel. Agent communication is English.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Lead Tools
|
|
2
2
|
|
|
3
|
-
- Write-role agents self-verify
|
|
4
|
-
verification, benches, and all git
|
|
5
|
-
- Use current project/workspace unless
|
|
3
|
+
- Write-role agents self-verify with `shell`. Lead uses `shell` for cross-scope
|
|
4
|
+
verification, benches, and all git.
|
|
5
|
+
- Use the current project/workspace unless the request or tool requires another.
|
|
@@ -1,50 +1,48 @@
|
|
|
1
1
|
# Tool Use
|
|
2
2
|
|
|
3
|
-
- Before the first call, gather
|
|
4
|
-
|
|
5
|
-
`
|
|
6
|
-
|
|
7
|
-
`
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
inspection
|
|
3
|
+
- Before the first call, gather every known facet in one tool message; for
|
|
4
|
+
each facet choose exactly one shortest locator route:
|
|
5
|
+
broad/uncertain→`explore` (roles without it: `find`); partial path/name→
|
|
6
|
+
`find`; verified root+wildcard→
|
|
7
|
+
`glob`; quoted/non-identifier literal or regex→`grep`; exact code
|
|
8
|
+
identifier/relation→`code_graph` before grep; known file/span→`read`
|
|
9
|
+
directly without `grep`; verified directory→`list`; edit→`apply_patch`;
|
|
10
|
+
program/state change→`shell`; web/current external info→`search`. Use grep
|
|
11
|
+
only for a requested literal occurrence or after graph zero/error.
|
|
12
|
+
- Batch compatible targets and combine variants, symbols, scopes, paths, and
|
|
13
|
+
queries. Parallelize distinct facets only, never alternative routes for one
|
|
14
|
+
facet. Put independent read-only calls in one turn; they may run
|
|
15
|
+
concurrently regardless of tool. Later turns are only for targets dependent
|
|
16
|
+
on prior results or unresolved facets. Shell/write calls are serial.
|
|
17
|
+
- After locator results, collect all known candidate files/regions before
|
|
18
|
+
inspection. Batch compatible reads, including same-file regions, in one
|
|
19
19
|
`path[]` call and graph targets in arrays. Parallelize independent
|
|
20
|
-
incompatible read-only inspections
|
|
20
|
+
incompatible read-only inspections. Do not start a singleton while a known
|
|
21
21
|
compatible candidate remains. Put all known edits in one patch.
|
|
22
22
|
- Project root, session cwd, user-provided and tool-returned paths are
|
|
23
|
-
verified.
|
|
24
|
-
|
|
25
|
-
`path:"."` with guessed `src/**`. ENOENT
|
|
26
|
-
guess. Unscoped root grep/glob/list
|
|
27
|
-
- At task start, `explore` facets
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
verified. Use `find` first for every genuinely guessed path/name fragment, in
|
|
24
|
+
the same turn as independent probes. Never find verified roots or use
|
|
25
|
+
`path:"."` with guessed `src/**`. On ENOENT, find the basename; never retry
|
|
26
|
+
the guess. Unscoped root grep/glob/list requires no find.
|
|
27
|
+
- At task start, batch all `explore` facets in one `query[]` call, maximum 8,
|
|
28
|
+
without rephrased duplicates. Retry `EXPLORATION_FAILED` once with changed
|
|
29
|
+
tokens.
|
|
30
30
|
- Stop when evidence covers the deliverable; never re-locate, re-verify,
|
|
31
|
-
refine or upgrade a sufficient anchor.
|
|
32
|
-
|
|
33
|
-
code_graph detail inspection is valid
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
- One carve-out to parallel-first: apply the edit, then verify it in a
|
|
46
|
-
separate shell turn and consume results in order. Otherwise stay parallel.
|
|
31
|
+
refine, or upgrade a sufficient anchor. Every returned requested `path:line`
|
|
32
|
+
freezes the LOCATION only. When content was not returned, read or
|
|
33
|
+
code_graph detail inspection is valid; never re-locate it.
|
|
34
|
+
- Give grep content mode enough `-C` to avoid rereads; a sufficient contextual
|
|
35
|
+
grep means no overlapping `read`. After `files_with_matches`, `count`,
|
|
36
|
+
capped, or insufficient context, inspect only missing content. A nonzero
|
|
37
|
+
`content_with_context` result resolves the concept; act directly without
|
|
38
|
+
token changes, narrowing, or re-search. Only zero/error results permit token
|
|
39
|
+
or scope changes.
|
|
40
|
+
- Batch independent read files/regions as real arrays, using
|
|
41
|
+
`{path,offset,limit}` regions, and read the whole logical unit. Never page or
|
|
42
|
+
reread returned spans.
|
|
43
|
+
- Apply edits before verification, then verify in a separate shell turn and
|
|
44
|
+
consume results in order. Otherwise remain parallel.
|
|
47
45
|
- A long-running command promoted to background is a decision point, not a
|
|
48
|
-
cue to wait
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
cue to wait. Estimate whether observed progress can finish within budget;
|
|
47
|
+
otherwise diagnose the bottleneck and switch routes. Choose waiting
|
|
48
|
+
explicitly.
|
|
@@ -256,6 +256,40 @@ function maintenanceRouteToPreset(routeOrName, agent) {
|
|
|
256
256
|
* provider call tears down promptly (parent→child cascade).
|
|
257
257
|
* @returns {(args: { prompt, preset?, sourceName? }) => Promise<string>}
|
|
258
258
|
*/
|
|
259
|
+
// runtime-liveness keeps one parent link per session because askSession swaps
|
|
260
|
+
// its controller at turn start. Agent dispatch can have several independent
|
|
261
|
+
// cancellation sources, so collapse them before installing that one link.
|
|
262
|
+
// The first already-aborted source wins (in declaration order), retaining its
|
|
263
|
+
// original reason instead of replacing it with a generic AbortError.
|
|
264
|
+
export function composeAgentDispatchAbortSignal(signals) {
|
|
265
|
+
const sources = (Array.isArray(signals) ? signals : [])
|
|
266
|
+
.filter((signal) => signal instanceof AbortSignal);
|
|
267
|
+
if (sources.length === 0) return { signal: null, dispose: () => {} };
|
|
268
|
+
const controller = new AbortController();
|
|
269
|
+
const listeners = [];
|
|
270
|
+
const abortFrom = (signal) => {
|
|
271
|
+
if (controller.signal.aborted) return;
|
|
272
|
+
try { controller.abort(signal.reason); } catch { try { controller.abort(); } catch { /* ignore */ } }
|
|
273
|
+
};
|
|
274
|
+
for (const signal of sources) {
|
|
275
|
+
if (signal.aborted) {
|
|
276
|
+
abortFrom(signal);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
const listener = () => abortFrom(signal);
|
|
280
|
+
signal.addEventListener('abort', listener, { once: true });
|
|
281
|
+
listeners.push([signal, listener]);
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
signal: controller.signal,
|
|
285
|
+
dispose: () => {
|
|
286
|
+
for (const [signal, listener] of listeners) {
|
|
287
|
+
try { signal.removeEventListener('abort', listener); } catch { /* ignore */ }
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
259
293
|
export function makeAgentDispatch(opts = {}) {
|
|
260
294
|
if (!opts.agent || typeof opts.agent !== 'string') {
|
|
261
295
|
throw new Error('[agent-dispatch] opts.agent is required');
|
|
@@ -375,14 +409,6 @@ export function makeAgentDispatch(opts = {}) {
|
|
|
375
409
|
const _getProgressSnapshot = _managerMod?.getSessionProgressSnapshot;
|
|
376
410
|
const _getLastProgressAt = _managerMod?.getSessionLastProgressAt;
|
|
377
411
|
const _getSession = _managerMod?.getSession || getSession;
|
|
378
|
-
if (_linkSignal) {
|
|
379
|
-
if (opts.parentSignal instanceof AbortSignal) {
|
|
380
|
-
try { _linkSignal(session.id, opts.parentSignal); } catch { /* ignore */ }
|
|
381
|
-
}
|
|
382
|
-
if (callParentSignal instanceof AbortSignal) {
|
|
383
|
-
try { _linkSignal(session.id, callParentSignal); } catch { /* ignore */ }
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
412
|
// Watchdog policy is split:
|
|
387
413
|
// - firstResponseTimeoutMs cuts quickly only when the model produces no
|
|
388
414
|
// first stream/tool activity at all.
|
|
@@ -396,8 +422,17 @@ export function makeAgentDispatch(opts = {}) {
|
|
|
396
422
|
const _idleController = (agentWatchdogPolicyActive(_watchdogPolicy) && _linkSignal)
|
|
397
423
|
? new AbortController()
|
|
398
424
|
: null;
|
|
399
|
-
|
|
400
|
-
|
|
425
|
+
// Do not link factory parent, per-call explore cancellation, and the
|
|
426
|
+
// watchdog one at a time: each link replaces the previous listener in
|
|
427
|
+
// runtime-liveness. One composite survives askSession's controller
|
|
428
|
+
// swap and makes every source reach the provider call.
|
|
429
|
+
const _abortLink = composeAgentDispatchAbortSignal([
|
|
430
|
+
opts.parentSignal,
|
|
431
|
+
callParentSignal,
|
|
432
|
+
_idleController?.signal,
|
|
433
|
+
]);
|
|
434
|
+
if (_linkSignal && _abortLink.signal) {
|
|
435
|
+
try { _linkSignal(session.id, _abortLink.signal); } catch { /* ignore */ }
|
|
401
436
|
}
|
|
402
437
|
// Watchdog blind spot guard: when the runtime snapshot is missing AND
|
|
403
438
|
// no progress timestamp exists (pre-liveness hang, swept runtime), the
|
|
@@ -491,6 +526,7 @@ export function makeAgentDispatch(opts = {}) {
|
|
|
491
526
|
try { closeSession(session.id, 'ephemeral-error'); } catch { /* ignore */ }
|
|
492
527
|
throw err;
|
|
493
528
|
} finally {
|
|
529
|
+
_abortLink.dispose();
|
|
494
530
|
if (_idleTimer) {
|
|
495
531
|
try { clearInterval(_idleTimer); } catch { /* ignore */ }
|
|
496
532
|
}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
resolveAgentToolThresholdSeconds,
|
|
12
12
|
} from '../stall-policy.mjs';
|
|
13
13
|
|
|
14
|
-
const WATCHDOG_ABORT_RE = /^agent (?:first response stale|task stale|tool running stale)\s*\(/;
|
|
14
|
+
const WATCHDOG_ABORT_RE = /^agent (?:first (?:transport|semantic response|response) stale|task stale|tool running stale)\s*\(/;
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Typed abort error for the agent progress watchdog. Carrying a stable `name`
|
|
@@ -209,12 +209,12 @@ function resolveExplicitMs(value, fallback) {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
export function resolveAgentWatchdogPolicy(agent, overrides = {}) {
|
|
212
|
-
const
|
|
213
|
-
overrides.firstResponseTimeoutMs,
|
|
212
|
+
const firstTransportMs = resolveExplicitMs(
|
|
213
|
+
overrides.firstTransportTimeoutMs ?? overrides.firstResponseTimeoutMs,
|
|
214
214
|
DEFAULT_FIRST_RESPONSE_TIMEOUT_MS,
|
|
215
215
|
);
|
|
216
|
-
const
|
|
217
|
-
overrides.firstVisibleTimeoutMs,
|
|
216
|
+
const firstSemanticMs = resolveExplicitMs(
|
|
217
|
+
overrides.firstSemanticTimeoutMs ?? overrides.firstVisibleTimeoutMs,
|
|
218
218
|
DEFAULT_FIRST_VISIBLE_CEILING_MS,
|
|
219
219
|
);
|
|
220
220
|
|
|
@@ -245,8 +245,12 @@ export function resolveAgentWatchdogPolicy(agent, overrides = {}) {
|
|
|
245
245
|
const toolRunningMs = Math.max(0, Math.floor(toolRunningSec * 1000));
|
|
246
246
|
|
|
247
247
|
return {
|
|
248
|
-
|
|
249
|
-
|
|
248
|
+
firstTransportMs,
|
|
249
|
+
firstSemanticMs,
|
|
250
|
+
// Compatibility aliases for persisted/background metadata and callers
|
|
251
|
+
// using the previous option names.
|
|
252
|
+
firstResponseMs: firstTransportMs,
|
|
253
|
+
firstVisibleCeilingMs: firstSemanticMs,
|
|
250
254
|
idleStaleMs,
|
|
251
255
|
toolRunningMs,
|
|
252
256
|
};
|
|
@@ -255,19 +259,21 @@ export function resolveAgentWatchdogPolicy(agent, overrides = {}) {
|
|
|
255
259
|
export function evaluateAgentWatchdogAbort(snapshot, now, policy) {
|
|
256
260
|
if (!snapshot || !policy) return null;
|
|
257
261
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
262
|
+
const startedAt = snapshot.modelRequestStartedAt || snapshot.askStartedAt;
|
|
263
|
+
const firstTransportMs = policy.firstTransportMs ?? policy.firstResponseMs ?? 0;
|
|
264
|
+
const firstSemanticMs = policy.firstSemanticMs ?? policy.firstVisibleCeilingMs ?? 0;
|
|
265
|
+
// Independent fixed deadlines from request start. Transport can satisfy
|
|
266
|
+
// only the transport deadline; it never switches, extends, or resets the
|
|
267
|
+
// semantic-response deadline.
|
|
268
|
+
if (snapshot.waitingForTransport && firstTransportMs > 0 && startedAt
|
|
269
|
+
&& now - startedAt > firstTransportMs) {
|
|
270
|
+
return new AgentStallAbortError(`agent first transport stale (${firstTransportMs}ms)`);
|
|
271
|
+
}
|
|
272
|
+
if ((snapshot.waitingForFirstSemantic ?? snapshot.waitingForFirstActivity)
|
|
273
|
+
&& firstSemanticMs > 0 && startedAt && now - startedAt > firstSemanticMs) {
|
|
274
|
+
return new AgentStallAbortError(`agent first semantic response stale (${firstSemanticMs}ms)`);
|
|
275
|
+
}
|
|
276
|
+
if (snapshot.waitingForFirstSemantic ?? snapshot.waitingForFirstActivity) {
|
|
271
277
|
return null;
|
|
272
278
|
}
|
|
273
279
|
|
|
@@ -290,15 +296,7 @@ export function evaluateAgentWatchdogAbort(snapshot, now, policy) {
|
|
|
290
296
|
// is still caught after the grace window. Unknown/missing self-deadline
|
|
291
297
|
// (selfDeadlineMs <= 0) keeps the plain toolRunningMs behavior. All
|
|
292
298
|
// other tools are unaffected.
|
|
293
|
-
|
|
294
|
-
const selfDeadlineMs = Number(snapshot.toolSelfDeadlineMs);
|
|
295
|
-
if (
|
|
296
|
-
isSelfDeadlineTool(snapshot.currentTool)
|
|
297
|
-
&& Number.isFinite(selfDeadlineMs)
|
|
298
|
-
&& selfDeadlineMs > 0
|
|
299
|
-
) {
|
|
300
|
-
ceilingMs = Math.max(policy.toolRunningMs, selfDeadlineMs + TOOL_SELF_DEADLINE_GRACE_MS);
|
|
301
|
-
}
|
|
299
|
+
const ceilingMs = resolveEffectiveToolRunningCeilingMs(snapshot, policy);
|
|
302
300
|
if (now - snapshot.toolStartedAt > ceilingMs) {
|
|
303
301
|
return new AgentStallAbortError(`agent tool running stale (${ceilingMs}ms)`);
|
|
304
302
|
}
|
|
@@ -307,10 +305,25 @@ export function evaluateAgentWatchdogAbort(snapshot, now, policy) {
|
|
|
307
305
|
return null;
|
|
308
306
|
}
|
|
309
307
|
|
|
308
|
+
/** The exact tool ceiling enforced by evaluateAgentWatchdogAbort. */
|
|
309
|
+
export function resolveEffectiveToolRunningCeilingMs(snapshot, policy) {
|
|
310
|
+
const policyMs = Number(policy?.toolRunningMs);
|
|
311
|
+
let ceilingMs = Number.isFinite(policyMs) && policyMs > 0 ? policyMs : 0;
|
|
312
|
+
const selfDeadlineMs = Number(snapshot?.toolSelfDeadlineMs);
|
|
313
|
+
if (
|
|
314
|
+
isSelfDeadlineTool(snapshot?.currentTool)
|
|
315
|
+
&& Number.isFinite(selfDeadlineMs)
|
|
316
|
+
&& selfDeadlineMs > 0
|
|
317
|
+
) {
|
|
318
|
+
ceilingMs = Math.max(ceilingMs, selfDeadlineMs + TOOL_SELF_DEADLINE_GRACE_MS);
|
|
319
|
+
}
|
|
320
|
+
return ceilingMs;
|
|
321
|
+
}
|
|
322
|
+
|
|
310
323
|
export function agentWatchdogPolicyActive(policy) {
|
|
311
324
|
if (!policy) return false;
|
|
312
|
-
return (policy.firstResponseMs > 0)
|
|
313
|
-
|| (policy.firstVisibleCeilingMs > 0)
|
|
325
|
+
return ((policy.firstTransportMs ?? policy.firstResponseMs) > 0)
|
|
326
|
+
|| ((policy.firstSemanticMs ?? policy.firstVisibleCeilingMs) > 0)
|
|
314
327
|
|| (policy.idleStaleMs > 0)
|
|
315
328
|
|| (policy.toolRunningMs > 0);
|
|
316
329
|
}
|
|
@@ -23,6 +23,7 @@ let _serviceUrl = null;
|
|
|
23
23
|
let _serviceAdvertPort = null;
|
|
24
24
|
let _flushInFlight = false;
|
|
25
25
|
let _localTracePath = null;
|
|
26
|
+
let _localTraceIsExplicit = false;
|
|
26
27
|
let _localTraceBuffer = [];
|
|
27
28
|
let _localTraceTimer = null;
|
|
28
29
|
let _localTraceFlushInFlight = false;
|
|
@@ -93,6 +94,12 @@ function _rotateLocalTraceIfNeeded(path) {
|
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
function _ensureLocalTraceDirectory(path) {
|
|
98
|
+
// The path may outlive an ephemeral runtime directory. Recreate its parent
|
|
99
|
+
// at flush time rather than relying on the one-time path resolution.
|
|
100
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
101
|
+
}
|
|
102
|
+
|
|
96
103
|
function _resolveLocalTracePath() {
|
|
97
104
|
if (process.env.MIXDOG_AGENT_TRACE_LOCAL_DISABLE === '1') return null;
|
|
98
105
|
if (_localTracePath) return _localTracePath;
|
|
@@ -104,9 +111,10 @@ function _resolveLocalTracePath() {
|
|
|
104
111
|
try {
|
|
105
112
|
_localTracePath = explicit
|
|
106
113
|
|| join(getPluginData(), 'history', 'agent-trace.jsonl');
|
|
114
|
+
_localTraceIsExplicit = Boolean(explicit);
|
|
107
115
|
// R4 data-at-rest: trace rows may carry tool payloads / prompts;
|
|
108
116
|
// clamp dir to owner-only on POSIX (advisory on Windows).
|
|
109
|
-
|
|
117
|
+
_ensureLocalTraceDirectory(_localTracePath);
|
|
110
118
|
return _localTracePath;
|
|
111
119
|
} catch {
|
|
112
120
|
return null;
|
|
@@ -142,10 +150,15 @@ function _flushLocalTrace() {
|
|
|
142
150
|
const chunk = _localTraceBuffer.join('');
|
|
143
151
|
_localTraceBuffer = [];
|
|
144
152
|
try {
|
|
153
|
+
_ensureLocalTraceDirectory(path);
|
|
145
154
|
// Throttle rotation stat checks to avoid unnecessary statSync calls
|
|
146
155
|
// on every flush. First flush (_lastRotateCheckMs === 0) always checks.
|
|
147
156
|
const now = Date.now();
|
|
148
|
-
|
|
157
|
+
// Explicit sinks can have many short-lived headless writers. Keep that
|
|
158
|
+
// shared file append-only so no writer can rename it out from under a
|
|
159
|
+
// peer or from session-bench while scoring.
|
|
160
|
+
if (!_localTraceIsExplicit
|
|
161
|
+
&& (_lastRotateCheckMs === 0 || now - _lastRotateCheckMs >= MIXDOG_AGENT_TRACE_ROTATE_CHECK_MS)) {
|
|
149
162
|
_rotateLocalTraceIfNeeded(path);
|
|
150
163
|
_lastRotateCheckMs = now;
|
|
151
164
|
}
|
|
@@ -183,8 +196,10 @@ function _flushLocalTraceSync() {
|
|
|
183
196
|
const chunk = _localTraceBuffer.join('');
|
|
184
197
|
_localTraceBuffer = [];
|
|
185
198
|
try {
|
|
199
|
+
_ensureLocalTraceDirectory(path);
|
|
186
200
|
const now = Date.now();
|
|
187
|
-
if (
|
|
201
|
+
if (!_localTraceIsExplicit
|
|
202
|
+
&& (_lastRotateCheckMs === 0 || now - _lastRotateCheckMs >= MIXDOG_AGENT_TRACE_ROTATE_CHECK_MS)) {
|
|
188
203
|
_rotateLocalTraceIfNeeded(path);
|
|
189
204
|
_lastRotateCheckMs = now;
|
|
190
205
|
}
|
|
@@ -185,6 +185,29 @@ function hasKeys(value) {
|
|
|
185
185
|
return !!value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
// Process-local write intent for project MCP overrides. A loaded config contains
|
|
189
|
+
// snapshots for every project, but only entries toggled by this process may
|
|
190
|
+
// override a newer value read from disk inside the save lock.
|
|
191
|
+
const dirtyMcpProjectOverrides = new Map();
|
|
192
|
+
let mcpProjectOverrideDirtySeq = 0;
|
|
193
|
+
function mcpProjectOverrideDirtyKey(projectKey, serverName) {
|
|
194
|
+
return `${String(projectKey)}\u0000${String(serverName)}`;
|
|
195
|
+
}
|
|
196
|
+
export function markMcpProjectOverrideDirty(projectKey, serverName, enabled) {
|
|
197
|
+
const pairKey = mcpProjectOverrideDirtyKey(projectKey, serverName);
|
|
198
|
+
dirtyMcpProjectOverrides.set(pairKey, {
|
|
199
|
+
enabled: enabled !== false,
|
|
200
|
+
seq: ++mcpProjectOverrideDirtySeq,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
function clearDurablySavedMcpProjectOverrides(appliedDirty) {
|
|
204
|
+
for (const { pairKey, seq } of appliedDirty) {
|
|
205
|
+
if (dirtyMcpProjectOverrides.get(pairKey)?.seq === seq) {
|
|
206
|
+
dirtyMcpProjectOverrides.delete(pairKey);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
188
211
|
function normalizeSearchRoute(route) {
|
|
189
212
|
if (!route || typeof route !== 'object' || Array.isArray(route))
|
|
190
213
|
return null;
|
|
@@ -456,6 +479,9 @@ export function loadConfig(options = {}) {
|
|
|
456
479
|
return {
|
|
457
480
|
providers: mergedProviders,
|
|
458
481
|
mcpServers,
|
|
482
|
+
mcpProjectOverrides: raw.mcpProjectOverrides && typeof raw.mcpProjectOverrides === 'object' && !Array.isArray(raw.mcpProjectOverrides)
|
|
483
|
+
? raw.mcpProjectOverrides
|
|
484
|
+
: {},
|
|
459
485
|
presets: normalizedPresets,
|
|
460
486
|
default: raw.default || null,
|
|
461
487
|
maintenance: { ...DEFAULT_MAINTENANCE, ...migratedMaint },
|
|
@@ -489,6 +515,7 @@ export function loadConfig(options = {}) {
|
|
|
489
515
|
return {
|
|
490
516
|
...defaults,
|
|
491
517
|
mcpServers: {},
|
|
518
|
+
mcpProjectOverrides: {},
|
|
492
519
|
presets: DEFAULT_PRESETS.map(p => ({ ...p })),
|
|
493
520
|
default: null,
|
|
494
521
|
maintenance: { ...DEFAULT_MAINTENANCE },
|
|
@@ -556,7 +583,7 @@ export async function patchSkillsDisabledAsync(disabledNames) {
|
|
|
556
583
|
return nextSkills;
|
|
557
584
|
}
|
|
558
585
|
|
|
559
|
-
function buildAgentSaveBuilder(config) {
|
|
586
|
+
function buildAgentSaveBuilder(config, appliedDirty) {
|
|
560
587
|
// Strip ephemeral defaults from providers but preserve any unknown
|
|
561
588
|
// per-provider subkey so future schema additions round-trip through the
|
|
562
589
|
// setup UI without changes here. apiKey is intentionally omitted —
|
|
@@ -590,40 +617,79 @@ function buildAgentSaveBuilder(config) {
|
|
|
590
617
|
// Build the replacement from `existingRaw` — the section read INSIDE the
|
|
591
618
|
// file lock — not a snapshot taken before it, so unmanaged keys written by
|
|
592
619
|
// a concurrent instance survive the save (lost-update guard).
|
|
593
|
-
return (existingRaw) =>
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
+
return (existingRaw) => {
|
|
621
|
+
const mcpServers = config.mcpServers || {};
|
|
622
|
+
const diskOverrides = existingRaw.mcpProjectOverrides && typeof existingRaw.mcpProjectOverrides === 'object'
|
|
623
|
+
? existingRaw.mcpProjectOverrides
|
|
624
|
+
: {};
|
|
625
|
+
const memoryOverrides = config.mcpProjectOverrides && typeof config.mcpProjectOverrides === 'object'
|
|
626
|
+
? config.mcpProjectOverrides
|
|
627
|
+
: {};
|
|
628
|
+
const mcpProjectOverrides = {};
|
|
629
|
+
for (const projectKey of new Set([...Object.keys(diskOverrides), ...Object.keys(memoryOverrides)])) {
|
|
630
|
+
const diskProject = diskOverrides[projectKey] && typeof diskOverrides[projectKey] === 'object'
|
|
631
|
+
? diskOverrides[projectKey]
|
|
632
|
+
: {};
|
|
633
|
+
const memoryProject = memoryOverrides[projectKey] && typeof memoryOverrides[projectKey] === 'object'
|
|
634
|
+
? memoryOverrides[projectKey]
|
|
635
|
+
: {};
|
|
636
|
+
// Disk wins for untouched snapshot entries. Only an override this
|
|
637
|
+
// process explicitly toggled may replace the in-lock disk value.
|
|
638
|
+
const mergedProject = { ...memoryProject, ...diskProject };
|
|
639
|
+
for (const serverName of Object.keys(memoryProject)) {
|
|
640
|
+
const pairKey = mcpProjectOverrideDirtyKey(projectKey, serverName);
|
|
641
|
+
const dirty = dirtyMcpProjectOverrides.get(pairKey);
|
|
642
|
+
if (dirty) {
|
|
643
|
+
const appliedValue = { enabled: dirty.enabled };
|
|
644
|
+
mergedProject[serverName] = appliedValue;
|
|
645
|
+
appliedDirty.push({ pairKey, seq: dirty.seq, appliedValue });
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
for (const serverName of Object.keys(mergedProject)) {
|
|
649
|
+
if (!Object.prototype.hasOwnProperty.call(mcpServers, serverName)) delete mergedProject[serverName];
|
|
650
|
+
}
|
|
651
|
+
if (Object.keys(mergedProject).length > 0) mcpProjectOverrides[projectKey] = mergedProject;
|
|
652
|
+
}
|
|
653
|
+
return {
|
|
654
|
+
...existingRaw,
|
|
655
|
+
guide: config.guide || existingRaw.guide || undefined,
|
|
656
|
+
providers: persistedProviders,
|
|
657
|
+
mcpServers,
|
|
658
|
+
mcpProjectOverrides,
|
|
659
|
+
presets,
|
|
660
|
+
default: config.default || null,
|
|
661
|
+
maintenance: config.maintenance || {},
|
|
662
|
+
workflowRoutes,
|
|
663
|
+
searchRoute: normalizeSearchRoute(config.searchRoute),
|
|
664
|
+
fastModels: config.fastModels || {},
|
|
665
|
+
modelSettings: config.modelSettings || {},
|
|
666
|
+
onboarding: config.onboarding || {},
|
|
667
|
+
agents: config.agents || {},
|
|
668
|
+
workflow: config.workflow || { active: 'default' },
|
|
669
|
+
agentMaintenance: config.agentMaintenance || {},
|
|
670
|
+
profile: normalizeProfileConfig(config.profile),
|
|
671
|
+
skills: normalizeSkillsConfig(config.skills),
|
|
672
|
+
autoClear: config.autoClear || {},
|
|
673
|
+
compaction: config.compaction || {},
|
|
674
|
+
trajectory: config.trajectory || {},
|
|
675
|
+
runtime: config.runtime || {},
|
|
676
|
+
shell: config.shell || {},
|
|
677
|
+
update: config.update || {},
|
|
678
|
+
recap: config.recap || {},
|
|
679
|
+
modules: config.modules || existingRaw.modules || {},
|
|
680
|
+
};
|
|
681
|
+
};
|
|
620
682
|
}
|
|
621
683
|
export function saveConfig(config) {
|
|
622
|
-
|
|
684
|
+
const appliedDirty = [];
|
|
685
|
+
persistAgentConfig(buildAgentSaveBuilder(config, appliedDirty));
|
|
686
|
+
clearDurablySavedMcpProjectOverrides(appliedDirty);
|
|
623
687
|
}
|
|
624
688
|
// Async twin used by the debounced config-save flush timer.
|
|
625
689
|
export async function saveConfigAsync(config) {
|
|
626
|
-
|
|
690
|
+
const appliedDirty = [];
|
|
691
|
+
await persistAgentConfigAsync(buildAgentSaveBuilder(config, appliedDirty));
|
|
692
|
+
clearDurablySavedMcpProjectOverrides(appliedDirty);
|
|
627
693
|
}
|
|
628
694
|
// --- Preset helpers ---
|
|
629
695
|
// preset shape: { id, name, type: 'agent', provider, model, effort?, fast?, tools? }
|
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
} from '../../../shared/markdown-frontmatter.mjs';
|
|
10
10
|
import { loadConfig, normalizeSkillsConfig } from '../config.mjs';
|
|
11
11
|
|
|
12
|
+
function skillsDisabled() {
|
|
13
|
+
return /^(?:1|true|on|yes)$/i.test(String(process.env.MIXDOG_DISABLE_SKILLS || ''));
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
// --- mixdog asset roots (standalone CLI owns its own paths; never .claude) ---
|
|
13
17
|
// Project-local: <cwd>/.mixdog/<kind>
|
|
14
18
|
// Data-local: <mixdogData>/<kind> (standalone default: ~/.mixdog/data/<kind>)
|
|
@@ -80,6 +84,7 @@ function pluginSkillDirs() {
|
|
|
80
84
|
* Full content loaded on demand via loadSkillContent().
|
|
81
85
|
*/
|
|
82
86
|
export function collectSkills(cwd) {
|
|
87
|
+
if (skillsDisabled()) return [];
|
|
83
88
|
// When cwd is null/missing (e.g. agent maintenance callers that pass
|
|
84
89
|
// cwd:null on purpose so provider-cache shards don't fork per caller
|
|
85
90
|
// workspace), skip project-scoped skills entirely — DO NOT fall back
|
|
@@ -144,6 +149,7 @@ export function isSkillDisabled(name, config = null) {
|
|
|
144
149
|
}
|
|
145
150
|
|
|
146
151
|
export function filterSkillsExcludingDisabled(skills, config = null) {
|
|
152
|
+
if (skillsDisabled()) return [];
|
|
147
153
|
const disabled = getDisabledSkillNameSet(config);
|
|
148
154
|
if (!disabled.size) return Array.isArray(skills) ? skills : [];
|
|
149
155
|
return (Array.isArray(skills) ? skills : []).filter((s) => {
|
|
@@ -160,6 +166,7 @@ const _skillsCache = new Map();
|
|
|
160
166
|
const _mtimeCache = new Map();
|
|
161
167
|
const _MTIME_TTL_MS = 2000;
|
|
162
168
|
export function collectSkillsCached(cwd) {
|
|
169
|
+
if (skillsDisabled()) return [];
|
|
163
170
|
const key = cwd ?? '';
|
|
164
171
|
const projectDir = (typeof cwd === 'string' && cwd.length > 0) ? cwd : null;
|
|
165
172
|
// Same mixdog-owned dirs collectSkills() reads, used as the freshness gate.
|
|
@@ -278,6 +285,7 @@ function compactSkillManifestText(value, max = 180) {
|
|
|
278
285
|
* Full SKILL.md content is still loaded only through Skill(name).
|
|
279
286
|
*/
|
|
280
287
|
export function buildSkillManifest(skills, { limit = 80 } = {}) {
|
|
288
|
+
if (skillsDisabled()) return '';
|
|
281
289
|
const list = (Array.isArray(skills) ? skills : [])
|
|
282
290
|
.map((skill) => ({
|
|
283
291
|
name: String(skill?.name || '').trim(),
|
|
@@ -507,6 +515,7 @@ let _skillToolDefsCache = null;
|
|
|
507
515
|
* behaviour.
|
|
508
516
|
*/
|
|
509
517
|
export function buildSkillToolDefs(skills, { ownerIsAgentSession = false } = {}) {
|
|
518
|
+
if (skillsDisabled()) return [];
|
|
510
519
|
if (!ownerIsAgentSession && !skills.length) return [];
|
|
511
520
|
if (_skillToolDefsCache) return _skillToolDefsCache;
|
|
512
521
|
_skillToolDefsCache = [
|