mixdog 0.7.11 → 0.7.13

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/.claude-plugin/marketplace.json +5 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +28 -74
  4. package/README.md +193 -249
  5. package/bin/statusline-launcher.mjs +5 -1
  6. package/bin/statusline-lib.mjs +14 -6
  7. package/bin/statusline.mjs +14 -6
  8. package/bun.lock +128 -3
  9. package/defaults/hidden-roles.json +3 -0
  10. package/defaults/user-workflow.json +1 -2
  11. package/defaults/user-workflow.md +5 -1
  12. package/hooks/lib/settings-loader.cjs +4 -3
  13. package/hooks/pre-tool-subagent.cjs +7 -2
  14. package/hooks/session-start.cjs +52 -24
  15. package/lib/mixdog-debug.cjs +163 -0
  16. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  17. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  18. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  19. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  20. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  21. package/package.json +9 -2
  22. package/scripts/builtin-utils-smoke.mjs +14 -8
  23. package/scripts/bump.mjs +80 -0
  24. package/scripts/doctor.mjs +8 -3
  25. package/scripts/ensure-deps.mjs +2 -2
  26. package/scripts/mutation-io-smoke.mjs +17 -1
  27. package/scripts/permission-eval-smoke.mjs +18 -1
  28. package/scripts/run-mcp.mjs +65 -9
  29. package/scripts/statusline-launcher-smoke.mjs +2 -2
  30. package/scripts/webhook-selfheal-smoke.mjs +1 -3
  31. package/server-main.mjs +57 -3
  32. package/setup/install.mjs +574 -574
  33. package/setup/launch-core.mjs +0 -1
  34. package/setup/setup-server.mjs +90 -35
  35. package/setup/setup.html +44 -11
  36. package/skills/setup/SKILL.md +12 -2
  37. package/src/agent/index.mjs +1 -1
  38. package/src/agent/orchestrator/config.mjs +58 -6
  39. package/src/agent/orchestrator/providers/model-catalog.mjs +1 -1
  40. package/src/agent/orchestrator/providers/openai-oauth.mjs +9 -2
  41. package/src/agent/orchestrator/providers/openai-ws.mjs +23 -0
  42. package/src/agent/orchestrator/session/loop.mjs +3 -3
  43. package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +6 -2
  44. package/src/agent/orchestrator/tools/bash-session.mjs +1 -0
  45. package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
  46. package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +29 -6
  47. package/src/agent/orchestrator/tools/builtin/list-tool.mjs +8 -4
  48. package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +29 -8
  49. package/src/agent/orchestrator/tools/builtin.mjs +5 -2
  50. package/src/agent/orchestrator/tools/cwd-tool.mjs +17 -17
  51. package/src/agent/orchestrator/tools/graph-manifest.json +11 -11
  52. package/src/agent/orchestrator/tools/patch-manifest.json +11 -11
  53. package/src/agent/tool-defs.mjs +1 -1
  54. package/src/channels/index.mjs +39 -9
  55. package/src/channels/lib/event-queue.mjs +24 -1
  56. package/src/channels/lib/hook-pipe-server.mjs +21 -8
  57. package/src/channels/lib/webhook.mjs +159 -20
  58. package/src/memory/index.mjs +5 -1
  59. package/src/memory/lib/core-memory-store.mjs +1 -1
  60. package/src/memory/lib/memory-cycle1.mjs +8 -4
  61. package/src/memory/lib/memory-cycle2.mjs +1 -1
  62. package/src/memory/lib/memory-cycle3.mjs +1 -1
  63. package/src/memory/lib/memory-recall-store.mjs +27 -10
  64. package/src/search/lib/backends/openai-oauth.mjs +6 -2
  65. package/src/search/lib/cache.mjs +55 -7
  66. package/tools.json +2 -2
  67. package/scripts/test-config-rmw-restore.mjs +0 -122
@@ -26,6 +26,8 @@ import {
26
26
  ensureRuntimeDeps,
27
27
  hasRequiredDeps,
28
28
  renameWithRetrySync,
29
+ RENAME_RETRY_CODES,
30
+ sleepSync,
29
31
  } from './ensure-deps.mjs';
30
32
 
31
33
  // Stable per-terminal session id for this proxy supervisor's lifetime. The
@@ -453,6 +455,12 @@ let childHasResponded = false;
453
455
  let announceListChangedOnReady = false;
