mixdog 0.9.151 → 0.9.152

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 (175) hide show
  1. package/README.md +110 -68
  2. package/package.json +3 -2
  3. package/scripts/tool-stress.mjs +0 -2
  4. package/src/defaults/skills/setup/SKILL.md +18 -27
  5. package/src/headless-exec.mjs +3 -10
  6. package/src/headless-exec.test.mjs +2 -7
  7. package/src/runtime/agent/orchestrator/config.mjs +6 -66
  8. package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
  9. package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
  10. package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
  11. package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
  12. package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
  13. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
  14. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
  15. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  16. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
  17. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
  18. package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
  19. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +10 -2
  20. package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
  21. package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
  22. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
  23. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
  24. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
  25. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
  26. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
  27. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -0
  28. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -2
  29. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
  30. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +4 -7
  31. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -0
  32. package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +235 -0
  33. package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
  34. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
  35. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
  36. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +14 -4
  37. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
  38. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +24 -1
  39. package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +161 -0
  40. package/src/runtime/agent/orchestrator/session/context-utils.mjs +14 -4
  41. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -1
  43. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +31 -18
  44. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -0
  45. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
  46. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +6 -1
  47. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +1 -3
  48. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +556 -0
  49. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +457 -0
  50. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +105 -186
  51. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
  52. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +2 -0
  53. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -0
  54. package/src/runtime/agent/orchestrator/session/store.mjs +23 -1
  55. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
  56. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
  57. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
  58. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
  59. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
  60. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
  61. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
  62. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
  63. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
  64. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
  65. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
  66. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
  67. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
  68. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
  69. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
  70. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
  71. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
  72. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
  73. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
  74. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  75. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  76. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +8 -0
  77. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  78. package/src/runtime/browser-bridge/client.mjs +103 -0
  79. package/src/runtime/browser-bridge/tool-defs.mjs +42 -0
  80. package/src/runtime/computer-bridge/client.mjs +101 -0
  81. package/src/runtime/computer-bridge/tool-defs.mjs +53 -0
  82. package/src/runtime/media/store.mjs +18 -8
  83. package/src/runtime/media/store.test.mjs +14 -0
  84. package/src/runtime/memory/index.mjs +1 -1
  85. package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
  86. package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
  87. package/src/runtime/shared/child-spawn-gate.mjs +17 -0
  88. package/src/runtime/shared/child-spawn-remote.mjs +69 -7
  89. package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
  90. package/src/runtime/shared/pristine-execution-contract.json +2 -1
  91. package/src/runtime/shared/provider-api-key.mjs +1 -0
  92. package/src/runtime/shared/session-runtime-health.mjs +91 -0
  93. package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
  94. package/src/runtime/shared/skill-document.mjs +86 -0
  95. package/src/runtime/shared/skill-document.test.mjs +81 -0
  96. package/src/runtime/shared/tool-surface.mjs +18 -0
  97. package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
  98. package/src/runtime/shared/turn-snapshot.mjs +32 -9
  99. package/src/runtime/shared/user-cwd.mjs +33 -14
  100. package/src/runtime/shared/user-cwd.test.mjs +43 -0
  101. package/src/session-runtime/cwd-plugins.mjs +26 -26
  102. package/src/session-runtime/cwd-plugins.test.mjs +75 -0
  103. package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
  104. package/src/session-runtime/global-extensions.mjs +26 -0
  105. package/src/session-runtime/global-extensions.test.mjs +28 -0
  106. package/src/session-runtime/goal-runtime.mjs +721 -0
  107. package/src/session-runtime/goal-runtime.test.mjs +108 -0
  108. package/src/session-runtime/lifecycle-api.mjs +18 -4
  109. package/src/session-runtime/lifecycle-api.test.mjs +21 -0
  110. package/src/session-runtime/mcp-glue.mjs +49 -47
  111. package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
  112. package/src/session-runtime/plugin-mcp.mjs +96 -22
  113. package/src/session-runtime/prewarm.mjs +1 -10
  114. package/src/session-runtime/resource-api-global.test.mjs +69 -0
  115. package/src/session-runtime/resource-api.mjs +173 -58
  116. package/src/session-runtime/runtime-core.mjs +148 -20
  117. package/src/session-runtime/runtime-tunables.mjs +6 -8
  118. package/src/session-runtime/session-turn-api.mjs +22 -11
  119. package/src/session-runtime/skills-api.mjs +79 -25
  120. package/src/session-runtime/skills-api.test.mjs +99 -0
  121. package/src/session-runtime/tool-catalog-schema.mjs +0 -5
  122. package/src/session-runtime/tool-catalog.mjs +4 -13
  123. package/src/session-runtime/tool-policy-surface.test.mjs +33 -11
  124. package/src/session-runtime/tool-surface.mjs +12 -0
  125. package/src/standalone/agent-dispatch-broker.mjs +5 -9
  126. package/src/standalone/agent-tool/notify.mjs +6 -8
  127. package/src/standalone/agent-tool/notify.test.mjs +37 -0
  128. package/src/standalone/daemon.mjs +31 -15
  129. package/src/standalone/hook-bus/config.mjs +1 -0
  130. package/src/standalone/plugin-admin.mjs +17 -0
  131. package/src/standalone/plugin-admin.test.mjs +7 -0
  132. package/src/standalone/provider-admin.mjs +1 -0
  133. package/src/standalone/session-protocol.mjs +5 -0
  134. package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
  135. package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
  136. package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
  137. package/src/standalone/session-runtime-host-factory.mjs +17 -0
  138. package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
  139. package/src/standalone/session-runtime-host-health.test.mjs +5 -0
  140. package/src/standalone/session-runtime-host.mjs +696 -58
  141. package/src/standalone/session-runtime-inline-host.mjs +351 -0
  142. package/src/standalone/session-runtime-inline-host.test.mjs +83 -0
  143. package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
  144. package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
  145. package/src/standalone/session-runtime-shard-host.test.mjs +569 -0
  146. package/src/standalone/session-runtime-shard-router.mjs +149 -0
  147. package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
  148. package/src/standalone/session-runtime-worker.mjs +476 -18
  149. package/src/standalone/session-service.mjs +81 -6
  150. package/src/tui/app/app-view.jsx +1 -0
  151. package/src/tui/app/core-memory-picker.mjs +2 -0
  152. package/src/tui/app/extension-pickers.mjs +1 -1
  153. package/src/tui/app/maintenance-pickers.mjs +3 -3
  154. package/src/tui/app/model-options.mjs +2 -0
  155. package/src/tui/app/model-picker.mjs +14 -4
  156. package/src/tui/app/panel-handoff.test.mjs +311 -0
  157. package/src/tui/app/project-picker.mjs +1 -0
  158. package/src/tui/app/route-pickers.mjs +21 -6
  159. package/src/tui/app/settings-picker.mjs +26 -3
  160. package/src/tui/app/slash-commands.mjs +1 -0
  161. package/src/tui/app/slash-dispatch.mjs +73 -22
  162. package/src/tui/app/theme-effort-pickers.mjs +7 -2
  163. package/src/tui/components/Picker.jsx +10 -1
  164. package/src/tui/dist/index.mjs +163 -44
  165. package/src/tui/session/context-state.mjs +1 -0
  166. package/src/tui/session/goal-continuation.mjs +110 -0
  167. package/src/tui/session/goal-continuation.test.mjs +84 -0
  168. package/src/tui/session/queue-helpers.mjs +5 -2
  169. package/src/tui/session/session-action-surface.test.mjs +108 -0
  170. package/src/tui/session/session-api-ext.mjs +39 -8
  171. package/src/tui/session/session-api.mjs +58 -0
  172. package/src/tui/session/session-flow.mjs +6 -0
  173. package/src/tui/session/turn.mjs +1 -0
  174. package/src/tui/session-local.mjs +15 -1
  175. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
