mixdog 0.9.110 → 0.9.111

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 (78) hide show
  1. package/package.json +1 -2
  2. package/scripts/routing-corpus.mjs +5 -6
  3. package/scripts/run-suite.mjs +0 -1
  4. package/src/app.mjs +0 -1
  5. package/src/defaults/agents.json +0 -12
  6. package/src/defaults/skills/setup/SKILL.md +1 -1
  7. package/src/headless-command.mjs +1 -3
  8. package/src/headless-role.mjs +2 -4
  9. package/src/help.mjs +1 -1
  10. package/src/rules/shared/01-tool.md +2 -7
  11. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +6 -6
  12. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +2 -25
  13. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +3 -3
  14. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +1 -1
  15. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +1 -1
  16. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +0 -1
  17. package/src/runtime/agent/orchestrator/config.mjs +6 -6
  18. package/src/runtime/agent/orchestrator/context/collect.mjs +1 -2
  19. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  20. package/src/runtime/agent/orchestrator/internal-agents.mjs +3 -3
  21. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +17 -12
  22. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +9 -1
  23. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +19 -9
  24. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -25
  25. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +0 -12
  26. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +1 -1
  27. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
  28. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +5 -5
  29. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -29
  30. package/src/runtime/agent/orchestrator/stall-policy.mjs +0 -1
  31. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -6
  32. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -1
  33. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +2 -2
  34. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +2 -15
  35. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -1
  36. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +22 -35
  37. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -5
  38. package/src/runtime/channels/lib/output-forwarder.mjs +0 -13
  39. package/src/runtime/channels/lib/tool-format.mjs +1 -2
  40. package/src/runtime/search/index.mjs +12 -6
  41. package/src/runtime/search/tool-defs.mjs +2 -2
  42. package/src/runtime/shared/agent-route-config.mjs +2 -3
  43. package/src/runtime/shared/child-spawn-gate.mjs +1 -1
  44. package/src/runtime/shared/resource-admission.mjs +1 -1
  45. package/src/runtime/shared/tool-card-model.mjs +3 -5
  46. package/src/runtime/shared/tool-primitives.mjs +0 -1
  47. package/src/runtime/shared/tool-result-summary.mjs +0 -7
  48. package/src/runtime/shared/tool-surface.mjs +1 -14
  49. package/src/session-runtime/runtime-core.mjs +1 -24
  50. package/src/session-runtime/settings-api.mjs +0 -8
  51. package/src/session-runtime/tool-catalog-data.mjs +4 -7
  52. package/src/session-runtime/tool-catalog-schema.mjs +1 -1
  53. package/src/session-runtime/workflow-agents-api.mjs +0 -3
  54. package/src/session-runtime/workflow.mjs +3 -7
  55. package/src/standalone/agent-tool/helpers.mjs +0 -1
  56. package/src/standalone/agent-tool/render.mjs +1 -1
  57. package/src/standalone/agent-tool/shard-spread.mjs +2 -2
  58. package/src/standalone/session-client.mjs +79 -20
  59. package/src/standalone/session-protocol.mjs +0 -1
  60. package/src/standalone/session-transport.mjs +19 -3
  61. package/src/tui/app/model-options.mjs +2 -3
  62. package/src/tui/app/onboarding-steps.mjs +1 -1
  63. package/src/tui/app/settings-picker.mjs +0 -12
  64. package/src/tui/app/transcript-row-estimate.mjs +1 -1
  65. package/src/tui/app/use-transcript-activity.mjs +7 -17
  66. package/src/tui/dist/index.mjs +9 -51
  67. package/src/tui/session/agent-envelope.mjs +1 -1
  68. package/src/tui/session/live-share.mjs +1 -1
  69. package/src/tui/session/session-api.mjs +0 -10
  70. package/src/tui/session/turn.mjs +2 -2
  71. package/src/tui/session-local.mjs +8 -11
  72. package/src/ui/statusline-agents.mjs +1 -2
  73. package/src/ui/statusline-format.mjs +1 -1
  74. package/src/ui/statusline.mjs +1 -6
  75. package/src/agents/explore/AGENT.md +0 -8
  76. package/src/agents/explore/agent.json +0 -6
  77. package/src/rules/agent/30-explorer.md +0 -55
  78. package/src/standalone/explore-tool.mjs +0 -770
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.110",
3
+ "version": "0.9.111",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -125,7 +125,6 @@
125
125
  "bench:run": "node scripts/bench-run.mjs",
126
126
  "bench:recall": "node scripts/recall-bench.mjs",
127
127
  "bench:tui-load": "node scripts/tui-runtime-load-bench.mjs",
128
- "bench:explore": "node scripts/explore-bench.mjs",
129
128
  "bench:output-style": "node scripts/output-style-bench.mjs",
130
129
  "bench:session-context": "node scripts/session-context-bench.mjs",
131
130
  "bench:session-transport": "node scripts/session-transport-bench.mjs",
