mixdog 0.9.9 → 0.9.11

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 (49) hide show
  1. package/package.json +1 -1
  2. package/scripts/agent-tag-reuse-smoke.mjs +12 -8
  3. package/scripts/bench/cache-probe-tasks.json +8 -0
  4. package/scripts/bench/lead-review-tasks-r3.json +1 -1
  5. package/scripts/bench/r4-mixed-tasks.json +1 -1
  6. package/scripts/bench/review-tasks.json +1 -1
  7. package/scripts/build-runtime-windows.ps1 +242 -242
  8. package/scripts/recall-usecase-cases.json +1 -1
  9. package/scripts/smoke-runtime-negative.ps1 +106 -106
  10. package/scripts/tool-smoke.mjs +109 -0
  11. package/src/defaults/mixdog-config.template.json +1 -0
  12. package/src/mixdog-session-runtime.mjs +8 -0
  13. package/src/rules/agent/30-explorer.md +18 -20
  14. package/src/rules/lead/02-channels.md +3 -3
  15. package/src/runtime/agent/orchestrator/config.mjs +30 -0
  16. package/src/runtime/agent/orchestrator/context/collect.mjs +160 -0
  17. package/src/runtime/agent/orchestrator/mcp/client.mjs +25 -1
  18. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +24 -9
  19. package/src/runtime/agent/orchestrator/providers/gemini.mjs +18 -2
  20. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +23 -0
  21. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +9 -0
  22. package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +79 -0
  23. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +7 -1
  24. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +9 -2
  25. package/src/runtime/agent/orchestrator/session/manager.mjs +3 -3
  26. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +5 -5
  27. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +1 -22
  28. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +26 -3
  29. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +29 -1
  30. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +17 -16
  31. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -1
  32. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +4 -0
  33. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +30 -4
  34. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +333 -14
  35. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +129 -2
  36. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +58 -18
  37. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +56 -83
  38. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +26 -56
  39. package/src/runtime/channels/lib/webhook/deliveries.mjs +4 -1
  40. package/src/session-runtime/settings-api.mjs +13 -0
  41. package/src/session-runtime/tool-catalog.mjs +34 -7
  42. package/src/standalone/agent-tool/render.mjs +2 -0
  43. package/src/standalone/agent-tool.mjs +28 -6
  44. package/src/standalone/explore-tool.mjs +2 -2
  45. package/src/standalone/memory-runtime-proxy.mjs +85 -21
  46. package/src/tui/App.jsx +20 -1
  47. package/src/tui/app/extension-pickers.mjs +6 -3
  48. package/src/tui/dist/index.mjs +28 -4
  49. package/src/tui/engine.mjs +2 -0
@@ -1440,16 +1440,38 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1440
1440
 
1441
1441
  // True when a tag has a lingering worker-index / role trace but no live
1442
1442
  // session in this terminal (finished worker still inside the reap grace window).
1443
+ function terminalWorkerRowForTag(tag, context = {}) {
1444
+ const value = clean(tag);
1445
+ if (!value) return null;
1446
+ return readWorkerRows(context).find((row) => {
1447
+ if (clean(row.tag) !== value) return false;
1448
+ if (!isTerminalWorkerStatus(row.status || row.stage)) return false;
1449
+ if (getLiveSession(clean(row.sessionId))) return false;
1450
+ return true;
1451
+ }) || null;
1452
+ }
1453
+
1443
1454
  function hasTerminalTrace(tag, context = {}) {
1444
1455
  const value = clean(tag);
1445
1456
  if (!value || value.startsWith('sess_')) return false;
1446
1457
  if (resolveTag(value, context, { excludeTerminalTraces: true })) return false; // live -> reuse, not trace
1447
- if (tagAgents.has(value)) return true;
1448
- return readWorkerRows(context).some((row) => clean(row.tag) === value);
1458
+ return Boolean(terminalWorkerRowForTag(value, context));
1449
1459
  }
1450
1460
 