@@ -0,0 +1,99 @@
1
+ import assert from 'node:assert/strict';
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ mkdtempSync,
6
+ readFileSync,
7
+ rmSync,
8
+ writeFileSync,
9
+ } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import { join } from 'node:path';
12
+ import test from 'node:test';
13
+
14
+ import * as contextMod from '../runtime/agent/orchestrator/context/collect.mjs';
15
+ import { parseSkillDocument } from '../runtime/shared/skill-document.mjs';
16
+ import { createSkillsApi } from './skills-api.mjs';
17
+
18
+ test('edits existing skills without dropping optional metadata or sibling resources', () => {
19
+ const root = mkdtempSync(join(tmpdir(), 'mixdog-skill-edit-'));
20
+ const previousDataDir = process.env.MIXDOG_DATA_DIR;
21
+ process.env.MIXDOG_DATA_DIR = join(root, 'data');
22
+ try {
23
+ const cwd = join(root, 'project');
24
+ const originalDir = join(process.env.MIXDOG_DATA_DIR, 'skills', 'existing-skill');
25
+ mkdirSync(join(originalDir, 'scripts'), { recursive: true });
26
+ writeFileSync(join(originalDir, 'SKILL.md'), [
27
+ '---',
28
+ 'name: existing-skill',
29
+ 'description: Use when editing an existing skill.',
30
+ 'license: MIT',
31
+ 'metadata:',
32
+ ' author: example',
33
+ 'allowed-tools:',
34
+ ' - shell',
35
+ '---',
36
+ '',
37
+ '# Original instructions',
38
+ '',
39
+ ].join('\n'));
40
+ writeFileSync(join(originalDir, 'scripts', 'check.py'), 'print("ok")\n');
41
+
42
+ const api = createSkillsApi({ contextMod, getCwd: () => cwd });
43
+ const saved = api.saveSkillDocument({
44
+ originalName: 'existing-skill',
45
+ name: 'renamed-skill',
46
+ description: 'Use when the renamed skill is needed.',
47
+ instructions: '# Updated instructions',
48
+ });
49
+ const renamedDir = join(process.env.MIXDOG_DATA_DIR, 'skills', 'renamed-skill');
50
+ const parsed = parseSkillDocument(readFileSync(saved.filePath, 'utf8'));
51
+
52
+ assert.equal(existsSync(originalDir), false);
53
+ assert.equal(saved.filePath, join(renamedDir, 'SKILL.md'));
54
+ assert.equal(existsSync(join(renamedDir, 'scripts', 'check.py')), true);
55
+ assert.equal(parsed.name, 'renamed-skill');
56
+ assert.equal(parsed.description, 'Use when the renamed skill is needed.');
57
+ assert.equal(parsed.body, '# Updated instructions\n');
58
+ assert.equal(parsed.frontmatter.license, 'MIT');
59
+ assert.deepEqual(parsed.frontmatter.metadata, { author: 'example' });
60
+ assert.deepEqual(parsed.frontmatter['allowed-tools'], ['shell']);
61
+ assert.ok(api.skillsStatus().skills.some((skill) => skill.name === 'renamed-skill'));
62
+ } finally {
63
+ contextMod.invalidateSkillsCache(join(root, 'project'));
64
+ if (previousDataDir === undefined) delete process.env.MIXDOG_DATA_DIR;
65
+ else process.env.MIXDOG_DATA_DIR = previousDataDir;
66
+ rmSync(root, { recursive: true, force: true });
67
+ }
68
+ });
69
+
70
+ test('creates new global skills with all three standard fields', () => {
71
+ const root = mkdtempSync(join(tmpdir(), 'mixdog-skill-create-'));
72
+ const previousDataDir = process.env.MIXDOG_DATA_DIR;
73
+ process.env.MIXDOG_DATA_DIR = join(root, 'data');
74
+ try {
75
+ const cwd = join(root, 'project');
76
+ const api = createSkillsApi({ contextMod, getCwd: () => cwd });
77
+ const created = api.addGlobalSkill({
78
+ name: 'new-skill',
79
+ description: 'Use when creating a standard skill.',
80
+ instructions: '# Instructions\n\nDo the work.',
81
+ });
82
+ const parsed = parseSkillDocument(readFileSync(created.filePath, 'utf8'));
83
+
84
+ assert.equal(parsed.name, 'new-skill');
85
+ assert.equal(parsed.description, 'Use when creating a standard skill.');
86
+ assert.equal(parsed.body, '# Instructions\n\nDo the work.\n');
87
+ assert.equal(created.filePath, join(process.env.MIXDOG_DATA_DIR, 'skills', 'new-skill', 'SKILL.md'));
88
+ assert.throws(() => api.addGlobalSkill({
89
+ name: 'Invalid_Name',
90
+ description: 'Use when invalid.',
91
+ instructions: '# Instructions',
92
+ }), /lowercase letters/);
93
+ } finally {
94
+ contextMod.invalidateSkillsCache(join(root, 'project'));
95
+ if (previousDataDir === undefined) delete process.env.MIXDOG_DATA_DIR;
96
+ else process.env.MIXDOG_DATA_DIR = previousDataDir;
97
+ rmSync(root, { recursive: true, force: true });
98
+ }
99
+ });
@@ -141,11 +141,6 @@ export function parseToolSelection(value) {
141
141
  .filter(Boolean);
142
142
  }