@@ -68,7 +68,6 @@ function argsSummary(tool, args) {
68
68
  const values = args.symbols || args.files || args.symbol || args.file || '';
69
69
  return clip(`${args.mode || '?'}:${Array.isArray(values) ? `[${values.length}]${values[0] || ''}` : values}`);
70
70
  }
71
- case 'explore': return arr(args.query);
72
71
  case 'find': return clip(args.query);
73
72
  case 'glob': return clip(Array.isArray(args.pattern) ? args.pattern[0] : args.pattern);
74
73
  case 'list': return clip(basename(String(args.path || '')));
@@ -95,7 +94,7 @@ function targetValues(tool, args) {
95
94
  ? (args.files != null ? 'files' : 'file')
96
95
  : symbolMode || args.mode === 'symbols' ? (args.symbols != null ? 'symbols' : 'symbol') : 'file';
97
96
  } else if (tool === 'grep' || tool === 'glob') key = 'pattern';
98
- else if (tool === 'find' || tool === 'explore') key = 'query';
97
+ else if (tool === 'find') key = 'query';
99
98
  const value = args[key];
100
99
  return Array.isArray(value) ? value : value == null ? [] : [value];
101
100
  }
@@ -110,7 +109,7 @@ function batchFields(tool, args) {
110
109
  ? (args?.files != null ? 'files' : args?.file != null ? 'file' : args?.symbols != null ? 'symbols' : 'symbol')
111
110
  : null].filter(Boolean);
112
111
  }
113
- return [tool === 'read' || tool === 'list' ? 'path' : tool === 'find' || tool === 'explore' ? 'query' : null].filter(Boolean);
112
+ return [tool === 'read' || tool === 'list' ? 'path' : tool === 'find' ? 'query' : null].filter(Boolean);
114
113
  }
