mixdog 0.9.142 → 0.9.144

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/package.json +1 -1
  2. package/scripts/agent-lead-e2e-probe.mjs +1 -6
  3. package/scripts/agent-turn-trace-probe.mjs +3 -5
  4. package/src/lib/rules-builder.cjs +44 -15
  5. package/src/rules/lead/lead-brief.md +7 -8
  6. package/src/rules/shared/10-tool-workflow.md +28 -8
  7. package/src/rules/shared/20-research.md +4 -0
  8. package/src/rules/shared/30-exploration.md +23 -35
  9. package/src/rules/shared/40-editing.md +4 -0
  10. package/src/rules/shared/50-execution.md +1 -0
  11. package/src/rules/shared/60-verification.md +9 -0
  12. package/src/rules/shared/70-delivery.md +1 -2
  13. package/src/rules/shared/80-memory.md +6 -2
  14. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +0 -39
  15. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +28 -22
  16. package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +22 -1
  17. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -2
  18. package/src/runtime/agent/orchestrator/session/evidence-union.test.mjs +1 -1
  19. package/src/runtime/agent/orchestrator/session/image-strip-recovery.test.mjs +61 -0
  20. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +1 -16
  21. package/src/runtime/agent/orchestrator/session/token-native.mjs +40 -40
  22. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +10 -10
  23. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -19
  24. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +17 -37
  25. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +1 -32
  26. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -15
  27. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +1 -1
  28. package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +10 -10
  29. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +7 -7
  30. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +6 -6
  31. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
  32. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  33. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +4 -9
  34. package/src/runtime/shared/child-spawn-remote.mjs +7 -8
  35. package/src/runtime/shared/resource-admission.mjs +20 -0
  36. package/src/runtime/shared/resource-admission.test.mjs +43 -0
  37. package/src/runtime/shared/{session-shard-health.mjs → session-runtime-health.mjs} +14 -14
  38. package/src/runtime/shared/session-runtime-health.test.mjs +30 -0
  39. package/src/session-runtime/agent-disable.test.mjs +5 -3
  40. package/src/session-runtime/runtime-core.mjs +21 -41
  41. package/src/session-runtime/session-lifecycle.mjs +0 -32
  42. package/src/session-runtime/session-turn-api.mjs +1 -1
  43. package/src/session-runtime/tool-policy-surface.test.mjs +64 -11
  44. package/src/session-runtime/workflow.mjs +8 -3
  45. package/src/standalone/agent-tool/helpers.mjs +1 -1
  46. package/src/standalone/agent-tool/spawn-flow.mjs +1 -30
  47. package/src/standalone/agent-tool.mjs +1 -7
  48. package/src/standalone/channel-worker.mjs +3 -3
  49. package/src/standalone/daemon.mjs +14 -18
  50. package/src/standalone/session-client.mjs +1 -1
  51. package/src/standalone/{session-runtime-pool-health.test.mjs → session-runtime-host-health.test.mjs} +22 -23
  52. package/src/standalone/{session-runtime-pool.mjs → session-runtime-host.mjs} +69 -291
  53. package/src/standalone/session-runtime-record.mjs +2 -2
  54. package/src/standalone/session-runtime-worker.mjs +20 -21
  55. package/src/standalone/session-service.mjs +3 -28
  56. package/src/standalone/session-state-patch.mjs +1 -1
  57. package/src/standalone/session-transport.mjs +1 -1
  58. package/src/tui/session/turn.mjs +5 -0
  59. package/src/tui/session-local.mjs +1 -3
  60. package/src/workflows/default/WORKFLOW.md +5 -4
  61. package/src/workflows/solo/WORKFLOW.md +3 -5
  62. package/scripts/agent-long-prompt-repro.mjs +0 -82
  63. package/scripts/agent-shard-spread-perf.mjs +0 -245
  64. package/scripts/fixtures/session-shard-fixture-worker.mjs +0 -13
  65. package/src/runtime/shared/session-shard-health.test.mjs +0 -30
  66. package/src/standalone/agent-tool/shard-spread.mjs +0 -709
  67. package/src/standalone/agent-tool/shard-spread.test.mjs +0 -68
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.142",
3
+ "version": "0.9.144",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -15,12 +15,7 @@ import { homedir, tmpdir } from 'node:os';
15
15
  import { join } from 'node:path';
16
16
 
17
17
  const ROOT = mkdtempSync(join(tmpdir(), 'mixdog-lead-e2e-'));