454
456
  let cachedInitRequest = null; // { id, params } from client's first initialize
455
457
  let cachedInitDone = false; // initialized notification observed from client
458
+ // One-shot latch: flips true the instant the client's initialize response is
459
+ // forwarded and never resets. It selects replayInitToChild's mode (real-id vs
460
+ // swallow) and tells handleChildGone whether the pending initialize may be
461
+ // flushed. Until it is true the client has NOT seen its initialize result, so
462
+ // that request must survive a child death and be re-driven, not errored.
463
+ let clientInitAnswered = false;
456
464
  let internalIdSeq = -1; // negative ids reserved for supervisor-internal requests
457
465
  const pendingFromClient = new Map(); // request id (from client) → { method }
458
466
  const pendingInternal = new Set(); // internal ids (init replay) — drop responses
@@ -670,14 +678,33 @@ function flushPendingClientErrors(tag) {
670
678
 
671
679
  function replayInitToChild() {
672
680
  if (!cachedInitRequest) return;
673
- const internalId = internalIdSeq--;
674
- pendingInternal.add(internalId);
675
- writeToChild(JSON.stringify({
676
- jsonrpc: '2.0',
677
- id: internalId,
678
- method: 'initialize',
679
- params: cachedInitRequest.params,
680
- }));
681
+ // Mode select by invariant — has the client's initialize been answered yet?
682
+ // • Already answered (steady-state respawn): the client is fully
683
+ // initialized and must NOT see a second result. Replay under an internal
684
+ // negative id and swallow the response.
685
+ // • Not yet (first-boot / handshake-time crash): replay under the client's
686
+ // OWN id. Its request is still pending (preserved across the child-gone
687
+ // flush in handleChildGone), so the new child's initialize response flows
688
+ // back through the normal forward path — the client sees one clean result
689
+ // instead of the -32603 that previously killed the connection and forced
690
+ // a manual /mcp reconnect.
691
+ if (clientInitAnswered) {
692
+ const internalId = internalIdSeq--;
693
+ pendingInternal.add(internalId);
694
+ writeToChild(JSON.stringify({
695
+ jsonrpc: '2.0',
696
+ id: internalId,
697
+ method: 'initialize',
698
+ params: cachedInitRequest.params,
699
+ }));
700
+ } else {
701
+ writeToChild(JSON.stringify({
702
+ jsonrpc: '2.0',
703
+ id: cachedInitRequest.id,
704
+ method: 'initialize',
705
+ params: cachedInitRequest.params,
706
+ }));
707
+ }
681
708
  if (cachedInitDone) {
682
709
  // Notification — no id, no response expected.
683
710
  writeToChild(JSON.stringify({
@@ -816,7 +843,13 @@ function handleChildLine(line) {
816
843
  for (const item of scanned) {
817
844
  if (item && item.id !== undefined) {
818
845
  if (pendingInternal.has(item.id)) { internalIds.add(item.id); pendingInternal.delete(item.id); _maybeResolveLivenessPong(item.id); }
819
- else { pendingFromClient.delete(item.id); }
846
+ else {
847
+ pendingFromClient.delete(item.id);
848
+ // Same latch as the scalar path below — keep the invariant consistent
849
+ // even if the client's initialize ever returns inside a batch, so a
850
+ // later respawn swallows its replay instead of re-driving the real id.
851
+ if (cachedInitRequest && item.id === cachedInitRequest.id) clientInitAnswered = true;
852
+ }
820
853
  }
821
854
  }
822
855
  if (internalIds.size) {
@@ -846,6 +879,10 @@ function handleChildLine(line) {
846
879
  return;
847
880
  }
848
881
  pendingFromClient.delete(scanned.id);
882
+ // The client's initialize is satisfied the instant its response is
883
+ // forwarded. Latch it so the next respawn swallows its replay (steady
884
+ // state) and so handleChildGone is free to error this id on a later death.
885
+ if (cachedInitRequest && scanned.id === cachedInitRequest.id) clientInitAnswered = true;
849
886
  }
850
887
  writeToClient(line);
851
888
  }
@@ -896,10 +933,29 @@ function handleChildGone(why) {
896
933
  }
897
934
  const _pendingClientAtGone = pendingFromClient.size;
898
935
  const _pendingInternalAtGone = pendingInternal.size;
936
+ // First-boot recovery invariant: the client's initialize must receive exactly
937
+ // one success response, from whichever child completes the handshake. If it
938
+ // has not been answered yet (clientInitAnswered=false), erroring it here makes
939
+ // the client mark the MCP server failed — it never re-issues initialize on its
940
+ // own, and the replay (internal id) never reaches it. That is the "startup
941
+ // fails, /mcp fixes it" symptom. Keep that single id pending across the flush;
942
+ // replayInitToChild re-drives it under the client's own id against the fresh
943
+ // child so the success response flows straight back. shuttingDown never
944
+ // preserves (the supervisor is exiting; nothing will replay).
945
+ const _preserveInitId = (!shuttingDown && !clientInitAnswered && cachedInitRequest)
946
+ ? cachedInitRequest.id
947
+ : undefined;
948
+ const _preservedInit = _preserveInitId !== undefined
949
+ ? pendingFromClient.get(_preserveInitId)
950
+ : undefined;
899
951
  for (const [id] of pendingFromClient) {
952
+ if (id === _preserveInitId) continue;
900
953
  sendErrorToClient(id, -32603, `[run-mcp] mcp child ${why.tag}; retry`);
901
954
  }
902
955
  pendingFromClient.clear();
956
+ if (_preserveInitId !== undefined && _preservedInit !== undefined) {
957
+ pendingFromClient.set(_preserveInitId, _preservedInit);
958
+ }
903
959
  pendingInternal.clear();
904
960
  // Fresh child = fresh response path; discard any in-flight liveness probe.
905
961
  _livenessPingId = null;
@@ -27,7 +27,7 @@ const tmp = mkdtempSync(join(tmpdir(), 'mixdog-sl-launcher-'));
27
27
  function run(homeDir) {
28
28
  return spawnSync(process.execPath, [LAUNCHER], {
29
29
  input: SAMPLE,
30
- env: { ...process.env, HOME: homeDir, USERPROFILE: homeDir },
30
+ env: { ...process.env, HOME: homeDir, USERPROFILE: homeDir, CLAUDE_CONFIG_DIR: join(homeDir, '.claude') },
31
31
  encoding: 'utf8',
32
32
  });
33
33
  }
@@ -60,7 +60,7 @@ try {
60
60
  + "process.stdout.write(await renderStatusLine(i));\n"
61
61
  );
62
62
  const directRun = spawnSync(process.execPath, [driver],
63
- { input: SAMPLE, env: { ...process.env, HOME: tmp, USERPROFILE: tmp }, encoding: 'utf8' });
63
+ { input: SAMPLE, env: { ...process.env, HOME: tmp, USERPROFILE: tmp, CLAUDE_CONFIG_DIR: join(tmp, '.claude') }, encoding: 'utf8' });
64
64
  const direct = directRun.stdout || '';
65
65
 
66
66
  const ok = run(tmp);
@@ -5,9 +5,7 @@
5
5
  import { tmpdir } from 'node:os';
6
6
  import { join } from 'node:path';
7
7
 
8
- if (!process.env.CLAUDE_PLUGIN_DATA) {
9
- process.env.CLAUDE_PLUGIN_DATA = join(tmpdir(), `mixdog-webhook-selfheal-smoke-${process.pid}`);
10
- }
8
+ process.env.CLAUDE_PLUGIN_DATA = join(tmpdir(), `mixdog-webhook-selfheal-smoke-${process.pid}`);
11
9
 
12
10
  const { decidePortReclaimAction } = await import('../src/channels/lib/webhook.mjs');
13
11
 
package/server-main.mjs CHANGED
@@ -1040,10 +1040,62 @@ async function _getBridgeLlmFactory() {
1040
1040
  // completion after the worker stopped waiting for the result.
1041
1041
  const AGENT_IPC_MAX_CONCURRENT = 2
1042
1042
  const _agentIpcInflight = new Map()
1043
- /** @type {Array<{ msg: object, worker: string, proc: import('child_process').ChildProcess }>} */
1043
+ /** @type {Array<{ msg: object, worker: string, proc: import('child_process').ChildProcess, lane?: string }>} */
1044
1044
  const _agentIpcQueue = []
1045
1045
  let _agentIpcRunning = 0
1046
1046
 
1047
+ const AGENT_IPC_LANE_USER = 'user'
1048
+ const AGENT_IPC_LANE_MAINTENANCE = 'maintenance'
1049
+ /** @type {Set<string>} */
1050
+ const AGENT_IPC_MAINTENANCE_WORKERS = new Set(['memory'])
1051
+
1052
+ function _normalizeAgentIpcLaneToken(v) {
1053
+ return typeof v === 'string' ? v.trim().toLowerCase() : ''
1054
+ }
1055
+
1056
+ function _isMaintenanceBridgeRole(role) {
1057
+ const r = _normalizeAgentIpcLaneToken(role)
1058
+ if (!r) return false
1059
+ if (r === 'maintenance' || r === 'scheduler-task' || r === 'webhook-handler' || r === 'memory-classification') {
1060
+ return true
1061
+ }
1062
+ if (r.startsWith('cycle')) return true
1063
+ return false
1064
+ }
1065
+
1066
+ /** @returns {'user'|'maintenance'} */
1067
+ function _classifyAgentIpcJobLane(msg, worker) {
1068
+ const lane = _normalizeAgentIpcLaneToken(msg?.lane)
1069
+ if (lane === AGENT_IPC_LANE_MAINTENANCE || lane === 'maint') return AGENT_IPC_LANE_MAINTENANCE
1070
+ if (lane === AGENT_IPC_LANE_USER) return AGENT_IPC_LANE_USER
1071
+
1072
+ const priority = _normalizeAgentIpcLaneToken(msg?.priority)
1073
+ if (priority === AGENT_IPC_LANE_MAINTENANCE || priority === 'maint') return AGENT_IPC_LANE_MAINTENANCE
1074
+ if (priority === AGENT_IPC_LANE_USER) return AGENT_IPC_LANE_USER
1075
+
1076
+ if (msg?.maintenance === true || msg?.isMaintenance === true) return AGENT_IPC_LANE_MAINTENANCE
1077
+
1078
+ const params = msg?.params && typeof msg.params === 'object' ? msg.params : {}
1079
+ if (_isMaintenanceBridgeRole(params.role)) return AGENT_IPC_LANE_MAINTENANCE
1080
+
1081
+ const w = _normalizeAgentIpcLaneToken(worker)
1082
+ if (AGENT_IPC_MAINTENANCE_WORKERS.has(w)) return AGENT_IPC_LANE_MAINTENANCE
1083
+
1084
+ return AGENT_IPC_LANE_USER
1085
+ }
1086
+
1087
+ function _dequeueHighestPriorityAgentIpcJob() {
1088
+ if (_agentIpcQueue.length === 0) return null
1089
+ for (let i = 0; i < _agentIpcQueue.length; i++) {
1090
+ const job = _agentIpcQueue[i]
1091
+ const lane = job.lane || _classifyAgentIpcJobLane(job.msg, job.worker)
1092
+ if (lane !== AGENT_IPC_LANE_MAINTENANCE) {
1093
+ return _agentIpcQueue.splice(i, 1)[0]
1094
+ }
1095
+ }
1096
+ return _agentIpcQueue.shift()
1097
+ }
1098
+
1047
1099
  function _sendAgentIpcResponse(proc, callId, body) {
1048
1100
  try { proc.send({ type: 'agent_ipc_response', callId, ...body }) } catch {}
1049
1101
  }
@@ -1066,14 +1118,16 @@ function _startAgentIpcJob(job) {
1066
1118
 
1067
1119
  function _drainAgentIpcQueue() {
1068
1120
  while (_agentIpcRunning < AGENT_IPC_MAX_CONCURRENT && _agentIpcQueue.length > 0) {
1069
- const job = _agentIpcQueue.shift()
1121
+ const job = _dequeueHighestPriorityAgentIpcJob()
1122
+ if (!job) break
1070
1123
  _agentIpcRunning++
1071
1124
  _startAgentIpcJob(job)
1072
1125
  }
1073
1126
  }
1074
1127
 
1075
1128
  function _enqueueAgentIpcRequest(msg, worker, proc) {
1076
- _agentIpcQueue.push({ msg, worker, proc })
1129
+ const lane = _classifyAgentIpcJobLane(msg, worker)
1130
+ _agentIpcQueue.push({ msg, worker, proc, lane })
1077
1131
  _drainAgentIpcQueue()
1078
1132
  }
1079
1133