1451
- function terminalTraceSpawnError(tag) {
1452
- return new Error(`agent spawn: tag "${tag}" refers to a finished or closed worker; wait for reap or use a new tag`);
1461
+ function reapTerminalTraceForTag(tag, context = {}) {
1462
+ const value = clean(tag);
1463
+ if (!value || value.startsWith('sess_')) return false;
1464
+ if (!hasTerminalTrace(value, context)) return false;
1465
+ refreshTagsFromSessions({ context });
1466
+ let sessionId = tags.get(value) || '';
1467
+ if (!sessionId) {
1468
+ const row = readWorkerRows(context).find((item) => clean(item.tag) === value);
1469
+ sessionId = clean(row?.sessionId) || '';
1470
+ }
1471
+ if (sessionId) cancelReap(sessionId);
1472
+ forgetTag(value);
1473
+ removeWorkerRow({ tag: value, sessionId });
1474
+ return true;
1453
1475
  }
1454
1476
 
1455
1477
  async function execute(args = {}, context = {}) {
@@ -1511,7 +1533,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1511
1533
  // Explicit-tag spawn priority (auto nextTag always creates a fresh session):
1512
1534
  // 1) live + busy -> queue the prompt (reuse)
1513
1535
  // 2) live + idle -> continue existing session (reuse)
1514
- // 3) lingering terminal trace -> error (no defensive respawn)
1536
+ // 3) lingering terminal trace -> reap trace and fresh spawn under same tag
1515
1537
  // 4) genuinely new tag -> fresh deferred spawn
1516
1538
  const explicitTag = clean(args.tag);
1517
1539
  if (explicitTag) {
@@ -1533,7 +1555,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
1533
1555
  return dispatchToExistingSession(prepared, notifyContext, { reused: true });
1534
1556
  }
1535
1557
  if (hasTerminalTrace(explicitTag, scopedContext)) {
1536
- throw terminalTraceSpawnError(explicitTag);
1558
+ reapTerminalTraceForTag(explicitTag, scopedContext);
1537
1559
  }
1538
1560
  }
1539
1561
  const job = startDeferredSpawnJob(args, callerCwd, context, notifyContext);
@@ -15,7 +15,7 @@ export const EXPLORE_TOOL = {
15
15
  name: 'explore',
16
16
  title: 'Explore',
17
17
  annotations: { title: 'Explore', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
18
- description: 'Repo anchor locator. LLM-backed; broad/uncertain only. Array only for independent targets.',
18
+ description: 'Repo anchor locator: broad/uncertain targets, no known path. Array = independent targets.',
19
19
  inputSchema: {
20
20
  type: 'object',
21
21
  properties: {
@@ -70,7 +70,7 @@ function escapeXml(str) {
70
70
  // reminder. The full no-verdict contract lives at system level
71
71
  // (rules/agent/30-explorer.md).
72
72
  export function buildExplorerPrompt(query) {
73
- return `<query>${escapeXml(query)}</query>\nReminder: coordinates only — WHERE, never WHY. Finish within 5 tool turns, ideally 1. Turn 1 MUST be one maximal batch: fire EVERY candidate lookup at once (grep with all token variants + code_graph + find/glob in the same turn); never one tool at a time. Answer straight from search output — grep/code_graph hits already carry path:line; never spend a turn reading to confirm, polish, or understand. The FIRST credible anchor ends the search: STOP and answer in that same turn, marking weak anchors with ?. By turn 5 answer with best-so-far or EXPLORATION_FAILED. Output only anchor lines formatted as path:line — symbol/name — short reason, or EXPLORATION_FAILED. No preamble, bullets, numbering, headings, summary, code quotes, analysis, verdicts, ratings, or recommendations.`;
73
+ return `<query>${escapeXml(query)}</query>\nReminder: coordinates only — WHERE, never WHY. Budget: 5 turns of maximal batches, ideally 1. Turn 1 fires everything at once: one grep whose pattern[] packs ALL token variants, plus code_graph/find/glob in the SAME turn when useful width is free, turns are not. Answer straight from search output — hits already carry path:line; never read to confirm, polish, or understand. Before every call ask: is a credible anchor already on screen? If yes STOP and answer now, marking weak anchors with ?. EXPLORATION_FAILED only after all 5 turns found zero anchors; at turn 5 answer with best-so-far. Output only anchor lines formatted as path:line — symbol/name — short reason, or EXPLORATION_FAILED. No preamble, bullets, numbering, headings, summary, code quotes, analysis, verdicts, ratings, or recommendations.`;
74
74
  }
75
75
 
76
76
  export function normalizeExploreQueries(rawQuery) {
@@ -54,6 +54,38 @@ function delay(ms) {
54
54
  return new Promise(resolve => setTimeout(resolve, ms));
55
55
  }
56
56
 
57
+ const TRANSIENT_MEMORY_RPC_BACKOFF_MS = 400;
58
+
59
+ function isConnResetLikeError(err) {
60
+ const code = String(err?.code || '');
61
+ const msg = String(err?.message || err || '');
62
+ return code === 'ECONNRESET'
63
+ || code === 'ECONNREFUSED'
64
+ || /ECONNRESET|socket hang up/i.test(msg);
65
+ }
66
+
67
+ function isMemoryWorkerNotReadyError(err) {
68
+ const msg = String(err?.message || err || '');
69
+ return /memory worker exited before ready|memory worker ready timeout|memory runtime did not become ready|memory worker degraded/i.test(msg);
70
+ }
71
+
72
+ function isTransientMemoryRpcError(err) {
73
+ return isConnResetLikeError(err) || isMemoryWorkerNotReadyError(err);
74
+ }
75
+
76
+ function isMemoryReadOnlyToolCall(name, args = {}) {
77
+ const tool = String(name || '').trim();
78
+ if (tool === 'recall' || tool === 'search_memories') return true;
79
+ if (tool !== 'memory') return false;
80
+ const action = String(args?.action || '').trim();
81
+ if (action === 'status') return true;
82
+ if (action === 'core') {
83
+ const op = String(args?.op || '').trim();
84
+ return op === 'list' || op === 'candidates';
85
+ }
86
+ return false;
87
+ }
88
+
57
89
  function requestJson({ port, method = 'GET', path = '/', body = null, timeoutMs = 10_000, headers = {} }) {
58
90
  return new Promise((resolvePromise, reject) => {
59
91
  const payload = body == null ? null : JSON.stringify(body);
@@ -116,6 +148,33 @@ export function createStandaloneMemoryRuntime({
116
148
  let child = null;
117
149
  let nextCallId = 1;
118
150
 
151
+ function invalidateMemoryRuntimeAfterTransient(err) {
152
+ portCache = null;
153
+ if (!isMemoryWorkerNotReadyError(err)) return;
154
+ startPromise = null;
155
+ const proc = child;
156
+ child = null;
157
+ if (!proc || proc.killed) return;
158
+ try { proc.kill(); } catch {}
159
+ }
160
+
161
+ function shouldRetryMemoryRpcError(err, { readOnlyRpc = false } = {}) {
162
+ if (isMemoryWorkerNotReadyError(err)) return true;
163
+ if (readOnlyRpc && isConnResetLikeError(err)) return true;
164
+ return false;
165
+ }
166
+
167
+ async function withTransientMemoryRpcRetry(run, { readOnlyRpc = false } = {}) {
168
+ try {
169
+ return await run();
170
+ } catch (err) {
171
+ if (!shouldRetryMemoryRpcError(err, { readOnlyRpc })) throw err;
172
+ invalidateMemoryRuntimeAfterTransient(err);
173
+ await delay(TRANSIENT_MEMORY_RPC_BACKOFF_MS);
174
+ return await run();
175
+ }
176
+ }
177
+
119
178
  async function findLivePort({ allowStarting = false } = {}) {
120
179
  const active = readActiveInstance();
121
180
  const port = parsePort(active?.memory_port);
@@ -269,31 +328,36 @@ export function createStandaloneMemoryRuntime({
269
328
  }
270
329
 
271
330
  async function handleToolCall(name, args = {}) {
272
- await start();
273
- const port = portCache || await findLivePort({ allowStarting: true });
274
- if (!port) throw new Error('memory runtime is not available');
331
+ const readOnlyRpc = isMemoryReadOnlyToolCall(name, args);
275
332
  const callId = `mem_${process.pid}_${nextCallId++}`;
276
- return await requestJson({
277
- port,
278
- method: 'POST',
279
- path: '/api/tool',
280
- body: { name, arguments: args || {} },
281
- timeoutMs: Math.max(1000, Number(process.env.MIXDOG_MEMORY_TOOL_TIMEOUT_MS) || 180_000),
282
- headers: { 'X-Mixdog-Call-Id': callId },
283
- });
333
+ return await withTransientMemoryRpcRetry(async () => {
334
+ await start();
335
+ const port = portCache || await findLivePort({ allowStarting: true });
336
+ if (!port) throw new Error('memory runtime is not available');
337
+ return await requestJson({
338
+ port,
339
+ method: 'POST',
340
+ path: '/api/tool',
341
+ body: { name, arguments: args || {} },
342
+ timeoutMs: Math.max(1000, Number(process.env.MIXDOG_MEMORY_TOOL_TIMEOUT_MS) || 180_000),
343
+ headers: { 'X-Mixdog-Call-Id': callId },
344
+ });
345
+ }, { readOnlyRpc });
284
346
  }
285
347
 
286
348
  async function buildSessionCoreMemoryPayload(sessionCwd) {
287
- await start();
288
- const port = portCache || await findLivePort({ allowStarting: true });
289
- if (!port) throw new Error('memory runtime is not available');
290
- return await requestJson({
291
- port,
292
- method: 'POST',
293
- path: '/session-start/core-memory',
294
- body: { cwd: sessionCwd || cwd },
295
- timeoutMs: 30_000,
296
- });
349
+ return await withTransientMemoryRpcRetry(async () => {
350
+ await start();
351
+ const port = portCache || await findLivePort({ allowStarting: true });
352
+ if (!port) throw new Error('memory runtime is not available');
353
+ return await requestJson({
354
+ port,
355
+ method: 'POST',
356
+ path: '/session-start/core-memory',
357
+ body: { cwd: sessionCwd || cwd },
358
+ timeoutMs: 30_000,
359
+ });
360
+ }, { readOnlyRpc: true });
297
361
  }
298
362
 
299
363
  async function stop() {
package/src/tui/App.jsx CHANGED
@@ -386,7 +386,26 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
386
386
  setSettingsPrompt,
387
387
  parseMemoryCoreRows,
388
388
  });
389
- const [disabledSkills, setDisabledSkills] = useState(() => new Set());
389
+ const [disabledSkills, setDisabledSkillsInner] = useState(() => {
390
+ try {
391
+ const { disabled = [] } = store.getDisabledSkills?.() || {};
392
+ return new Set(disabled);
393
+ } catch {
394
+ return new Set();
395
+ }
396
+ });
397
+ const setDisabledSkills = useCallback((next) => {
398
+ setDisabledSkillsInner((current) => {
399
+ const base = current instanceof Set ? current : new Set(current);
400
+ const set = typeof next === 'function' ? next(base) : (next instanceof Set ? next : new Set(next));
401
+ try {
402
+ store.setDisabledSkills?.([...set]);
403
+ } catch (e) {
404
+ store.pushNotice(`skill disable persist failed: ${e?.message || e}`, 'error');
405
+ }
406
+ return set;
407
+ });
408
+ }, [store]);
390
409
  const {
391
410
  openMcpServersPicker,
392
411
  openMcpPicker,
@@ -177,7 +177,10 @@ export function createExtensionPickers({
177
177
  const next = new Set(disabledSet);
178
178
  if (item._enabled) next.add(name); else next.delete(name);
179
179
  setDisabledSkills(next);
180
- store.pushNotice(`skill ${item._enabled ? 'disabled' : 'enabled'}: ${name}`, 'info');
180
+ store.pushNotice(
181
+ `skill ${item._enabled ? 'disabled' : 'enabled'}: ${name} (prompt updates next session /clear)`,
182
+ 'info',
183
+ );
181
184
  openSkillsPicker({ highlightValue: name, disabledOverride: next });
182
185
  };
183
186
  setPicker({
@@ -221,7 +224,7 @@ export function createExtensionPickers({
221
224
  next.delete(skill.name);
222
225
  return next;
223
226
  });
224
- store.pushNotice(`skill enabled: ${skill.name}`, 'info');
227
+ store.pushNotice(`skill enabled: ${skill.name} (prompt updates next session /clear)`, 'info');
225
228
  openSkillsPicker();
226
229
  return;
227
230
  }
@@ -231,7 +234,7 @@ export function createExtensionPickers({
231
234
  next.add(skill.name);
232
235
  return next;
233
236
  });
234
- store.pushNotice(`skill disabled: ${skill.name}`, 'info');
237
+ store.pushNotice(`skill disabled: ${skill.name} (prompt updates next session /clear)`, 'info');
235
238
  openSkillsPicker();
236
239
  return;
237
240
  }
@@ -13232,7 +13232,10 @@ function createExtensionPickers({
13232
13232
  if (item._enabled) next.add(name);
13233
13233
  else next.delete(name);
13234
13234
  setDisabledSkills(next);
13235
- store.pushNotice(`skill ${item._enabled ? "disabled" : "enabled"}: ${name}`, "info");
13235
+ store.pushNotice(
13236
+ `skill ${item._enabled ? "disabled" : "enabled"}: ${name} (prompt updates next session /clear)`,
13237
+ "info"
13238
+ );
13236
13239
  openSkillsPicker({ highlightValue: name, disabledOverride: next });
13237
13240
  };
13238
13241
  setPicker({
@@ -13275,7 +13278,7 @@ function createExtensionPickers({
13275
13278
  next.delete(skill.name);
13276
13279
  return next;
13277
13280
  });
13278
- store.pushNotice(`skill enabled: ${skill.name}`, "info");
13281
+ store.pushNotice(`skill enabled: ${skill.name} (prompt updates next session /clear)`, "info");
13279
13282
  openSkillsPicker();
13280
13283
  return;
13281
13284
  }
@@ -13285,7 +13288,7 @@ function createExtensionPickers({
13285
13288
  next.add(skill.name);
13286
13289
  return next;
13287
13290
  });
13288
- store.pushNotice(`skill disabled: ${skill.name}`, "info");
13291
+ store.pushNotice(`skill disabled: ${skill.name} (prompt updates next session /clear)`, "info");
13289
13292
  openSkillsPicker();
13290
13293
  return;
13291
13294
  }
@@ -18296,7 +18299,26 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
18296
18299
  setSettingsPrompt,
18297
18300
  parseMemoryCoreRows
18298
18301
  });
18299
- const [disabledSkills, setDisabledSkills] = useState8(() => /* @__PURE__ */ new Set());
18302
+ const [disabledSkills, setDisabledSkillsInner] = useState8(() => {
18303
+ try {
18304
+ const { disabled = [] } = store.getDisabledSkills?.() || {};
18305
+ return new Set(disabled);
18306
+ } catch {
18307
+ return /* @__PURE__ */ new Set();
18308
+ }
18309
+ });
18310
+ const setDisabledSkills = useCallback6((next) => {
18311
+ setDisabledSkillsInner((current) => {
18312
+ const base = current instanceof Set ? current : new Set(current);
18313
+ const set = typeof next === "function" ? next(base) : next instanceof Set ? next : new Set(next);
18314
+ try {
18315
+ store.setDisabledSkills?.([...set]);
18316
+ } catch (e) {
18317
+ store.pushNotice(`skill disable persist failed: ${e?.message || e}`, "error");
18318
+ }
18319
+ return set;
18320
+ });
18321
+ }, [store]);
18300
18322
  const {
18301
18323
  openMcpServersPicker,
18302
18324
  openMcpPicker,
@@ -23512,6 +23534,8 @@ async function createEngineSession({
23512
23534
  set({ commandBusy: false });
23513
23535
  }
23514
23536
  },
23537
+ getDisabledSkills: () => runtime.getDisabledSkills?.() || { disabled: [] },
23538
+ setDisabledSkills: (disabled) => runtime.setDisabledSkills?.(disabled) || { disabled: [] },
23515
23539
  skillsStatus: () => {
23516
23540
  return runtime.skillsStatus?.() || { cwd: state.cwd, count: 0, skills: [] };
23517
23541
  },
@@ -2098,6 +2098,8 @@ export async function createEngineSession({
2098
2098
  set({ commandBusy: false });
2099
2099
  }
2100
2100
  },
2101
+ getDisabledSkills: () => runtime.getDisabledSkills?.() || { disabled: [] },
2102
+ setDisabledSkills: (disabled) => runtime.setDisabledSkills?.(disabled) || { disabled: [] },
2101
2103
  skillsStatus: () => {
2102
2104
  return runtime.skillsStatus?.() || { cwd: state.cwd, count: 0, skills: [] };
2103
2105
  },