18
- // MIXDOG_LEAD_E2E_LIVE_DAEMON=1: keep the inherited runtime root (live daemon
19
- // discovery) and opt into agent shard spread, so the spawned worker runs on
20
- // the INSTALLED daemon's shard pool — the deployed remote-completion path.
21
- const LIVE_DAEMON = process.env.MIXDOG_LEAD_E2E_LIVE_DAEMON === '1';
22
- if (LIVE_DAEMON) process.env.MIXDOG_AGENT_SHARD_SPREAD = '1';
23
- else process.env.MIXDOG_RUNTIME_ROOT = ROOT;
18
+ process.env.MIXDOG_RUNTIME_ROOT = ROOT;
24
19
  process.env.MIXDOG_BOOT_CORE_MEMORY = '0';
25
20
  process.env.MIXDOG_DAEMON_SKIP_MEMORY = '1';
26
21
  process.env.MIXDOG_FEATURE_MEMORY = '0';
@@ -17,9 +17,7 @@ const ROOT = mkdtempSync(join(tmpdir(), 'mixdog-turn-trace-'));
17
17
  process.env.MIXDOG_RUNTIME_ROOT = ROOT;
18
18
  process.env.MIXDOG_DAEMON_SKIP_MEMORY = '1';
19
19
  process.env.MIXDOG_BOOT_CORE_MEMORY = '0';
20
- process.env.MIXDOG_AGENT_SHARD_SPREAD = '1';
21
- // The probe's whole point is the trace: explicit path (shared by daemon and
22
- // shard children via env inheritance) + timing rows.
20
+ // The probe's whole point is the trace: explicit path plus timing rows.
23
21
  const TRACE_PATH = join(ROOT, 'agent-trace.jsonl');
24
22
  delete process.env.MIXDOG_AGENT_TRACE_DISABLE;
25
23
  process.env.MIXDOG_AGENT_TRACE_PATH = TRACE_PATH;
@@ -75,7 +73,7 @@ try {
75
73
  '2) read README.md (first 40 lines)',
76
74
  '3) run the shell command: node -v',
77
75
  '4) read apps/desktop/package.json',
78
- '5) grep the string "createSessionRuntimePool" under src/standalone (files list only)',
76
+ '5) grep the string "createSessionRuntimeHost" under src/standalone (files list only)',
79
77
  'Then reply with one line: DONE <package name> <node version>. Do not edit anything.',
80
78
  ].join('\n'),
81
79
  }, { invocationSource: 'model-tool', cwd: REPO });