143
143
 
144
- export function parseToolSearchQuerySelection(query) {
145
- const match = clean(query).match(/^select\s*:\s*(.+)$/i);
146
- return match ? parseToolSelection(match[1]) : [];
147
- }
148
-
149
144
  export function routeToolRank(name) {
150
145
  const index = ROUTE_TOOL_ORDER.indexOf(clean(name));
151
146
  return index === -1 ? Number.MAX_SAFE_INTEGER : index;
@@ -25,9 +25,9 @@ import {
25
25
  MEASURED_TOOL_USAGE,
26
26
  READONLY_TOOL_NAMES,
27
27
  } from './tool-catalog-data.mjs';
28
- import { toolKind, measuredToolUsage, parseToolSelection, parseToolSearchQuerySelection, routeToolRank, sortedCatalogByMeasuredUsage, activeToolForSurface, deferredProviderMode, nativeProviderFamily } from './tool-catalog-schema.mjs';
28
+ import { toolKind, measuredToolUsage, parseToolSelection, routeToolRank, sortedCatalogByMeasuredUsage, activeToolForSurface, deferredProviderMode, nativeProviderFamily } from './tool-catalog-schema.mjs';
29
29
  import { filterModelEditTools } from '../runtime/shared/edit-tool-dialect.mjs';
30
- export { toolKind, toolSchemaBucket, estimateToolSchemaBreakdown, measuredToolUsage, parseToolSelection, parseToolSearchQuerySelection, sortedCatalogByMeasuredUsage } from './tool-catalog-schema.mjs';
30
+ export { toolKind, toolSchemaBucket, estimateToolSchemaBreakdown, measuredToolUsage, parseToolSelection, sortedCatalogByMeasuredUsage } from './tool-catalog-schema.mjs';
31
31
  export { resolveProviderRequestTools, snapshotProviderRequestTools } from './provider-request-snapshot.mjs';
32
32
  export {
33
33
  DEFERRED_DEFAULT_FULL_TOOLS,
@@ -686,18 +686,9 @@ export function selectDeferredTools(session, names, mode) {
686
686
  return { added, already, blocked, missing, native };
687
687
  }
688
688
 
689
- // Collect the exact deferred-tool names to load, honoring back-compat inputs:
690
- // names[] → primary loader input
691
- // select / "select:" → legacy alias (parseToolSelection strips the prefix)
692
- // query "select:a,b" → legacy query-side loader
693
- // A free-text `query` is NOT a search anymore: it yields no names, and the
694
- // caller is steered back to names[].
689
+ // Collect the exact deferred-tool names requested by the loader.
695
690
  function parseLoadToolNames(args = {}) {
696
- const fromNames = parseToolSelection(args.names);
697
- if (fromNames.length) return fromNames;
698
- const fromSelect = parseToolSelection(args.select);
699
- if (fromSelect.length) return fromSelect;
700
- return parseToolSearchQuerySelection(args.query);
691
+ return parseToolSelection(args.names);
701
692
  }
702
693
 
703
694
  // Split live MCP servers into "still connecting" (pending) and "failed" so the
@@ -121,17 +121,15 @@ test('shared tool rules keep workflow and shell-boundary anchors', () => {
121
121
  // Advisory drift check: update these anchors when the rule text
122
122
  // intentionally changes.
123
123
  const full = buildSharedToolContent({ PLUGIN_ROOT: join(process.cwd(), 'src') });
124
- assert.match(full, /Minimize tool turns through maximal useful parallelism/i);
125
- assert.match(full, /In each round, issue every necessary non-overlapping call whose inputs are\s+already known/i);
126
- assert.match(full, /Determine the required outcome and its gaps/i);
127
- assert.match(full, /gather only what is missing, act, then verify the affected facets/i);
128
- assert.match(full, /Investigate, build, and verify only what the requested outcome requires, at\s+the level it requires/i);
124
+ assert.match(full, /Determine the required outcome and missing evidence/i);
125
+ assert.match(full, /Before exploration or implementation, consult prior work, current external\s+information, or repository state only when needed/i);
126
+ assert.match(full, /Minimize tool turns by batching only calls that are independently necessary/i);
127
+ assert.match(full, /A call whose necessity or scope can change after\s+another result waits for that result/i);
128
+ assert.match(full, /Respect tool\/schema limits, never omit required fanout/i);
129
129
  assert.match(full, /A check runs at the strictness the task requires; never raise a tool's own\s+severity beyond it/i);
130
- assert.match(full, /Cost is counted in\s+rounds, not calls/i);
131
- assert.match(full, /defer a call only when its target or arguments require an\s+earlier result/i);
132
130
  assert.match(full, /Route each evidence facet once to its primary owner/i);
133
- assert.match(full, /enumeration is not a prerequisite when that operation's/i);
134
- assert.match(full, /apply\s+one analysis to many targets as one parameterized call/i);
131
+ assert.match(full, /A summary,\s+overview, or enumeration is not a prerequisite to an operation whose complete\s+inputs are already known/i);
132
+ assert.match(full, /apply one analysis\s+to many targets as one parameterized call/i);
135
133
  assert.match(full, /Evidence or artifacts available only through program execution, calculation,\s+data transformation, generated output, or unsupported-format decoding→`shell`/i);
136
134
  assert.match(full, /an already-open shell is never a routing reason/i);
137
135
  assert.match(full, /Route the missing evidence to its primary owner/i);
@@ -144,9 +142,9 @@ test('shared tool rules keep workflow and shell-boundary anchors', () => {
144
142
  assert.match(full, /literal, regex, or text location→`grep`;\s+known-file content, range, or image→`read`/i);
145
143
  assert.match(full, /is never re-found, re-derived, or\s+re-verified at any granularity/i);
146
144
  assert.match(full, /Mine each returned result fully before opening the next round/i);
147
- assert.match(full, /Evidence that determines the answer, edit, or deliverable ends retrieval/i);
145
+ assert.match(full, /Stop exploring once sufficient evidence determines the next action required\s+by the request/i);
148
146
  assert.match(full, /Enter Verification only after all planned work is complete/i);
149
- assert.match(full, /use an umbrella suite only when the user explicitly requests it\s+or a documented project or release process requires it/i);
147
+ assert.match(full, /Use an umbrella suite only when explicitly requested or required by the\s+documented project or release process/i);
150
148
  assert.match(full, /If verification fails, collect all failures, leave Verification/i);
151
149
  assert.match(full, /A successful verification closes the task unless later changes affect it/i);
152
150
  assert.doesNotMatch(full, /affected failed checks once/i);
@@ -235,6 +233,30 @@ test('modelStandaloneTools hides agent and disabled web-search/memory tools', ()
235
233
  assert.deepEqual(modelStandaloneTools().map((tool) => tool.name), ['read']);
236
234
  });
237
235
 
236
+ test('activateTools refreshes a stale session catalog before selecting new runtime tools', () => {
237
+ const read = { name: 'read', annotations: { readOnlyHint: true } };
238
+ const updateGoal = { name: 'update_goal', annotations: { readOnlyHint: false } };
239
+ const session = {
240
+ provider: 'grok-oauth',
241
+ messages: [{ role: 'system', content: '# Existing session' }],
242
+ tools: [read],
243
+ deferredToolCatalog: [read],
244
+ deferredCallableTools: ['read'],
245
+ deferredSelectedTools: ['read'],
246
+ deferredToolBp2Applied: true,
247
+ };
248
+ const surface = surfaceFor({ session, standalone: [read, updateGoal] });
249
+ const result = surface.activateTools(['update_goal']);
250
+ assert.deepEqual(result.missing, []);
251
+ assert.equal(
252
+ new Set([
253
+ ...session.tools.map((tool) => tool.name),
254
+ ...(session.deferredCallableTools || []),
255
+ ]).has('update_goal'),
256
+ true,
257
+ );
258
+ });
259
+
238
260
  test('empty session refresh strips denied tools and BP1 routes', async () => {
239
261
  const session = {
240
262
  id: 'sess_empty',
@@ -81,8 +81,20 @@ export function createToolSurface({
81
81
  return surface;
82
82
  }
83
83
 
84
+ function activateTools(names) {
85
+ const session = getSession();
86
+ if (!session) return null;
87
+ const surfaceMode = deferredSurfaceModeForLead(mode);
88
+ applyDeferredToolSurface(session, surfaceMode, modelStandaloneTools(), {
89
+ provider: getRoute().provider,
90
+ model: getRoute().model,
91
+ });
92
+ return selectDeferredTools(session, names, surfaceMode);
93
+ }
94
+
84
95
  return {
85
96
  modelStandaloneTools,
97
+ activateTools,
86
98
  invalidatePreSessionToolSurface() {
87
99
  preSessionSurface = null;
88
100
  },
@@ -34,11 +34,9 @@ function dispatchSignature(agent, params) {
34
34
  * The memory process keeps PG, embeddings, and recall, and forwards only
35
35
  * cycle LLM calls here. The broker owns admission (fair scheduler), callId
36
36
  * idempotency, and cancellation; actual agent execution is delegated through
37
- * dispatchAgent to the recyclable session runtime worker so orchestrator and
38
- * provider churn never accumulates in the permanent daemon process. A
39
- * singleton is NOT a serial lane: every call gets its own AbortController and
40
- * the fair scheduler starts calls in parallel unless an operator explicitly
41
- * configures a finite active limit.
37
+ * dispatchAgent to the session runtime host. A singleton is NOT a serial lane:
38
+ * every call gets its own AbortController and the fair scheduler starts calls
39
+ * in parallel unless an operator explicitly configures a finite active limit.
42
40
  */
43
41
  export function createAgentDispatchBroker({
44
42
  dispatchAgent,
@@ -99,10 +97,8 @@ export function createAgentDispatchBroker({
99
97
  const run = async () => {
100
98
  if (controller.signal.aborted) throw abortError(controller.signal);
101
99
  const timeout = Number(params.timeout);
102
- // Execution happens in the recyclable session runtime worker, not in
103
- // this permanent daemon process: provider/orchestrator churn from
104
- // memory-cycle agents returns to the OS at the worker's next memory
105
- // recycle instead of accumulating in daemon commit forever.
100
+ // The host owns whether execution is in-process or isolated; the broker
101
+ // keeps scheduling, cancellation, and call identity transport-neutral.
106
102
  return dispatchAgent({
107
103
  dispatchId: id,
108
104
  agent,
@@ -50,15 +50,13 @@ export function createNotify(mgr, { notifySessionCompletion } = {}) {
50
50
 
51
51
  function workerNotifyFn(workerSessionId, notifyContext = {}) {
52
52
  const workerId = clean(workerSessionId);
53
- const ownerSessionId = clean(notifyContext?.callerSessionId || notifyContext?.sessionId);
54
53
  return (text, meta = {}) => {
55
- const ownerDelivered = ownerSessionId
56
- ? notifyOwner(ownerSessionId, text, meta)
57
- : false;
58
- const workerDelivered = workerId && workerId !== ownerSessionId
59
- ? enqueueCompletionMessage(workerId, text, meta)
60
- : ownerDelivered;
61
- return ownerSessionId ? ownerDelivered : workerDelivered;
54
+ // Tool completions produced inside a Subagent belong to that Subagent's
55
+ // session. Mirroring them to the owner made every promoted shell command
56
+ // appear as a second Lead-level completion card. Agent task completion
57
+ // has its own owner delivery path below; only that terminal handoff
58
+ // crosses the parent boundary.
59
+ return workerId ? enqueueCompletionMessage(workerId, text, meta) : false;
62
60
  };
63
61
  }
64
62
 
@@ -0,0 +1,37 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { createNotify } from './notify.mjs';
5
+
6
+ test('Subagent tool completion stays in the worker session', () => {
7
+ const queued = [];
8
+ const ownerDeliveries = [];
9
+ const { workerNotifyFn } = createNotify({
10
+ enqueuePendingMessage(sessionId, message) {
11
+ queued.push({ sessionId, message });
12
+ return 1;
13
+ },
14
+ }, {
15
+ notifySessionCompletion(sessionId, text, meta) {
16
+ ownerDeliveries.push({ sessionId, text, meta });
17
+ return true;
18
+ },
19
+ });
20
+
21
+ const delivered = workerNotifyFn('sess_worker', {
22
+ callerSessionId: 'sess_lead',
23
+ })(
24
+ 'Async shell task job_test (completed, exit 0) finished.\n\nresult',
25
+ {
26
+ type: 'shell_task_result',
27
+ execution_surface: 'shell',
28
+ execution_id: 'job_test',
29
+ status: 'completed',
30
+ },
31
+ );
32
+
33
+ assert.equal(delivered, true);
34
+ assert.equal(ownerDeliveries.length, 0);
35
+ assert.equal(queued.length, 1);
36
+ assert.equal(queued[0].sessionId, 'sess_worker');
37
+ });
@@ -59,7 +59,7 @@ import { createChannelSessionRouter } from './channel-session-router.mjs';
59
59
  import { createSessionTransport } from './session-transport.mjs';
60
60
  import { createSessionService } from './session-service.mjs';
61
61
  import { createSessionProtocolClient } from './session-protocol.mjs';
62
- import { createSessionRuntimeHost } from './session-runtime-host.mjs';
62
+ import { createDaemonSessionRuntimeHost } from './session-runtime-host-factory.mjs';
63
63
  import { getStandaloneMemoryRuntime } from './memory-runtime-proxy.mjs';
64
64
  import {
65
65
  compareRuntimeVersions,
@@ -244,10 +244,10 @@ const eventLoopDelay = monitorEventLoopDelay({ resolution: 20 });
244
244
  eventLoopDelay.enable();
245
245
  let eventLoopLagTimer = null;
246
246
 
247
- function startMemoryRuntimeEarly() {
248
- // Start the isolated memory process immediately after singleton ownership is
249
- // established. Do not await it: daemon front-door readiness remains
250
- // independent while PG/embedding cold-start overlaps all other boot work.
247
+ function registerMemoryRuntimeLazy() {
248
+ // Register the shared proxy immediately so daemon shutdown owns its
249
+ // lifecycle, but keep the isolated process off the startup path. A real
250
+ // memory call or the post-connect idle warmup starts the exact singleton.
251
251
  if (process.env.MIXDOG_DAEMON_SKIP_MEMORY === '1') {
252
252
  log('memory runtime skipped (MIXDOG_DAEMON_SKIP_MEMORY=1)');
253
253
  return;
@@ -259,9 +259,7 @@ function startMemoryRuntimeEarly() {
259
259
  dataDir: DATA_DIR,
260
260
  cwd: CWD,
261
261
  });
262
- void memoryRuntime.init()
263
- .then(() => log('memory runtime ready in isolated process'))
264
- .catch((e) => log(`memory.start failed (non-fatal): ${e?.message || e}`));
262
+ log('memory runtime registered for lazy start');
265
263
  } catch (e) { log(`memory.start setup failed (non-fatal): ${e?.message || e}`); }
266
264
  }
267
265
 
@@ -414,11 +412,10 @@ async function main() {
414
412
  process.exit(0);
415
413
  }
416
414
  process.on('exit', () => { try { releaseSingletonOwner(OWNER_PATH, process.pid); } catch {} });
417
- startMemoryRuntimeEarly();
415
+ registerMemoryRuntimeLazy();
418
416
  agentDispatchBroker = createAgentDispatchBroker({
419
- // Memory-cycle agents execute inside the recyclable session runtime
420
- // worker: this permanent daemon never loads the orchestrator graph for
421
- // them, so its commit stays flat across cycles.
417
+ // Memory-cycle agents use the same lazy provider/orchestrator graph as
418
+ // session actors; it stays unloaded until a real cycle requests it.
422
419
  dispatchAgent: (payload, options) => {
423
420
  if (!sessionRuntimeHost) throw new Error('session runtime host is not ready');
424
421
  return sessionRuntimeHost.agentDispatch(payload, options);
@@ -580,10 +577,14 @@ async function main() {
580
577
  };
581
578
  let sessionRuntimePrewarmStarted = false;
582
579
  let sessionRuntimePrewarmPromise = null;
583
- sessionRuntimeHost = createSessionRuntimeHost({
580
+ sessionRuntimeHost = createDaemonSessionRuntimeHost({
584
581
  cwd: CWD,
585
582
  log,
586
583
  });
584
+ // Codex-style runtime: daemon routing and independent async session actors
585
+ // share one V8 isolate and module graph. CPU-heavy work stays in bounded
586
+ // native helpers/worker pools instead of duplicating the whole runtime.
587
+ log(`session runtime mode=${sessionRuntimeHost.status.mode}`);
587
588
  function prewarmSessionRuntime() {
588
589
  if (sessionRuntimePrewarmPromise) return sessionRuntimePrewarmPromise;
589
590
  sessionRuntimePrewarmStarted = true;
@@ -592,7 +593,7 @@ async function main() {
592
593
  .catch((error) => log(`image pipeline prewarm failed (non-fatal): ${error?.message || error}`));
593
594
  sessionRuntimePrewarmPromise = sessionRuntimeHost.prewarm()
594
595
  .then(() => {
595
- log('session runtime worker/agent-loop/keychain/memory prewarm ready');
596
+ log('session runtime/agent-loop/keychain prewarm ready');
596
597
  return true;
597
598
  })
598
599
  .catch((error) => {
@@ -605,6 +606,8 @@ async function main() {
605
606
  }
606
607
  sessionService = createSessionService({
607
608
  createSessionRuntime: (options) => sessionRuntimeHost.create(options),
609
+ subscribeExternalSessionStates: (listener) =>
610
+ sessionRuntimeHost.subscribeAgentSessionStates(listener),
608
611
  sessionExists: async (sessionId) => {
609
612
  const store = await desktopRuntime.loadSessionStore();
610
613
  return store.storedSessionExists?.(sessionId) === true;
@@ -670,7 +673,9 @@ async function main() {
670
673
  ...eventLoopStatus(),
671
674
  }),
672
675
  onClientsEmpty: () => { maybeSelfShutdown('no live session clients'); },
673
- onClientRegistered: () => { void prewarmSessionRuntime(); },
676
+ onClientRegistered: () => {
677
+ void prewarmSessionRuntime();
678
+ },
674
679
  onClientDropped: (token) => { try { sessionService.releaseClient(token); } catch {} },
675
680
  onUpgradeRequested: requestDaemonReplacement,
676
681
  });
@@ -720,6 +725,17 @@ async function main() {
720
725
  if (status.eventLoopP99Ms >= 250) {
721
726
  log(`event-loop lag p95=${status.eventLoopP95Ms}ms p99=${status.eventLoopP99Ms}ms max=${status.eventLoopMaxMs}ms`);
722
727
  }
728
+ // Legacy external runtime hosts may still report shard-local lag. The
729
+ // production in-process host is already covered by daemon loop telemetry.
730
+ const shards = sessionRuntimeHost?.status?.shards || [];
731
+ const lagging = shards.filter((shard) => Number(shard?.lag?.p99Ms) >= 250 || shard?.degraded);
732
+ if (lagging.length > 0) {
733
+ log(`session runtime shard lag ${lagging.map((shard) => (
734
+ `#${shard.index}${shard.degraded ? '(quarantined)' : ''}`
735
+ + ` p95=${shard.lag?.p95Ms ?? -1}ms p99=${shard.lag?.p99Ms ?? -1}ms`
736
+ + ` max=${shard.lag?.maxMs ?? -1}ms runtimes=${shard.runtimes}`
737
+ )).join(' ')}`);
738
+ }
723
739
  eventLoopDelay.reset();
724
740
  }, 30_000);
725
741
  eventLoopLagTimer.unref?.();
@@ -81,6 +81,7 @@ function pluginHookConfigEntries(dataDir) {
81
81
  }
82
82
  const entries = [];
83
83
  for (const plugin of plugins || []) {
84
+ if (plugin?.enabled === false) continue;
84
85
  const root = String(plugin?.root || '').trim();
85
86
  if (!root || !existsSync(root)) continue;
86
87
  const id = cleanHookId(plugin.id || plugin.name || plugin.title || root.split(/[\\/]/).pop());
@@ -242,6 +242,7 @@ function enrichEntry(entry) {
242
242
  const name = clean(manifest.name) || clean(manifest.id) || clean(entry.name) || displayNameFromUrl(entry.source);
243
243
  return {
244
244
  ...entry,
245
+ enabled: entry.enabled !== false,
245
246
  name,
246
247
  title: clean(manifest.title) || clean(manifest.displayName) || clean(entry.title) || name,
247
248
  version: clean(manifest.version) || clean(entry.version) || null,
@@ -280,6 +281,7 @@ export function addPlugin(sourceInput, { dataDir = resolvePluginData(), name } =
280
281
  title: clean(manifest.title) || clean(manifest.displayName) || clean(name) || displayNameFromUrl(normalized.displaySource),
281
282
  version: clean(manifest.version) || null,
282
283
  description: clean(manifest.description),
284
+ enabled: true,
283
285
  installedAt: nowIso(),
284
286
  updatedAt: nowIso(),
285
287
  };
@@ -338,6 +340,21 @@ export function updatePlugin(idOrName, { dataDir = resolvePluginData() } = {}) {
338
340
  });
339
341
  }
340
342
 
343
+ export function setPluginEnabled(idOrName, enabled, { dataDir = resolvePluginData() } = {}) {
344
+ const key = clean(idOrName);
345
+ if (!key) throw new Error('plugin id/name is required');
346
+ return mutateRegistry(dataDir, (registry) => {
347
+ const index = pluginIndex(registry, key);
348
+ if (index < 0) throw new Error(`plugin not registered: ${key}`);
349
+ const next = {
350
+ ...registry.plugins[index],
351
+ enabled: enabled !== false,
352
+ };
353
+ registry.plugins[index] = next;
354
+ return enrichEntry(next);
355
+ });
356
+ }
357
+
341
358
  export function removePlugin(idOrName, { dataDir = resolvePluginData() } = {}) {
342
359
  const key = clean(idOrName);
343
360
  if (!key) throw new Error('plugin id/name is required');
@@ -18,7 +18,9 @@ import {
18
18
  _publishManagedPluginRoot,
19
19
  addPlugin,
20
20
  listRegisteredPlugins,
21
+ setPluginEnabled,
21
22
  } from './plugin-admin.mjs';
23
+ import { hookConfigEntries } from './hook-bus/config.mjs';
22
24
 
23
25
  test('plugin registry persists atomically with owner-only permissions', () => {
24
26
  const root = mkdtempSync(join(tmpdir(), 'mixdog-plugin-registry-'));
@@ -33,6 +35,11 @@ test('plugin registry persists atomically with owner-only permissions', () => {
33
35
  const registry = JSON.parse(readFileSync(registryPath, 'utf8'));
34
36
  assert.equal(registry.plugins.length, 1);
35
37
  assert.equal(listRegisteredPlugins({ dataDir })[0].name, 'local-test');
38
+ assert.equal(listRegisteredPlugins({ dataDir })[0].enabled, true);
39
+ assert.equal(hookConfigEntries(dataDir, root).some((entry) => entry.sourceType === 'plugin'), true);
40
+ setPluginEnabled('local-test', false, { dataDir });
41
+ assert.equal(listRegisteredPlugins({ dataDir })[0].enabled, false);
42
+ assert.equal(hookConfigEntries(dataDir, root).some((entry) => entry.sourceType === 'plugin'), false);
36
43
  if (process.platform !== 'win32') {
37
44
  assert.equal(statSync(registryPath).mode & 0o777, 0o600);
38
45
  }
@@ -44,6 +44,7 @@ import {
44
44
 
45
45
  const API_PROVIDERS = Object.freeze([
46
46
  Object.freeze({ id: 'opencode-go', name: 'OpenCode Go API', env: 'OPENCODE_API_KEY', url: 'https://opencode.ai' }),
47
+ Object.freeze({ id: 'openrouter', name: 'OpenRouter', env: 'OPENROUTER_API_KEY', url: 'https://openrouter.ai/keys' }),
47
48
  Object.freeze({ id: 'openai', name: 'OpenAI API', env: 'OPENAI_API_KEY', url: 'https://platform.openai.com/api-keys' }),
48
49
  Object.freeze({ id: 'anthropic', name: 'Anthropic API', env: 'ANTHROPIC_API_KEY', url: 'https://console.anthropic.com/settings/keys' }),
49
50
  Object.freeze({ id: 'gemini', name: 'Gemini API', env: 'GEMINI_API_KEY', url: 'https://aistudio.google.com/apikey' }),
@@ -15,6 +15,7 @@ export const SESSION_READ_ACTIONS = Object.freeze([
15
15
  'getCompactionSettings',
16
16
  'getDisabledSkills',
17
17
  'getMediaJob',
18
+ 'getMcpServerConfig',
18
19
  'getOnboardingStatus',
19
20
  'getOAuthProviderLoginStatus',
20
21
  'getOutputStyle',
@@ -79,6 +80,7 @@ export const SESSION_CONFIGURE_ACTIONS = Object.freeze([
79
80
  'deleteWorkflow',
80
81
  'enablePluginMcp',
81
82
  'forgetProviderAuth',
83
+ 'goalControl',
82
84
  'inheritFrom',
83
85
  'loginOAuthProvider',
84
86
  'loginOpenCodeGoUsage',
@@ -107,10 +109,12 @@ export const SESSION_CONFIGURE_ACTIONS = Object.freeze([
107
109
  'runScheduleNow',
108
110
  'runUpdateNow',
109
111
  'saveAgentDefinition',
112
+ 'saveMcpServer',
110
113
  'saveOpenAIUsageSessionKey',
111
114
  'saveOpenCodeGoUsageAuth',
112
115
  'saveProviderApiKey',
113
116
  'saveSchedule',
117
+ 'saveSkill',
114
118
  'saveWebhook',
115
119
  'saveWorkflowPack',
116
120
  'selectTools',
@@ -129,6 +133,7 @@ export const SESSION_CONFIGURE_ACTIONS = Object.freeze([
129
133
  'setMemoryToolsEnabled',
130
134
  'setModel',
131
135
  'setOutputStyle',
136
+ 'setPluginEnabled',
132
137
  'setProfile',
133
138
  'setProgressHint',
134
139
  'setRecapEnabled',