115
114
  function compatibleBatchCalls(tool, left, right) {
116
115
  if (tool === 'read') {
@@ -149,7 +148,7 @@ function batchSpec(tool, args, forcedField = null) {
149
148
  : (args.symbols != null ? 'symbols' : 'symbol');
150
149
  return { field, values: targetValues(tool, args) };
151
150
  }
152
- const fieldName = forcedField || (tool === 'read' || tool === 'list' ? 'path' : tool === 'grep' || tool === 'glob' ? 'pattern' : tool === 'find' || tool === 'explore' ? 'query' : null);
151
+ const fieldName = forcedField || (tool === 'read' || tool === 'list' ? 'path' : tool === 'grep' || tool === 'glob' ? 'pattern' : tool === 'find' ? 'query' : null);
153
152
  if (!fieldName) return null;
154
153
  const value = args[fieldName];
155
154
  return { field: fieldName, values: Array.isArray(value) ? value : value == null ? [] : [value] };
@@ -166,7 +165,7 @@ function sameIterationBatchObservations(sequence) {
166
165
  if (isMutation(next.tool)) break;
167
166
  group.push(next);
168
167
  }
169
- for (const candidate of ['read', 'grep', 'find', 'glob', 'list', 'explore', 'code_graph']) {
168
+ for (const candidate of ['read', 'grep', 'find', 'glob', 'list', 'code_graph']) {
170
169
  if (candidate === 'read') {
171
170
  const calls = group.filter((entry) => entry.tool === 'read' && !entry.failed).map((entry) => ({ entry, targets: readTargets(entry.rawArgs) })).filter(({ targets }) => targets?.length);
172
171
  if (calls.some(({ entry, targets }, index) => calls.some(({ entry: other, targets: otherTargets }, otherIndex) => (
@@ -238,7 +237,7 @@ function buildCase(sid, toolRows) {
238
237
  const observations = [];
239
238
  if (sequence.some((s) => s.tool === 'code_graph' && s.rawArgs?.mode === 'find_symbol' && !s.rawArgs?.file && !s.rawArgs?.files)) flags.push('find_symbol_noscope');
240
239
  // Exact duplicate requests are the only relookup signal available in tool
241
- // traces. Do not infer waste from counts, roles, turns, or explore→inspection:
240
+ // traces. Do not infer waste from counts, roles, turns, or locator→inspection:
242
241
  // exploration followed by inspection can be the intended route.
243
242
  const seenRequests = new Set();
244
243
  const readWindows = [];
@@ -25,7 +25,6 @@ export const SUITES = {
25
25
  'dead-owner-attach-test.mjs',
26
26
  'debounced-skills-async-save-test.mjs',
27
27
  'dispatch-persist-recovery-test.mjs',
28
- 'explore-prompt-policy-test.mjs',
29
28
  'find-fuzzy-hidden-test.mjs',
30
29
  'ingest-pure-conversation-smoke.mjs',
31
30
  'internal-tools-normalization-test.mjs',
package/src/app.mjs CHANGED
@@ -87,7 +87,6 @@ export async function run(argv = [], classifiedInvocation = null) {
87
87
  model: opts.model,
88
88
  effort: opts.effort,
89
89
  fast: opts.fast,
90
- explore: opts.explore,
91
90
  webSearch: opts.webSearch,
92
91
  memory: opts.memory,
93
92
  cwd: process.cwd(),
@@ -1,17 +1,5 @@
1
1
  {
2
2
  "agents": [
3
- {
4
- "agent": "explorer",
5
- "slot": "explore",
6
- "systemFile": "rules/agent/30-explorer.md",
7
- "description": "Filesystem navigation agent invoked by the `explore` MCP tool",
8
- "invokedBy": "explore",
9
- "toolSchemaProfile": "read",
10
- "schemaAllowedTools": ["grep", "find", "glob", "code_graph"],
11
- "kind": "retrieval",
12
- "permission": "read",
13
- "stallCap": { "idleSeconds": 240, "toolRunningSeconds": 180 }
14
- },
15
3
  {
16
4
  "agent": "cycle1-agent",
17
5
  "slot": "cycle1",
@@ -73,7 +73,7 @@ apply it, and verify the result.
73
73
  ### Workflow / agent definitions
74
74
 
75
75
  1. Workflows and agents are Markdown packs. Built-in services are Web Search,
76
- Explore, and Maintainer. Mixdog ships editable starter agents `worker`,
76
+ and Maintainer. Mixdog ships editable starter agents `worker`,
77
77
  `heavy-worker`, and `reviewer`; custom packs live at
78
78
  `<mixdogData>/workflows/<id>/WORKFLOW.md` and
79
79
  `<mixdogData>/agents/<id>/AGENT.md`.
@@ -1,10 +1,9 @@
1
1
  const VALUE_OPTIONS = new Set(['--provider', '--model', '--effort', '--workflow']);
2
2
  const FLAG_OPTIONS = new Set([
3
3
  '--readonly', '--help', '-h', '--plain', '--react', '--remote', '--onboarding', '--fast',
4
- '--explore', '--web-search', '--memory',
4
+ '--web-search', '--memory',
5
5
  ]);
6
6
  const HEADLESS_ROLE_ALIASES = new Map([
7
- ['explorer', 'explore'], ['explore', 'explore'],
8
7
  ['maint', 'maintainer'], ['maintenance', 'maintainer'], ['maintainer', 'maintainer'],
9
8
  ['worker', 'worker'],
10
9
  ['heavy', 'heavy-worker'], ['heavyworker', 'heavy-worker'], ['heavy-worker', 'heavy-worker'],
@@ -108,7 +107,6 @@ export function classifyCliInvocation(argv = []) {
108
107
  model: parsed.values['--model'],
109
108
  effort: parsed.values['--effort'],
110
109
  fast: argv.includes('--fast'),
111
- explore: argv.includes('--explore'),
112
110
  webSearch: argv.includes('--web-search'),
113
111
  memory: argv.includes('--memory'),
114
112
  toolMode: argv.includes('--readonly') ? 'readonly' : 'full',
@@ -83,7 +83,6 @@ export async function runHeadlessRole({
83
83
  model,
84
84
  effort,
85
85
  fast,
86
- explore = false,
87
86
  webSearch = false,
88
87
  memory = false,
89
88
  cwd = process.cwd(),
@@ -93,11 +92,10 @@ export async function runHeadlessRole({
93
92
  } = {}) {
94
93
  const cleanAgent = clean(agent);
95
94
  const cleanMessage = clean(message);
96
- // Classic headless surface: explorer, web search, and memory tools start OFF
97
- // and opt back in per run (--explore / --web-search / --memory). An explicit
95
+ // Classic headless surface: web search and memory tools start OFF
96
+ // and opt back in per run (--web-search / --memory). An explicit
98
97
  // MIXDOG_FEATURE_* value from the caller environment always wins.
99
98
  for (const [key, enabled] of [
100
- ['MIXDOG_FEATURE_EXPLORE', explore],
101
99
  ['MIXDOG_FEATURE_WEB_SEARCH', webSearch],
102
100
  ['MIXDOG_FEATURE_MEMORY', memory],
103
101
  ]) {
package/src/help.mjs CHANGED
@@ -21,7 +21,7 @@ export const HELP_LINES = [
21
21
  '',
22
22
  'Headless role commands require an explicit provider/model pair and run with',
23
23
  'ephemeral config/data; host behavioral config and personal state are not loaded.',
24
- 'Roles: explore, worker, heavy-worker, reviewer, maintainer, web-researcher.',
24
+ 'Roles: worker, heavy-worker, reviewer, maintainer, web-researcher.',
25
25
  '',
26
26
  'Slash commands (inside mixdog):',
27
27
  ' /clear start a fresh chat (alias: /new)',
@@ -11,12 +11,6 @@
11
11
  durable compact English memory→`memory`; explicit project change→`cwd`;
12
12
  explicit user-requested conversation reset→`session_manage`.
13
13
  Use only named tools present in the current tool surface.
14
- `explore`, when exposed, is a fast path only for facets whose repository
15
- coordinates remain unknown: call it first once for all such independent
16
- facets in one query array. It returns direct `path:line` anchors, not
17
- analysis or solutions; anchors are tool-verified coordinates that END
18
- their location facets — never re-locate or second-guess them; resume
19
- baseline routing from them.
20
14
  - Act only on verified identities (cwd/project/user/tool-returned) — paths,
21
15
  module specifiers, symbols, data/record shapes alike; a guessed identity is
22
16
  verified by one lookup or sample only when the next call or edit references
@@ -38,7 +32,8 @@
38
32
  window. The moment evidence determines the answer, edit, or deliverable,
39
33
  stop retrieving; patch if needed.
40
34
  - Once the edit or deliverable is determined, finish in one assistant turn:
41
- one `apply_patch` per file or cohesive unit, all patches first, then one
35
+ issue `apply_patch` calls serially, never in parallel; use one cohesive call
36
+ with one file section per target, all patches first, then one
42
37
  batched verification `shell` that runs the real required postconditions
43
38
  on every changed file and produced artifact, never echoes a claim;
44
39
  runtime waits for every patch and skips the shell
@@ -47,7 +47,7 @@ import { resourceAdmission } from '../../../shared/resource-admission.mjs';
47
47
  export { resolveMaintenanceRoute } from './maintenance-route.mjs';
48
48
 
49
49
  // Cap agent role synthesis to ~3000 tokens (~12 KB at the 4 B/tok
50
- // working average). Pool B explore/recall/search answers occasionally land
50
+ // working average). Pool B recall/search answers occasionally land
51
51
  // 8-10k-token walls that then ride in the Lead context for the rest of the
52
52
  // turn; the cap keeps those outliers bounded without touching the 95%+ of
53
53
  // answers already under the threshold.
@@ -68,7 +68,7 @@ function formatCompactElapsedSeconds(ms) {
68
68
 
69
69
  // True when an abort explicitly opted into partial salvage — the error object
70
70
  // or the abort reason carries `salvagePartial: true`. A DEADLINE-driven caller
71
- // (explore hard timeout) sets it so the anchors the sub-agent already produced
71
+ // A hard timeout sets it so partial output the sub-agent already produced
72
72
  // are returned instead of discarded; user cancellation (ESC) never sets it and
73
73
  // keeps the throw-everything behaviour.
74
74
  function salvagePartialRequested(error, signal) {
@@ -151,7 +151,7 @@ export function resolveHiddenRoleSchemaAllowedTools(hidden) {
151
151
  * against config.presets.
152
152
  * - null — unresolved.
153
153
  *
154
- * Explore and memory hidden roles mirror public spawning precedence:
154
+ * Hidden maintenance roles mirror public spawning precedence:
155
155
  * `agents.<role>` (including the `agents.maintenance` alias) → workflow route →
156
156
  * maintenance route → Main. The cycle1/2/3 agents share the memory knob via
157
157
  * their `maintKey: 'memory'` override. Scheduler and webhook are unchanged.
@@ -293,7 +293,7 @@ export function makeAgentDispatch(opts = {}) {
293
293
  agentId: agent,
294
294
  });
295
295
 
296
- // Callers (e.g. aiWrapped explore dispatch) may pass an explicit
296
+ // Callers may pass an explicit
297
297
  // `cwd` to scope the agent's filesystem view. Absolute path expected
298
298
  // (aiWrapped already expands `~` and resolves relatives). When unset
299
299
  // we pass `null` through instead of falling back to `process.cwd()`
@@ -418,7 +418,7 @@ export function makeAgentDispatch(opts = {}) {
418
418
  const _idleController = (agentWatchdogPolicyActive(_watchdogPolicy) && _linkSignal)
419
419
  ? new AbortController()
420
420
  : null;
421
- // Do not link factory parent, per-call explore cancellation, and the
421
+ // Do not link factory parent, per-call cancellation, and the
422
422
  // watchdog one at a time: each link replaces the previous listener in
423
423
  // runtime-liveness. One composite survives askSession's controller
424
424
  // swap and makes every source reach the provider call.
@@ -497,7 +497,7 @@ export function makeAgentDispatch(opts = {}) {
497
497
  });
498
498
  process.stderr.write(`[agent-dispatch] agent=${agent} session=${session.id} elapsed=${Date.now() - _agentDispatchT0}ms\n`);
499
499
  const raw = result?.content || '';
500
- // Brief cap. Agent role answers (explore/recall/search)
500
+ // Brief cap. Agent role answers (recall/search)
501
501
  // occasionally balloon to 8-10k token walls that then ride in the
502
502
  // parent Lead's context for the rest of the turn. A 3000-token
503
503
  // (~12 KB) ceiling trims the long tail while leaving the vast
@@ -1,7 +1,5 @@
1
1
  /**
2
- * Agent loop ceilings. Lead and general delegated agents share one high
3
- * runaway guard. Explorer is the sole bounded exception: locator work gets at
4
- * at most five tool-capable turns, followed by the loop's tool-less report turn.
2
+ * Agent loop ceilings. Lead and delegated agents share one high runaway guard.
5
3
  */
6
4
 
7
5
  function envPositiveInt(name, fallback) {
@@ -15,29 +13,8 @@ function envPositiveInt(name, fallback) {
15
13
  // to raise/lower the safety ceiling, never used as a general task-length budget.
16
14
  export const LEAD_MAX_LOOP_ITERATIONS = envPositiveInt('MIXDOG_AGENT_MAX_LOOP', 200);
17
15
 
18
- // Explorer's first turn is the whole maximum-fanout search; turns 2-5 are
19
- // bounded miss recovery. The override may shorten this but never add a sixth.
20
- export const EXPLORE_MAX_LOOP_ITERATIONS = Math.min(
21
- 5,
22
- envPositiveInt('MIXDOG_EXPLORE_MAX_LOOP', 5),
23
- );
24
-
25
- /**
26
- * Resolve the hard cap used by agentLoop for this session.
27
- *
28
- * Explorer: the lowest positive explicit/session value, clamped to its
29
- * dedicated five-turn ceiling. Others: explicit → session-pinned → shared guard.
30
- */
16
+ /** Resolve the hard cap used by agentLoop for this session. */
31
17
  export function resolveSessionMaxLoopIterations(sessionRef, explicit) {
32
- const sessionAgent = String(sessionRef?.agent || '').trim().toLowerCase();
33
- if (sessionAgent === 'explorer' || sessionAgent === 'explore') {
34
- const requested = Number.isFinite(explicit) && explicit > 0
35
- ? Math.floor(explicit)
36
- : Number.isFinite(sessionRef?.maxLoopIterations) && sessionRef.maxLoopIterations > 0
37
- ? Math.floor(sessionRef.maxLoopIterations)
38
- : EXPLORE_MAX_LOOP_ITERATIONS;
39
- return Math.min(EXPLORE_MAX_LOOP_ITERATIONS, requested);
40
- }
41
18
  if (Number.isFinite(explicit) && explicit > 0) return Math.floor(explicit);
42
19
  if (Number.isFinite(sessionRef?.maxLoopIterations) && sessionRef.maxLoopIterations > 0) {
43
20
  return Math.floor(sessionRef.maxLoopIterations);
@@ -17,7 +17,7 @@ import {
17
17
  // Ordering guarantee, stated in stall-policy.mjs: the provider layer — which
18
18
  // can retry in place or fall back to non-streaming — must fire STRICTLY before
19
19
  // the agent watchdog's terminal abort. Role abort budgets (worker/reviewer
20
- // 300s, explore 240s) sat at or BELOW the provider semantic-idle window
20
+ // 300s) sat at or BELOW the provider semantic-idle window
21
21
  // (300s), inverting that order: the watchdog aborted the shared signal first,
22
22
  // so the provider's recovery never ran and the `agent_stall` failure — which
23
23
  // the classifier calls retryable — died on throwIfAborted instead. Hold the
@@ -140,7 +140,7 @@ export function watchdogPartialHandoffFromError(error, session, messageStartInde
140
140
  }
141
141
 
142
142
  // Salvage path for NON-watchdog aborts that explicitly opt in (the abort error
143
- // / abort reason carries `salvagePartial: true` — e.g. the explore wall-clock
143
+ // / abort reason carries `salvagePartial: true` — e.g. a bounded wall-clock
144
144
  // hard timeout). Same collection rule as the watchdog handoff: only assistant
145
145
  // text appended during this run. Plain user cancellation never opts in, so ESC
146
146
  // still discards the run.
@@ -266,7 +266,7 @@ export function resolveAgentWatchdogPolicy(agent, overrides = {}) {
266
266
  ? Math.min(DEFAULT_STALE_TIMEOUT_MS, backstopMs)
267
267
  : DEFAULT_STALE_TIMEOUT_MS;
268
268
  // Same floor for the public backstop: a workflow role (worker 300s,
269
- // explore 240s) must not undercut the provider window either.
269
+ // role-specific caps must not undercut the provider window either.
270
270
  idleStaleMs = Math.max(idleStaleMs, PROVIDER_RECOVERY_FLOOR_MS);
271
271
  }
272
272
 
@@ -29,7 +29,7 @@ export function resolveMaintenanceRoute({ preset, optsPreset, agent, config: cfg
29
29
  try {
30
30
  const config = cfgIn || loadConfig({ secrets: false });
31
31
  const key = hidden.maintKey || hidden.slot;
32
- const role = key === 'explore' ? 'explore' : (key === 'memory' ? 'maintainer' : '');
32
+ const role = key === 'memory' ? 'maintainer' : '';
33
33
  if (!role) return config?.maintenance?.[key] ?? null;
34
34
  const candidates = [
35
35
  ...configuredAgentRouteCandidates(config, role),
@@ -49,7 +49,7 @@ function normalizeAgentCompactionConfig(value = {}, { memoryEnabled = true } = {
49
49
 
50
50
  /**
51
51
  * @param {object} opts
52
- * @param {string} opts.agent — canonical agent name ('worker', 'explorer', ...)
52
+ * @param {string} opts.agent — canonical agent name ('worker', 'reviewer', ...)
53
53
  * @param {string} opts.presetName — resolved preset identifier
54
54
  * @param {object} opts.preset — resolved preset object from agent-config
55
55
  * @param {object} opts.runtimeSpec — resolveRuntimeSpec output; must carry .scopeKey / .lane
@@ -137,7 +137,6 @@ const TOOL_ARG_KEYS = {
137
137
  list: ['path', 'head_limit', 'offset'],
138
138
  recall: ['query', 'limit', 'session_id', 'cwd'],
139
139
  search: ['query', 'limit', 'cwd'],
140
- explore: ['query', 'queries', 'limit', 'cwd'],
141
140
  code_graph: ['mode', 'file', 'files', 'symbol', 'symbols', 'body', 'language', 'limit', 'depth', 'page', 'cwd'],
142
141
  shell: ['command', 'cwd', 'timeout', 'mode', 'run_in_background', 'persistent', 'session_id'],
143
142
  task: ['task_id', 'action', 'timeout_ms', 'poll_ms'],
@@ -22,13 +22,12 @@ export function getPluginData() {
22
22
  // Canonical maintenance defaults. Single source of truth — imported by
23
23
  // llm/index.mjs and setup-server.mjs so UI/runtime cannot drift from config.
24
24
  //
25
- // Explore and Maintainer start without a route so they dynamically inherit the
26
- // Main route. Their explicit routes live canonically in `agents.explore` and
27
- // `agents.maintainer`; load-time migration still accepts the older workflow /
28
- // maintenance aliases.
25
+ // Maintainer starts without a route so it dynamically inherits the Main route.
26
+ // Its explicit route lives canonically in `agents.maintainer`; load-time
27
+ // migration still accepts the older workflow / maintenance aliases.
29
28
  // Webhook endpoints may omit a model and use the fallback route below.
30
29
  // Legacy route slots accepted only at config ingress for migration.
31
- const MAINTENANCE_SLOTS = Object.freeze(['explore', 'memory']);
30
+ const MAINTENANCE_SLOTS = Object.freeze(['memory']);
32
31
 
33
32
  // --- User profile (statusline /profile) -------------------------------------
34
33
  // Supported response languages for the /profile picker. `system` is the default
@@ -340,7 +339,8 @@ function canonicalizeShellStorage(value) {
340
339
  function canonicalizeModulesStorage(value) {
341
340
  const modules = configObject(value);
342
341
  delete modules.memory;
343
- for (const name of ['search', 'explore']) {
342
+ delete modules.explore;
343
+ for (const name of ['search']) {
344
344
  if (!Object.prototype.hasOwnProperty.call(modules, name)) continue;
345
345
  const raw = modules[name];
346
346
  modules[name] = {
@@ -711,8 +711,7 @@ export function loadScopedRoleInstructions(agent, provider = null) {
711
711
  agentRuleSectionsToEmit = hiddenPairs.map(p => `## ${p.name}\n\n${p.body}`);
712
712
  agentSectionsToEmit = agentSections;
713
713
  } else if (agent && classification.retrieval.has(agent)) {
714
- // Retrieval agents (explorer) get their own contract section
715
- // (rules/agent/30-explorer.md) in BP2.
714
+ // Retrieval agents get their own contract section in BP2.
716
715
  const self = hiddenPairs.find(p => p.name === agent);
717
716
  agentRuleSectionsToEmit = self ? [`## ${self.name}\n\n${self.body}`] : [];
718
717
  agentSectionsToEmit = agentSections.filter(s =>
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * This module persists the minimum needed to recover:
10
10
  * - handle (`dispatch_<tool>_...`)
11
- * - tool (`recall` / `search` / `explore`)
11
+ * - tool (`recall` / `search`)
12
12
  * - queries (for the abort message)
13
13
  * - createdAt
14
14
  *
@@ -2,7 +2,7 @@
2
2
  * Internal hidden agents — Mixdog-managed, user-untouchable.
3
3
  *
4
4
  * Unlike public workflow agents, these hidden agents are NEVER exposed to callers of the `agent` tool. They are
5
- * invoked only by internal handlers (explore / recall / search) and carry
5
+ * invoked only by internal handlers (recall / search) and carry
6
6
  * their own system prompt + tool-set policy.
7
7
  *
8
8
  * Lookup order (agent-dispatch.resolveMaintenanceRoute):
@@ -20,7 +20,7 @@
20
20
  * found" error rather than silently mis-dispatching.
21
21
  *
22
22
  * Kind classification:
23
- * - 'retrieval' : short-lived hidden retrieval agents (explore).
23
+ * - 'retrieval' : short-lived hidden retrieval agents.
24
24
  * - 'maintenance' : background-trigger hidden agents (memory cycle and
25
25
  * title generation). Receive only their own self section.
26
26
  *
@@ -184,7 +184,7 @@ export function listHiddenAgentsByKind(kind) {
184
184
 
185
185
  /**
186
186
  * Return the agents/<name>.md sections a hidden agent shares in its BP2 catalog
187
- * (in addition to its own self section). Drives the explorer→worker cache
187
+ * (in addition to its own self section). Drives hidden-role cache
188
188
  * alignment declaratively instead of a hard-coded agent-name branch in
189
189
  * collect.mjs. Returns [] when the agent declares none.
190
190
  */
@@ -105,6 +105,16 @@ const WS_MIDSTREAM_POLICY = {
105
105
  const HANDSHAKE_MAX_ATTEMPTS = MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT + 1;
106
106
  const HANDSHAKE_BACKOFF_BASE_MS = 200;
107
107
  const HANDSHAKE_BACKOFF_CAP_MS = 3200;
108
+ // These statuses are decisions made by the CURRENT attempt. They must never
109
+ // be replaced by a stale transient from an earlier retry: auth recovery,
110
+ // transport fallback, and Retry-After handling all depend on the live error.
111
+ const CURRENT_ATTEMPT_DECISION_STATUSES = new Set([401, 403, 426, 429]);
112
+
113
+ function _mustSurfaceCurrentAttempt(err, externalSignal) {
114
+ return CURRENT_ATTEMPT_DECISION_STATUSES.has(Number(err?.httpStatus || 0))
115
+ || externalSignal?.aborted
116
+ || err?.unsafeToRetry === true;
117
+ }
108
118
 
109
119
  function _classifyHandshakeError(err, { retry429 = true } = {}) {
110
120
  return classifyHandshakeError(err, { retry429 });
@@ -738,12 +748,12 @@ export async function sendViaWebSocket({
738
748
  // A later auth/upgrade decision must win over an earlier transient
739
749
  // failure so the caller can refresh or switch transport. Likewise,
740
750
  // never replace a current cancellation with stale retry history.
741
- const currentStatus = Number(err?.httpStatus || 0);
742
- const surfaceCurrent = currentStatus === 401
743
- || currentStatus === 426
744
- || externalSignal?.aborted
745
- || err?.unsafeToRetry === true;
746
- if (surfaceCurrent) {
751
+ if (_mustSurfaceCurrentAttempt(err, externalSignal)) {
752
+ if (retryable && attemptIndex >= MAX_MIDSTREAM_RETRIES) {
753
+ try { err.midstreamRetries = attemptIndex; } catch {}
754
+ try { err.midstreamClassifier = classifier; } catch {}
755
+ try { err.wsRetriesExhausted = true; } catch {}
756
+ }
747
757
  emitSendSpan('error');
748
758
  throw _stampTool(_stampLiveText(err));
749
759
  }
@@ -1126,12 +1136,7 @@ export async function sendViaWebSocket({
1126
1136
  // Not retryable, OR we've already exhausted the retry budget.
1127
1137
  // Do not let stale retry history mask a current auth/upgrade
1128
1138
  // decision, cancellation, or newly unsafe-to-replay outcome.
1129
- const currentStatus = Number(err?.httpStatus || 0);
1130
- const surfaceCurrent = currentStatus === 401
1131
- || currentStatus === 426
1132
- || externalSignal?.aborted
1133
- || err?.unsafeToRetry === true;
1134
- if (surfaceCurrent) {
1139
+ if (_mustSurfaceCurrentAttempt(err, externalSignal)) {
1135
1140
  emitSendSpan('error');
1136
1141
  throw _stampTool(_stampLiveText(err));
1137
1142
  }
@@ -44,6 +44,10 @@ export const WS_IDLE_MS = resolveTimeoutMs(
44
44
  );
45
45
  const WS_HANDSHAKE_TIMEOUT_MS = PROVIDER_WS_HANDSHAKE_TIMEOUT_MS;
46
46
  const WS_ACQUIRE_TIMEOUT_MS = PROVIDER_WS_ACQUIRE_TIMEOUT_MS;
47
+ // Enforced by `ws` while fragments are assembled, before a complete payload
48
+ // reaches the stream consumer or is decoded. Shared by Codex, direct OpenAI,
49
+ // and xAI so no provider can opt into an unbounded receive allocation.
50
+ export const WS_MAX_INCOMING_FRAME_BYTES = 16 * 1024 * 1024;
47
51
  // A write that never reaches the ws callback is indistinguishable from a
48
52
  // wedged transport. Keep it under the same short bound as socket acquisition
49
53
  // so the caller can discard the entry and reconnect before arming stream
@@ -459,7 +463,11 @@ function _openSocket({ auth, sessionToken, turnState, externalSignal, cacheKey,
459
463
  }
460
464
  (ok ? resolve : reject)(val);
461
465
  };
462
- const socket = new WebSocket(url, { headers, handshakeTimeout: WS_HANDSHAKE_TIMEOUT_MS });
466
+ const socket = new WebSocket(url, {
467
+ headers,
468
+ handshakeTimeout: WS_HANDSHAKE_TIMEOUT_MS,
469
+ maxPayload: WS_MAX_INCOMING_FRAME_BYTES,
470
+ });
463
471
  acquireTimer = setTimeout(() => {
464
472
  if (settled) return;
465
473
  if (process.env.MIXDOG_DEBUG_AGENT) {
@@ -33,7 +33,7 @@ import {
33
33
  streamStalledError,
34
34
  } from '../stall-policy.mjs';
35
35
  import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
36
- import { _wsErrLabel } from './openai-ws-pool.mjs';
36
+ import { _wsErrLabel, WS_MAX_INCOMING_FRAME_BYTES } from './openai-ws-pool.mjs';
37
37
  import {
38
38
  _sansInput,
39
39
  _stableStringify,
@@ -100,10 +100,9 @@ export const WS_PRE_RESPONSE_CREATED_MS = (() => {
100
100
  // including metadata/keepalive, proves the socket is live.
101
101
  export const WS_INTER_CHUNK_MS = PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS;
102
102
  // Bound the second allocation performed when ws RawData is decoded to UTF-8.
103
- // The ws library has already assembled the Buffer by this point, so this check
104
- // must stay before data.toString() below. xAI shares this stream implementation
105
- // but retains its existing unbounded behavior.
106
- const OPENAI_WS_MAX_INCOMING_FRAME_BYTES = 16 * 1024 * 1024;
103
+ // The pool also applies this bound as `maxPayload` before assembly; this
104
+ // defense-in-depth check must stay before data.toString() for injected sockets
105
+ // and test seams.
107
106
  const X_CODEX_TURN_STATE_HEADER = 'x-codex-turn-state';
108
107
  const WS_TRACE_ENABLED = process.env.MIXDOG_WS_TRACE === '1';
109
108
 
@@ -288,9 +287,10 @@ export async function _streamResponse({
288
287
  }
289
288
  const preResponseCreatedMs = _positiveInt(_timeouts?.preResponseCreatedMs, WS_PRE_RESPONSE_CREATED_MS);
290
289
  const interChunkMs = _positiveInt(_timeouts?.interChunkMs, WS_INTER_CHUNK_MS);
291
- const maxIncomingFrameBytes = traceProvider === 'xai'
292
- ? 0
293
- : _positiveInt(_timeouts?.maxIncomingFrameBytes, OPENAI_WS_MAX_INCOMING_FRAME_BYTES);
290
+ const maxIncomingFrameBytes = _positiveInt(
291
+ _timeouts?.maxIncomingFrameBytes,
292
+ WS_MAX_INCOMING_FRAME_BYTES,
293
+ );
294
294
  // First-MEANINGFUL-frame deadline. Distinct from preResponseCreatedMs (a
295
295
  // short pre-created byte-silence window that resetIdle clears on the FIRST
296
296
  // frame of any kind): this timer is cleared only by a meaningful response
@@ -804,7 +804,7 @@ export async function _streamResponse({
804
804
  const frameBytes = _incomingFrameByteLength(data);
805
805
  if (maxIncomingFrameBytes > 0 && frameBytes != null && frameBytes > maxIncomingFrameBytes) {
806
806
  const err = new Error(
807
- `OpenAI WebSocket response frame is too large (${frameBytes} bytes; limit ${maxIncomingFrameBytes} bytes); request is retryable`,
807
+ `${errLabel} response frame is too large (${frameBytes} bytes; limit ${maxIncomingFrameBytes} bytes); request is retryable`,
808
808
  );
809
809
  err.code = 'EOPENAIWSFRAMETOOLARGE';
810
810
  err.wsFrameTooLarge = true;
@@ -1375,6 +1375,16 @@ export async function _streamResponse({
1375
1375
  if (WS_TRACE_ENABLED) _writeWsLifecycleTrace('error');
1376
1376
  if (done) return;
1377
1377
  const wrapped = err instanceof Error ? err : new Error(String(err));
1378
+ // `ws` rejects an over-limit fragmented message before assembly
1379
+ // with this typed code. Preserve the same retry classification as
1380
+ // the defense-in-depth complete-frame check above.
1381
+ if (wrapped.code === 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH') {
1382
+ try {
1383
+ wrapped.wsFrameTooLarge = true;
1384
+ wrapped.retryable = true;
1385
+ midState.wsFrameTooLarge = true;
1386
+ } catch {}
1387
+ }
1378
1388
  if (terminalError) {
1379
1389
  // Preserve the first terminalError; chain the later socket
1380
1390
  // error in via `cause` (or `suppressed` if cause already set)