@@ -90,7 +88,7 @@ try {
90
88
  process.stdout.write(`wall=${((Date.now() - t0) / 1000).toFixed(1)}s\n--- result ---\n${last.slice(0, 400)}\n`);
91
89
  try { agent.closeAll('turn-trace probe end'); } catch { /* teardown */ }
92
90
 
93
- // Shard children flush their local trace buffers on a short timer.
91
+ // The runtime worker flushes its local trace buffer on a short timer.
94
92
  await sleep(9_000);
95
93
  const rows = existsSync(TRACE_PATH)
96
94
  ? readFileSync(TRACE_PATH, 'utf8').split('\n').filter(Boolean).flatMap((line) => {
@@ -202,12 +202,32 @@ function stripFrontmatter(markdown) {
202
202
  return String(markdown || '').replace(/^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n|$)/, '').trim();
203
203
  }
204
204
 
205
- const WEB_SEARCH_ROUTE_RE = /^[ \t]*current or external information discovery→`web_search`;[ \t]*\r?\n?/gm;
206
- const WEB_FETCH_ROUTE_RE = /^[ \t]*page or documentation body retrieval from a known URL→`web_fetch`\.[ \t]*\r?\n?/gm;
207
- const RECALL_ROUTE_RE = /^-[ \t]*past facts recorded in prior work or sessions→`recall`[ \t]*\r?\n[ \t]*\(stored history only, never current local state\)\.[ \t]*\r?\n?/gm;
208
- const MEMORY_ROUTE_RE = /^-[ \t]*Durable memory creation or update→`memory`; store a compact English[ \t]*\r?\n[ \t]*statement\.[\s\S]*$/m;
209
- const EMPTY_RESEARCH_RE = /^# Research[ \t]*\r?\n(?:[ \t]*\r?\n)*-[ \t]*Research routes:[ \t]*\r?\n?/gm;
210
- const EMPTY_MEMORY_RE = /^# Memory[ \t]*\r?\n(?:[ \t]*\r?\n)*/gm;
205
+ // Tool dependency is declared as metadata, not matched against prose. A
206
+ // `<!-- tools: a, b -->` marker binds the block that follows it: the block
207
+ // survives while any listed tool is on the session surface and disappears
208
+ // once every one of them is omitted. Markers never reach the model.
209
+ const TOOL_MARKER_RE = /^[ \t]*<!--[ \t]*tools:[ \t]*([^>]*?)[ \t]*-->[ \t]*$/;
210
+
211
+ function markerTools(line) {
212
+ const match = TOOL_MARKER_RE.exec(String(line ?? ''));
213
+ if (!match) return null;
214
+ return match[1].split(',').map((name) => name.trim().toLowerCase()).filter(Boolean);
215
+ }
216
+
217
+ // A marked block runs from the line after the marker through every deeper
218
+ // indented continuation line, ending at the next marker, blank line, or a
219
+ // line at the same or shallower indent.
220
+ function markedBlockEnd(lines, start) {
221
+ const indent = lines[start].search(/\S/);
222
+ let end = start + 1;
223
+ while (end < lines.length) {
224
+ const line = lines[end];
225
+ if (!line.trim() || markerTools(line)) break;
226
+ if (line.search(/\S/) <= indent) break;
227
+ end += 1;
228
+ }
229
+ return end;
230
+ }
211
231
 
212
232
  function omitKeySet(omitTools) {
213
233
  return new Set((Array.isArray(omitTools) ? omitTools : []).map((name) => String(name || '').toLowerCase()).filter(Boolean));
@@ -216,16 +236,25 @@ function omitKeySet(omitTools) {
216
236
  /** Drop routing clauses for tools that are not on the session surface. */
217
237
  function omitToolRoutes(text, omitTools = []) {
218
238
  const deny = omitKeySet(omitTools);
219
- let out = String(text || '');
220
- if (deny.has('web_search')) out = out.replace(WEB_SEARCH_ROUTE_RE, '');
221
- if (deny.has('web_fetch')) out = out.replace(WEB_FETCH_ROUTE_RE, '');
222
- if (deny.has('recall')) out = out.replace(RECALL_ROUTE_RE, '');
223
- if (deny.has('memory')) out = out.replace(MEMORY_ROUTE_RE, '');
224
- if (deny.has('web_search') && deny.has('web_fetch')) {
225
- out = out.replace(EMPTY_RESEARCH_RE, '');
239
+ const lines = String(text || '').split(/\r?\n/);
240
+ const kept = [];
241
+ let index = 0;
242
+ while (index < lines.length) {
243
+ const tools = markerTools(lines[index]);
244
+ if (!tools) {
245
+ kept.push(lines[index]);
246
+ index += 1;
247
+ continue;
248
+ }
249
+ index += 1;
250
+ if (index >= lines.length) break;
251
+ const end = markedBlockEnd(lines, index);
252
+ if (!tools.length || !tools.every((name) => deny.has(name))) {
253
+ kept.push(...lines.slice(index, end));
254
+ }
255
+ index = end;
226
256
  }
227
- if (deny.has('recall') && deny.has('memory')) out = out.replace(EMPTY_MEMORY_RE, '');
228
- return out.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n');
257
+ return kept.join('\n').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
229
258
  }
230
259
 
231
260
  // Framing line under the style header: the block owns user-facing prose only,
@@ -1,13 +1,12 @@
1
1
  # Lead Brief
2
2
 
3
- - Minimum chars, maximum info: one-line fragments. Every role's `Task:` is
4
- mandatory and lossless build it from the original request and the official
5
- spec/test acceptance criteria, preserving intent, required and forbidden
6
- outcomes, completion/stop boundary, user-supplied exact targets, and exact
7
- replacements/outputs. Never infer exactness from task name, file count, or
8
- difficulty.
9
- - Omit role-known rules, repeated context/facts, and padding; split scope
10
- without discarding requirements.
3
+ - Every role's `Task:` is mandatory and lossless — build it from the original
4
+ request and the official spec/test acceptance criteria, preserving intent,
5
+ required and forbidden outcomes, completion/stop boundary, user-supplied
6
+ exact targets, and exact replacements/outputs.
7
+ - Never infer exactness from task name, file count, or difficulty.
8
+ - Minimum chars, maximum info: one-line fragments, no role-known rules, no
9
+ repeated context or facts, no padding.
11
10
  - Other fields are task-specific deltas — `Anchors:` (`file:line` plus a
12
11
  one-line conclusion, never log/code bodies), `Allow/Forbid:`, `Deliver:`
13
12
  (sets handoff shape/size); omit empty fields. State outcomes, not methods,
@@ -1,20 +1,40 @@
1
1
  # Tool Workflow
2
2
 
3
- - Minimize tool turns through maximal useful parallelism: in each turn, issue
4
- every necessary non-overlapping call whose inputs are already known.
5
- - Defer a call only when its inputs depend on an earlier result; never add
6
- duplicate or irrelevant calls merely to increase fanout.
7
- - Apply one analysis to many targets as one parameterized call when supported,
8
- not one call per target.
3
+ - Investigate, build, and verify only what the requested outcome requires, at
4
+ the level it requires; trust internal and framework guarantees.
5
+ - Minimize tool turns through maximal useful parallelism. Cost is counted in
6
+ rounds, not calls: a batch is one round, so a call-count saving never
7
+ justifies a worse-routed call. Plan the fewest evidence-complete dependent
8
+ rounds first, then the fewest calls within each round.
9
+ - In each round, issue every necessary non-overlapping call whose inputs are
10
+ already known; defer a call only when its target or arguments require an
11
+ earlier result.
12
+ - Route each remaining evidence facet once to its primary owner, preferring the
13
+ operation that directly returns the evidence needed for the next decision. A
14
+ summary, overview, or enumeration is not a prerequisite when that operation's
15
+ complete inputs are already known; if independently required, batch it with
16
+ the detailed operation.
17
+ - Never duplicate a facet, widen retrieval speculatively, or cap fanout; apply
18
+ one analysis to many targets as one parameterized call when supported.
9
19
  1. Determine the required outcome and missing information; requirements are
10
20
  not evidence.
11
21
  2. If needed, gather only missing information through Research or Exploration;
12
22
  use Execution when the information can only be produced by running a program
13
- or observing runtime state. Stop when it is already known or sufficiently
14
- obtained.
23
+ or observing runtime state.
15
24
  3. Perform the required answer, edit, or execution in the fewest safe coherent
16
25
  calls.
17
26
  4. Verify only affected facets and essential invariants when required.
27
+ - Known state — system guarantees, supplied facts, visible tool returns,
28
+ applied patches, and passed checks — is never re-found, re-derived, or
29
+ re-verified at any granularity: no re-query call, no confirmation subcommand
30
+ inside a shell command, no availability probe for what the operation itself
31
+ would report, no reopening a file to confirm an edit, no rerun of a passed
32
+ check.
33
+ - Mine each returned result fully before opening the next round. A follow-up
34
+ is valid only for evidence a result omitted, invalidated, or newly made
35
+ necessary; an independently required call that no result created belonged in
36
+ the earlier batch.
37
+ - Evidence that determines the answer, edit, or deliverable ends retrieval.
18
38
  - Treat failure as new evidence and repeat steps 1–4 only for affected facets.
19
39
  Report a blocker when no deterministic next action remains.
20
40
  - Use only named tools present in the current tool surface.
@@ -1,6 +1,10 @@
1
+ <!-- tools: web_search, web_fetch -->
1
2
  # Research
2
3
 
4
+ <!-- tools: web_search, web_fetch -->
3
5
  - Research routes:
6
+ <!-- tools: web_search -->
4
7
  current or external information discovery→`web_search`;
8
+ <!-- tools: web_fetch -->
5
9
  page or documentation body retrieval from a known URL→`web_fetch`.
6
10
 
@@ -2,43 +2,31 @@
2
2
 
3
3
  - Use read-only means for inspection; never mutate to clear an obstacle or
4
4
  unexpected state. Preserve evidence before a required mutation can destroy it.
5
- - Local Project exploration routes:
6
- unknown file/directory location, paths only needed→`find`;
7
- wildcard/recursive paths→`glob` (including known-root unknown descendants);
5
+ - Ownership is exclusive: each evidence type has one owner, another tool's
6
+ ability to reach the same target is never an alternative route, and a
7
+ successful owner result closes that facet — only a different evidence type
8
+ routes elsewhere.
9
+ - Route the missing evidence to its primary owner:
10
+ repository state, history, or diff→`git`;
11
+ exact symbol declaration, body, usage, or relation→`code_graph`;
12
+ literal, regex, or text location→`grep`;
13
+ known-file content, range, or image→`read`;
14
+ wildcard or recursive file paths→`glob`;
8
15
  known directory's immediate entries→`list`;
9
- exact symbol, body, or relation→`code_graph`
10
- (identifier declarations/usages→`code_graph`; literal values/strings→`grep`);
11
- literal/regex pattern search within file contents→`grep`;
12
- content or an anchored line range from a known file when pattern search is
13
- insufficient or unnecessary→`read`.
14
- - Read-only tools — `find`, `glob`, `list`, `grep`, `code_graph`, `read` —
15
- always batch safely in parallel.
16
- - Paths reachable by expanding an environment variable or the home directory
17
- are resolved locations, not unknowns.
18
- - In the first response, launch all investigations knowable from the request
19
- alone (enumeration, content probes, file samples) as one batch; each
20
- follow-up batch exists only for questions the previous results created.
21
- - Batching never licenses a guessed `glob.path`
22
- (unknown location → `find` first; omit path for the current Project).
16
+ unknown file or directory location→`find`.
17
+ - Use a path locator only when the owner's required target is unknown. Paths
18
+ reachable by expanding an environment variable or the home directory are
19
+ resolved locations, not unknowns.
23
20
  - Enumerate sibling directories or same-kind files with one wildcard call
24
21
  (`glob`, or `read` with a glob for content sampling), never a
25
22
  directory-by-directory `list` walk or one `read` per file.
26
- - Before choosing an implementation, inspect only the nearest relevant code,
27
- configuration, and established pattern needed to verify local conventions or
28
- dependency availability.
29
-
30
- - Requirements define what must be true; evidence establishes what is true.
31
- Never use one as the other. Treat supplied target locations as resolved;
32
- access them directly without locator searches. Before deciding how to parse,
33
- count, transform, or summarize files whose format has not been inspected,
34
- inspect the original content itself. Within the current project, pass project-relative
35
- paths and omit optional scopes equal to its root; explicit paths may be
36
- outside cwd only for targets outside the project.
37
- - Do not re-read content already returned by any tool or reopen a successfully
38
- edited file solely to confirm the edit. Read only missing context or content
39
- invalidated by a reported failure, partial operation, or external change.
40
- - `code_graph references` supplies the declaration and scoped usages and ends
41
- that facet; values/locations end at the context `grep` returns; `read` covers
42
- only omitted lines or missing anchored ranges. Any visible returned span can
43
- supply exact source context; do not fetch it again.
23
+ - Treat supplied target locations as resolved; access them directly without
24
+ locator searches. Within the current project, pass project-relative paths and
25
+ omit optional scopes equal to its root; explicit paths may be outside cwd
26
+ only for targets outside the project.
27
+ - Before deciding how to parse, count, transform, or summarize files whose
28
+ format has not been inspected, inspect the original content itself.
29
+ - Returned declarations, bodies, usages, relations, and contextual spans from
30
+ any tool not only `read` are source context; `read` covers only omitted
31
+ lines or missing anchored ranges.
44
32
 
@@ -1,11 +1,15 @@
1
1
  # Editing
2
2
 
3
+ - A required new file is created directly: Add File is itself the atomic
4
+ absence check, so inspect only if it reports the target already exists.
3
5
  - Source: use exact current target text from any visible evidence, including
4
6
  user input, tool output, or an applied edit result; never reconstruct it from
5
7
  another file, a sample, or expectation.
6
8
  - Placement: with `edit`, use an exact unique target string, expanding exact
7
9
  surrounding text when needed; with `apply_patch`, use exact unchanged context
8
10
  and add a class/function locator when context alone is not unique.
11
+ - Apply all determined changes in the fewest safe calls the active tool
12
+ supports; a file written in one call is written complete.
9
13
  - Batch scope: never split one file across concurrent edit calls. Group
10
14
  same-intent changes with exact context into coherent calls; issue disjoint
11
15
  calls together in one turn, and defer ambiguous or result-dependent changes.
@@ -2,4 +2,5 @@
2
2
 
3
3
  - Evidence or artifacts available only through program execution, calculation,
4
4
  data transformation, generated output, or unsupported-format decoding→`shell`;
5
+ an already-open shell is never a routing reason.
5
6
 
@@ -5,6 +5,15 @@
5
5
  invariants; use an umbrella suite only when the user explicitly requests it
6
6
  or a documented project or release process requires it.
7
7
  - Issue all independent checks in one turn.
8
+ - Blocking checks cover only essential integrity, security, compatibility, and
9
+ buildability invariants. Treat mutable behavior, UX, exact text, snapshots,
10
+ and implementation shape as advisory specifications; update them when the
11
+ requested behavior changes instead of preserving obsolete behavior.
12
+ - A check runs at the strictness the task requires; never raise a tool's own
13
+ severity beyond it.
8
14
  - If verification fails, collect all failures, leave Verification, complete all
9
15
  determinable fixes, then re-enter Verification for the resulting state.
16
+ - A successful verification closes the task unless later changes affect it;
17
+ rerun a failed action only after its inputs or subject change, otherwise
18
+ report it unresolved.
10
19
 
@@ -2,5 +2,4 @@
2
2
 
3
3
  - Commit, push, release, and deployment happen only on the user's explicit
4
4
  request.
5
- - Repository state or history explicitly asked about, and every repository
6
- mutation→`git`; never part of exploration batching.
5
+ - Every repository mutation→`git`.
@@ -1,11 +1,15 @@
1
+ <!-- tools: recall, memory -->
1
2
  # Memory
2
3
 
4
+ <!-- tools: recall -->
3
5
  - past facts recorded in prior work or sessions→`recall`
4
6
  (stored history only, never current local state).
7
+ <!-- tools: memory -->
5
8
  - Durable memory creation or update→`memory`; store a compact English
6
9
  statement.
10
+ <!-- tools: memory -->
7
11
  - Use judgment to decide whether a durable memory should be stored, whether
8
12
  user confirmation is needed, and which scope best fits the context.
13
+ <!-- tools: memory -->
9
14
  - Omit `project_id` for the current Project, use `"common"` for shared memory,
10
- or provide an explicit Project slug for another named Project. `*` is
11
- read-only.
15
+ or an explicit Project slug for another named Project; `*` is read-only.
@@ -334,45 +334,6 @@ export function makeAgentDispatch(opts = {}) {
334
334
  // pooled or resumed. Cache prefix matching happens at the provider
335
335
  // layer (account-level), not the session level.
336
336
  const finalPrompt = prompt;
337
- // Agent shard spread: inside a shard child the ephemeral hidden-role
338
- // session runs on a peer shard through the daemon session protocol —
339
- // the same single spawn path the agent tool uses. ONE path per mode:
340
- // a remote failure fails the dispatch (no in-process fallback).
341
- const spreadMod = await import('../../../../standalone/agent-tool/shard-spread.mjs')
342
- .catch(() => null);
343
- if (spreadMod?.agentShardSpreadEnabled?.()) {
344
- const raw = await spreadMod.dispatchHiddenAgentRemote({
345
- spec: {
346
- agent,
347
- presetName,
348
- preset,
349
- runtimeSpec,
350
- permission,
351
- cwd,
352
- sourceType: opts.sourceType,
353
- sourceName: sourceNameArg || opts.sourceName,
354
- parentSessionId: opts.parentSessionId || null,
355
- ownerSessionId: opts.ownerSessionId === undefined ? (opts.parentSessionId || null) : opts.ownerSessionId,
356
- clientHostPid: opts.clientHostPid,
357
- skipRoleReminder: isPoolC,
358
- schemaAllowedTools: resolveHiddenRoleSchemaAllowedTools(hidden),
359
- taskType: opts.taskType,
360
- maxLoopIterations: opts.maxLoopIterations,
361
- },
362
- provider: preset.provider,
363
- model: preset.model,
364
- cwd,
365
- prompt: finalPrompt,
366
- parentSignals: [opts.parentSignal, callParentSignal],
367
- watchdogPolicy: resolveAgentWatchdogPolicy(agent, {
368
- idleTimeoutMs: Number.isFinite(callIdleTimeoutMs)
369
- ? callIdleTimeoutMs
370
- : opts.idleTimeoutMs,
371
- firstResponseTimeoutMs: opts.firstResponseTimeoutMs,
372
- }),
373
- });
374
- return opts.brief === false ? raw : applyBriefCap(raw);
375
- }
376
337
  const { session } = prepareAgentSession({
377
338
  agent,
378
339
  presetName,
@@ -1,5 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { AsyncLocalStorage } from 'node:async_hooks';
3
+ import { resourceAdmission } from '../../../shared/resource-admission.mjs';
3
4
 
4
5
  // Normal provider traffic is not concurrency-gated. Independent sessions and
5
6
  // accounts start immediately; a finite limit exists only when an operator
@@ -577,28 +578,33 @@ export function wrapProviderAdmission(provider, providerName, scheduler = provid
577
578
  const opts = sendOpts || {};
578
579
  const signal = opts.signal || null;
579
580
  const key = providerAdmissionKey(providerName, this);
580
- return scheduler.run(key, (admissionSignal) => {
581
- // Admission is the common request-clock boundary for WS/SSE/HTTP.
582
- // Queue wait therefore cannot consume first-byte or agent-watchdog
583
- // time. Provider-local retry remains the sole retry owner.
584
- try { opts.onStageChange?.('requesting'); } catch {}
585
- return originalSend.call(this, messages, model, tools, {
586
- ...opts,
587
- signal: admissionSignal,
588
- });
589
- }, {
590
- signal,
591
- ownerKey: opts.admissionOwner || opts.sessionId || null,
592
- priority: opts.admissionPriority || 'user-visible',
593
- onCooldownWait: (waitMs) => {
594
- // Display-only: the TUI/desktop 'reconnecting' stage already
595
- // renders a custom verb, so no new stage vocabulary is needed.
596
- const secs = Math.max(1, Math.ceil(waitMs / 1000));
597
- try {
598
- opts.onStageChange?.('reconnecting', { message: `Rate-limited waiting ~${secs}s for the provider window` });
599
- } catch { /* display-only */ }
600
- },
601
- });
581
+ // Provider queueing and network wait consume no local CPU slot. The
582
+ // resource controller reacquires the agent lease before model output
583
+ // continues through local context/tool processing.
584
+ return resourceAdmission.runYielded(() =>
585
+ scheduler.run(key, (admissionSignal) => {
586
+ // Admission is the common request-clock boundary for WS/SSE/HTTP.
587
+ // Queue wait therefore cannot consume first-byte or agent-watchdog
588
+ // time. Provider-local retry remains the sole retry owner.
589
+ try { opts.onStageChange?.('requesting'); } catch {}
590
+ return originalSend.call(this, messages, model, tools, {
591
+ ...opts,
592
+ signal: admissionSignal,
593
+ });
594
+ }, {
595
+ signal,
596
+ ownerKey: opts.admissionOwner || opts.sessionId || null,
597
+ priority: opts.admissionPriority || 'user-visible',
598
+ onCooldownWait: (waitMs) => {
599
+ // Display-only: the TUI/desktop 'reconnecting' stage already
600
+ // renders a custom verb, so no new stage vocabulary is needed.
601
+ const secs = Math.max(1, Math.ceil(waitMs / 1000));
602
+ try {
603
+ opts.onStageChange?.('reconnecting', { message: `Rate-limited — waiting ~${secs}s for the provider window` });
604
+ } catch { /* display-only */ }
605
+ },
606
+ })
607
+ );
602
608
  };
603
609
  return provider;
604
610
  }
@@ -65,6 +65,26 @@ function firstExclusiveRequired(branches) {
65
65
  return [];
66
66
  }
67
67
 
68
+ const ARRAY_DROP_NOTE = 'This provider accepts a single value here, not an array.';
69
+
70
+ function describesArray(schema) {
71
+ if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return false;
72
+ return schema.type === 'array' || (Array.isArray(schema.type) && schema.type.includes('array'));
73
+ }
74
+
75
+ // Flattening keeps one branch, so a description that still promises the dropped
76
+ // shape would advertise more than the wire schema accepts. Project the loss
77
+ // into the text the model actually reads.
78
+ function projectDroppedBranches(schema, dropped) {
79
+ if (describesArray(schema) || !dropped.some(describesArray)) return schema;
80
+ const description = String(schema.description || '').trim();
81
+ if (description.includes(ARRAY_DROP_NOTE)) return schema;
82
+ return {
83
+ ...schema,
84
+ description: description ? `${description} ${ARRAY_DROP_NOTE}` : ARRAY_DROP_NOTE,
85
+ };
86
+ }
87
+
68
88
  function normalizeGrokPropertySchema(schema) {
69
89
  if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return schema;
70
90
  const branches = [
@@ -75,7 +95,8 @@ function normalizeGrokPropertySchema(schema) {
75
95
  const first = branches.find(branch => branch && typeof branch === 'object' && !Array.isArray(branch));
76
96
  if (first) {
77
97
  const { anyOf: _anyOf, oneOf: _oneOf, ...siblings } = schema;
78
- return normalizeGrokPropertySchema({ ...first, ...siblings });
98
+ const dropped = branches.filter(branch => branch !== first);
99
+ return normalizeGrokPropertySchema(projectDroppedBranches({ ...first, ...siblings }, dropped));
79
100
  }
80
101
  }
81
102
  if (!schema.properties || typeof schema.properties !== 'object') return schema;
@@ -203,7 +203,13 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
203
203
  };
204
204
  const sessionRef = opts.session || null;
205
205
  let _providerPrefixGuardState = sessionRef?._providerPrefixGuardState || null;
206
- let _fixedProviderToolSurface = sessionRef?._providerToolSurfaceSnapshot || null;
206
+ // Provider tool snapshots are request-loop state, never durable session
207
+ // state. Older builds persisted this field, which let a resumed session
208
+ // keep advertising a retired schema even after session.tools was rebuilt.
209
+ if (sessionRef && Object.prototype.hasOwnProperty.call(sessionRef, '_providerToolSurfaceSnapshot')) {
210
+ delete sessionRef._providerToolSurfaceSnapshot;
211
+ }
212
+ let _fixedProviderToolSurface = null;
207
213
  const loopUsageMetricsEpoch = () => Number(sessionRef?.usageMetricsEpoch) || 0;
208
214
  const loopUsageMetricsTurnId = () => Number(sessionRef?.usageMetricsTurnId) || 0;
209
215
  // Sub-agent (worker/heavy-worker/reviewer/…) sessions
@@ -496,7 +502,6 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
496
502
  });
497
503
  if (!_fixedProviderToolSurface) {
498
504
  _fixedProviderToolSurface = _candidateSendTools;
499
- if (sessionRef) sessionRef._providerToolSurfaceSnapshot = _fixedProviderToolSurface;
500
505
  }
501
506
  sendTools = _fixedProviderToolSurface;
502
507
  requestToolScope = {
@@ -142,7 +142,7 @@ test('apply_patch, shell, and mutating git batches invalidate all earlier eviden
142
142
  ['apply_patch', {}],
143
143
  ['shell', {}],
144
144
  ['git', { command: 'git commit -m test' }],
145
- ['git', { command: "git reflog delete 'HEAD@{1}'", confirm: true }],
145
+ ['git', { command: "git reflog delete 'HEAD@{1}'" }],
146
146
  ]) {
147
147
  const messages = [
148
148
  call('read_1', 'read', { file_path: 'src/a.mjs' }),
@@ -164,6 +164,67 @@ test('agent loop heals one rejected tail image and the next turn stays usable',
164
164
  assert.equal(nextCalls, 1);
165
165
  });
166
166
 
167
+ test('agent loop keeps provider tool snapshots turn-local', async () => {
168
+ const oldTool = {
169
+ name: 'shell',
170
+ description: 'old schema',
171
+ inputSchema: { type: 'object', properties: { retired: { type: 'boolean' } } },
172
+ };
173
+ const currentTool = {
174
+ name: 'shell',
175
+ description: 'current schema',
176
+ inputSchema: { type: 'object', properties: { command: { type: 'string' } } },
177
+ };
178
+ const nextTool = {
179
+ name: 'shell',
180
+ description: 'next schema',
181
+ inputSchema: { type: 'object', properties: { command: { type: 'string' }, timeout_ms: { type: 'number' } } },
182
+ };
183
+ const sentToolDescriptions = [];
184
+ const provider = {
185
+ async send(_messages, _model, tools) {
186
+ sentToolDescriptions.push(tools?.[0]?.description);
187
+ return { content: 'done', toolCalls: [], stopReason: 'end_turn' };
188
+ },
189
+ };
190
+ const session = {
191
+ id: 'provider-tool-snapshot-lifetime-test',
192
+ owner: 'cli',
193
+ contextWindow: 200_000,
194
+ rawContextWindow: 200_000,
195
+ compaction: { auto: false },
196
+ _providerToolSurfaceSnapshot: [oldTool],
197
+ };
198
+ const messages = [
199
+ { role: 'system', content: 'system' },
200
+ { role: 'user', content: 'first turn' },
201
+ ];
202
+
203
+ await agentLoop(
204
+ provider,
205
+ messages,
206
+ 'fake-model',
207
+ [currentTool],
208
+ null,
209
+ process.cwd(),
210
+ { session, sessionId: session.id },
211
+ );
212
+ assert.equal(Object.hasOwn(session, '_providerToolSurfaceSnapshot'), false);
213
+
214
+ messages.push({ role: 'user', content: 'next turn' });
215
+ await agentLoop(
216
+ provider,
217
+ messages,
218
+ 'fake-model',
219
+ [nextTool],
220
+ null,
221
+ process.cwd(),
222
+ { session, sessionId: session.id },
223
+ );
224
+
225
+ assert.deepEqual(sentToolDescriptions, ['current schema', 'next schema']);
226
+ });
227
+
167
228
  test('mid-stream xAI generation crash is retryable even as invalid_request_error', () => {
168
229
  const err = new Error('xAI Responses stream error: Internal error during token generation');
169
230
  err.providerWireError = true;