mixdog 0.9.19 → 0.9.20

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 (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -29
  3. package/package.json +3 -2
  4. package/scripts/build-runtime-windows.ps1 +242 -242
  5. package/scripts/build-tui.mjs +6 -0
  6. package/scripts/hook-bus-test.mjs +8 -0
  7. package/scripts/log-writer-guard-smoke.mjs +131 -0
  8. package/scripts/reactive-compact-persist-smoke.mjs +22 -6
  9. package/scripts/recall-bench-cases.json +0 -1
  10. package/scripts/recall-quality-cases.json +1 -2
  11. package/scripts/recall-usecase-cases.json +1 -1
  12. package/scripts/session-ingest-compaction-smoke.mjs +241 -0
  13. package/scripts/smoke-runtime-negative.ps1 +106 -106
  14. package/scripts/tool-efficiency-diag.mjs +1 -1
  15. package/scripts/tool-smoke.mjs +150 -45
  16. package/src/help.mjs +2 -5
  17. package/src/lib/mixdog-debug.cjs +13 -0
  18. package/src/mixdog-session-runtime.mjs +7 -3328
  19. package/src/rules/lead/02-channels.md +3 -3
  20. package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
  21. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +663 -0
  22. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
  23. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
  24. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
  25. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
  26. package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
  27. package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
  28. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
  29. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
  30. package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
  31. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
  32. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
  33. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
  34. package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
  35. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
  36. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
  37. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
  38. package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
  39. package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
  40. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
  41. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
  42. package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
  43. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
  44. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
  45. package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
  46. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
  47. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
  48. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +108 -2
  49. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
  50. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
  51. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
  52. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
  53. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
  54. package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
  55. package/src/runtime/channels/index.mjs +6 -2183
  56. package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
  57. package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
  58. package/src/runtime/channels/lib/network-retry.mjs +23 -0
  59. package/src/runtime/channels/lib/owned-runtime.mjs +547 -0
  60. package/src/runtime/channels/lib/transcript-binding.mjs +336 -0
  61. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
  62. package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
  63. package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
  64. package/src/runtime/channels/lib/worker-main.mjs +771 -0
  65. package/src/runtime/memory/index.mjs +73 -1725
  66. package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
  67. package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
  68. package/src/runtime/memory/lib/http-router.mjs +772 -0
  69. package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
  70. package/src/runtime/memory/lib/memory-embed.mjs +28 -7
  71. package/src/runtime/memory/lib/memory-recall-store.mjs +4 -4
  72. package/src/runtime/memory/lib/query-handlers.mjs +2 -2
  73. package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
  74. package/src/session-runtime/boot-profile.mjs +36 -0
  75. package/src/session-runtime/channel-config-api.mjs +70 -0
  76. package/src/session-runtime/context-status.mjs +181 -0
  77. package/src/session-runtime/env.mjs +17 -0
  78. package/src/session-runtime/lifecycle-api.mjs +242 -0
  79. package/src/session-runtime/model-route-api.mjs +198 -0
  80. package/src/session-runtime/provider-auth-api.mjs +135 -0
  81. package/src/session-runtime/resource-api.mjs +282 -0
  82. package/src/session-runtime/runtime-core.mjs +2046 -0
  83. package/src/session-runtime/session-turn-api.mjs +274 -0
  84. package/src/session-runtime/tool-catalog.mjs +18 -264
  85. package/src/session-runtime/tool-defs.mjs +2 -2
  86. package/src/session-runtime/workflow-agents-api.mjs +238 -0
  87. package/src/standalone/agent-tool.mjs +2 -2
  88. package/src/standalone/channel-worker.mjs +4 -0
  89. package/src/standalone/memory-runtime-proxy.mjs +56 -6
  90. package/src/tui/App.jsx +39 -28
  91. package/src/tui/app/core-memory-picker.mjs +1 -1
  92. package/src/tui/app/use-mouse-input.mjs +6 -0
  93. package/src/tui/app/use-transcript-scroll.mjs +77 -3
  94. package/src/tui/dist/index.mjs +1990 -1527
  95. package/src/tui/engine/context-state.mjs +145 -0
  96. package/src/tui/engine/prompt-history.mjs +27 -0
  97. package/src/tui/engine/session-api-ext.mjs +478 -0
  98. package/src/tui/engine/session-api.mjs +545 -0
  99. package/src/tui/engine/session-flow.mjs +485 -0
  100. package/src/tui/engine/turn.mjs +1078 -0
  101. package/src/tui/engine.mjs +68 -2620
  102. package/src/tui/index.jsx +7 -0
  103. package/vendor/ink/build/ink.js +16 -1
  104. package/vendor/ink/build/output.js +30 -4
  105. package/vendor/ink/build/render.js +5 -0
  106. package/src/workflows/sequential/WORKFLOW.md +0 -51
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { dirname, join, resolve } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { __applyStandaloneToolDefaultsForTest, __renderToolSearchForTest, compactToolSearchDescription, defaultDeferredToolNames, SKILL_TOOL, TOOL_SEARCH_TOOL } from '../src/mixdog-session-runtime.mjs';
7
+ import { applyInitialDeferredToolManifestToBp1, buildDeferredToolManifest } from '../src/runtime/agent/orchestrator/context/collect.mjs';
7
8
  import { buildExplorerPrompt, EXPLORE_TOOL, MAX_FANOUT_QUERIES, normalizeExploreQueries } from '../src/standalone/explore-tool.mjs';
8
9
  import { AGENT_TOOL, createStandaloneAgent } from '../src/standalone/agent-tool.mjs';
9
10
  import { parseHeadlessRoleCommand } from '../src/app.mjs';
@@ -644,6 +645,72 @@ if (!/^Error[\s:[]/.test(String(stalePatchOut)) || !/apply_patch/i.test(String(s
644
645
  throw new Error(`apply_patch stale context must return an Error result, not throw or pass:\n${stalePatchOut}`);
645
646
  }
646
647
 
648
+ // Malformed-but-unambiguous patch openings must be absorbed (dry-run, so no
649
+ // write). Each targets the same known-good smoke.mjs line the cases above use.
650
+ const smokeBody = `@@
651
+ -process.stdout.write('smoke passed ✓\\n');
652
+ +process.stdout.write('smoke passed ok\\n');
653
+ *** End Patch
654
+ `;
655
+ const absorbCases = [
656
+ ['leading blank lines', `\n\n*** Begin Patch\n*** Update File: scripts/smoke.mjs\n${smokeBody}`],
657
+ ['decorated begin header', `*** Begin Patch (V4A) ***\n*** Update File: scripts/smoke.mjs\n${smokeBody}`],
658
+ ['bare file path opening', `*** Begin Patch\nscripts/smoke.mjs\n${smokeBody}`],
659
+ ['File: prefixed opening', `*** Begin Patch\nFile: scripts/smoke.mjs\n${smokeBody}`],
660
+ ['unified body in envelope', `*** Begin Patch\n--- scripts/smoke.mjs\n+++ scripts/smoke.mjs\n${smokeBody}`],
661
+ ];
662
+ for (const [label, patch] of absorbCases) {
663
+ const out = await executePatchTool('apply_patch', { base_path: root, dry_run: true, fuzzy: false, patch }, root);
664
+ assertOk(`apply_patch absorbs ${label}`, out, /checked|validated|dry|OK/i);
665
+ }
666
+
667
+ const ambiguousPatchOut = await executePatchTool('apply_patch', {
668
+ base_path: root,
669
+ dry_run: true,
670
+ fuzzy: false,
671
+ patch: `*** Begin Patch\nthis line is not a valid opening\n${smokeBody}`,
672
+ }, root);
673
+ if (!/^Error[\s:[]/.test(String(ambiguousPatchOut)) || !/before a file header|V4A/i.test(String(ambiguousPatchOut))) {
674
+ throw new Error(`apply_patch must keep erroring on genuinely ambiguous openings:\n${ambiguousPatchOut}`);
675
+ }
676
+
677
+ // Unified-looking first body line but real V4A file sections appear later: the
678
+ // envelope must NOT be stripped to unified — it stays ambiguous and errors.
679
+ const mixedPatchOut = await executePatchTool('apply_patch', {
680
+ base_path: root,
681
+ dry_run: true,
682
+ fuzzy: false,
683
+ patch: `*** Begin Patch\n--- scripts/smoke.mjs\n*** Update File: scripts/smoke.mjs\n${smokeBody}`,
684
+ }, root);
685
+ if (!/^Error[\s:[]/.test(String(mixedPatchOut)) || !/before a file header|V4A/i.test(String(mixedPatchOut))) {
686
+ throw new Error(`apply_patch must keep erroring on mixed unified/V4A openings:\n${mixedPatchOut}`);
687
+ }
688
+
689
+ // Compacted-history placeholder guard: EVERY [mixdog compacted …] variant must
690
+ // be rejected with the corrective message BEFORE format dispatch/salvage, both
691
+ // as the first line and standalone mid-body (after a *** Begin Patch header).
692
+ const compactedGuardCases = [
693
+ ['legacy key: prefix', '[mixdog compacted patch: 4096 chars, sha256:deadbeefdeadbeef]\n*** Begin Patch\n*** Update File: a.txt\n+x\n*** End Patch\n'],
694
+ ['variant key form', '[mixdog compacted patch v4a, sha256:deadbeefdeadbeef]\n*** Begin Patch\n*** Update File: a.txt\n+x\n*** End Patch\n'],
695
+ ['no chars/sha detail', '[mixdog compacted old_string]\n'],
696
+ ['mid-body standalone', '*** Begin Patch\n*** Update File: a.txt\n[mixdog compacted patch v4a, sha256:deadbeefdeadbeef]\n*** End Patch\n'],
697
+ ];
698
+ for (const [label, patch] of compactedGuardCases) {
699
+ const out = await executePatchTool('apply_patch', { base_path: root, dry_run: true, fuzzy: false, patch }, root);
700
+ if (!/^Error[\s:[]/.test(String(out)) || !/compacted-history placeholder/i.test(String(out))) {
701
+ throw new Error(`apply_patch must reject compacted placeholder (${label}):\n${out}`);
702
+ }
703
+ }
704
+ // A legit unified edit whose body content mentions the literal text on a diff
705
+ // line (+/-/space) must still parse — the guard only trips on non-diff lines.
706
+ const compactedFalsePositiveOut = await executePatchTool('apply_patch', {
707
+ base_path: root,
708
+ dry_run: true,
709
+ fuzzy: false,
710
+ patch: `*** Begin Patch\n*** Add File: compacted-note.txt\n+[mixdog compacted patch: 10 chars, sha256:abc]\n*** End Patch\n`,
711
+ }, root);
712
+ assertOk('apply_patch keeps diff-line placeholder text', compactedFalsePositiveOut, /checked|validated|dry|OK/i);
713
+
647
714
  const shellOutPromise = executeBuiltinTool('shell', {
648
715
  command: 'node --version',
649
716
  cwd: root,
@@ -1485,7 +1552,10 @@ setInternalToolsProvider({
1485
1552
  const resumed = await resumeSession(session.id, 'full');
1486
1553
  const resumedTools = (resumed?.tools || []).map((tool) => tool?.name).filter(Boolean);
1487
1554
  const expected = expectedForHiddenAgent(permission, schemaAllowedTools);
1488
- if (expected && (JSON.stringify(tools) !== JSON.stringify(expected) || JSON.stringify(resumedTools) !== JSON.stringify(expected))) {
1555
+ // Order-insensitive: the session tool surface follows catalog order, while
1556
+ // schemaAllowedTools declares an allow-set; only set equality is contractual.
1557
+ const asSet = (list) => JSON.stringify(list.slice().sort());
1558
+ if (expected && (asSet(tools) !== asSet(expected) || asSet(resumedTools) !== asSet(expected))) {
1489
1559
  throw new Error(`hidden agent ${agent} schema mismatch: expected=${expected.join(', ')} tools=${tools.join(', ')} resumed=${resumedTools.join(', ')}`);
1490
1560
  }
1491
1561
  const leaked = tools.filter((name) => hiddenBadTools.has(name) && !(expected || []).includes(name));
@@ -1644,15 +1714,25 @@ if (/line\/context/i.test(JSON.stringify(readTool?.inputSchema || {}))) {
1644
1714
  // session's provider/model in place, or a mid-conversation model/provider
1645
1715
  // switch silently forces a full prompt-cache rewrite (seen as a
1646
1716
  // promptΔ spike + cache_ratio=0% turn in session-bench).
1647
- const runtimeSrc = readFileSync(resolve(root, 'src/mixdog-session-runtime.mjs'), 'utf8');
1717
+ // God-file splits move implementation into module dirs; scan facade + all
1718
+ // split modules so these source-text guards survive refactors.
1719
+ const readMjsSources = (rel) => {
1720
+ const abs = resolve(root, rel);
1721
+ if (rel.endsWith('.mjs')) return readFileSync(abs, 'utf8');
1722
+ return readdirSync(abs, { recursive: true })
1723
+ .filter((f) => String(f).endsWith('.mjs'))
1724
+ .map((f) => readFileSync(resolve(abs, String(f)), 'utf8'))
1725
+ .join('\n');
1726
+ };
1727
+ const runtimeSrc = [readMjsSources('src/mixdog-session-runtime.mjs'), readMjsSources('src/session-runtime')].join('\n');
1648
1728
  const setRouteBlock = runtimeSrc.match(/async setRoute\(next, options = \{\}\) \{[\s\S]*?\n \},\n/)?.[0] || '';
1649
1729
  if (!/applyToCurrentSession = options\?\.applyToCurrentSession === true/.test(setRouteBlock)) {
1650
1730
  throw new Error('setRoute must default applyToCurrentSession to false (model changes apply to the next session only)');
1651
1731
  }
1652
- if (!/if \(!applyToCurrentSession\)/.test(setRouteBlock) || !/return route;/.test(setRouteBlock)) {
1732
+ if (!/if \(!applyToCurrentSession\)/.test(setRouteBlock) || !/return (?:route|getRoute\(\));/.test(setRouteBlock)) {
1653
1733
  throw new Error('setRoute must early-return before touching the live session when applyToCurrentSession is false');
1654
1734
  }
1655
- const engineSrc = readFileSync(resolve(root, 'src/tui/engine.mjs'), 'utf8');
1735
+ const engineSrc = [readMjsSources('src/tui/engine.mjs'), readMjsSources('src/tui/engine')].join('\n');
1656
1736
  if (/setRoute\(\{ model: m \}, \{ applyToCurrentSession: true \}\)/.test(engineSrc)) {
1657
1737
  throw new Error('TUI setModel must not force applyToCurrentSession:true (model changes must apply to the next session only)');
1658
1738
  }
@@ -1774,16 +1854,23 @@ const toolSearchSession = {
1774
1854
  deferredToolCatalog: smokeCatalog.slice(),
1775
1855
  deferredSelectedTools: [...fullDefaults],
1776
1856
  };
1777
- const searchOnlyResult = JSON.parse(__renderToolSearchForTest({ query: 'shell' }, toolSearchSession, 'full'));
1778
- for (const name of ['shell', 'task']) {
1779
- if (!searchOnlyResult.selected?.tools?.added?.includes(name)) {
1780
- throw new Error(`tool_search high-confidence query should auto-load ${name}: ${JSON.stringify(searchOnlyResult.selected)}`);
1781
- }
1857
+ // A plain query is a case-insensitive substring filter over name+description.
1858
+ // It lists matches only it never auto-loads or ranks.
1859
+ const listQueryResult = JSON.parse(__renderToolSearchForTest({ query: 'shell' }, toolSearchSession, 'full'));
1860
+ if (listQueryResult.selected) {
1861
+ throw new Error(`tool_search plain query must not auto-load: ${JSON.stringify(listQueryResult.selected)}`);
1862
+ }
1863
+ if (!listQueryResult.matches.some((row) => row.name === 'shell')) {
1864
+ throw new Error(`tool_search plain query should list substring matches: ${JSON.stringify(listQueryResult.matches)}`);
1782
1865
  }
1783
- if (!searchOnlyResult.activeTools.includes('shell') || !searchOnlyResult.activeTools.includes('task')) {
1784
- throw new Error(`tool_search query should activate legacy selected tools: ${searchOnlyResult.activeTools.join(',')}`);
1866
+ if (listQueryResult.activeTools.includes('shell') || (Array.isArray(listQueryResult.discoveredTools) && listQueryResult.discoveredTools.includes('shell'))) {
1867
+ throw new Error(`tool_search plain query must not activate/discover tools: ${JSON.stringify(listQueryResult)}`);
1785
1868
  }
1869
+ // query "select:a,b" is the explicit query-side loader (aliases expand).
1786
1870
  const bulkSelectResult = JSON.parse(__renderToolSearchForTest({ query: 'select:shell,recall' }, toolSearchSession, 'full'));
1871
+ if (bulkSelectResult.selected?.mode !== 'select') {
1872
+ throw new Error(`tool_search query-select must report select mode: ${JSON.stringify(bulkSelectResult.selected)}`);
1873
+ }
1787
1874
  for (const name of ['shell', 'task', 'recall']) {
1788
1875
  if (!bulkSelectResult.activeTools.includes(name)) {
1789
1876
  throw new Error(`tool_search bulk select missing ${name}: ${JSON.stringify(bulkSelectResult)}`);
@@ -1858,7 +1945,8 @@ if (nativeGrokPatchTool?.type !== 'function' || nativeGrokPatchTool?.format || n
1858
1945
  if (nativeGrokPatchTool.parameters?.properties?.patch?.type !== 'string') {
1859
1946
  throw new Error(`Grok native tool_search apply_patch must preserve patch JSON schema: ${JSON.stringify(nativeGrokPatchTool)}`);
1860
1947
  }
1861
- const nativeRunQuerySession = {
1948
+ // Native query-select discovers (without mutating active schemas); aliases expand.
1949
+ const nativeSelectQuerySession = {
1862
1950
  tools: smokeCatalog.filter((tool) => fullDefaults.has(tool?.name)),
1863
1951
  deferredToolCatalog: smokeCatalog.slice(),
1864
1952
  deferredSelectedTools: [...fullDefaults],
@@ -1866,16 +1954,20 @@ const nativeRunQuerySession = {
1866
1954
  deferredProviderMode: 'native',
1867
1955
  deferredNativeTools: true,
1868
1956
  };
1869
- const nativeRunQueryResult = JSON.parse(__renderToolSearchForTest({ query: 'run tests' }, nativeRunQuerySession, 'full'));
1870
- for (const name of ['shell', 'task']) {
1871
- if (!nativeRunQueryResult.discoveredTools.includes(name)) {
1872
- throw new Error(`native tool_search run/tests query should discover ${name}: ${JSON.stringify(nativeRunQueryResult)}`);
1957
+ const nativeSelectQueryResult = JSON.parse(__renderToolSearchForTest({ query: 'select:search' }, nativeSelectQuerySession, 'full'));
1958
+ for (const name of ['search', 'web_fetch']) {
1959
+ if (!nativeSelectQueryResult.discoveredTools.includes(name)) {
1960
+ throw new Error(`native tool_search query-select should discover ${name}: ${JSON.stringify(nativeSelectQueryResult)}`);
1873
1961
  }
1874
1962
  }
1875
- if (nativeRunQueryResult.activeTools.includes('shell') || nativeRunQueryResult.activeTools.includes('task')) {
1876
- throw new Error(`native tool_search query must not mutate active schemas: ${JSON.stringify(nativeRunQueryResult)}`);
1963
+ if (nativeSelectQueryResult.activeTools.includes('search') || nativeSelectQueryResult.activeTools.includes('web_fetch')) {
1964
+ throw new Error(`native tool_search must not mutate active schemas: ${JSON.stringify(nativeSelectQueryResult)}`);
1965
+ }
1966
+ if (!nativeSelectQueryResult.nativeToolSearch?.toolReferences?.includes('search')) {
1967
+ throw new Error(`native query-select must return nativeToolSearch payload: ${JSON.stringify(nativeSelectQueryResult.nativeToolSearch)}`);
1877
1968
  }
1878
- const nativeWebQuerySession = {
1969
+ // A plain query never auto-loads/discovers, even on native providers.
1970
+ const nativePlainQuerySession = {
1879
1971
  tools: smokeCatalog.filter((tool) => fullDefaults.has(tool?.name)),
1880
1972
  deferredToolCatalog: smokeCatalog.slice(),
1881
1973
  deferredSelectedTools: [...fullDefaults],
@@ -1883,35 +1975,48 @@ const nativeWebQuerySession = {
1883
1975
  deferredProviderMode: 'native',
1884
1976
  deferredNativeTools: true,
1885
1977
  };
1886
- const nativeWebQueryResult = JSON.parse(__renderToolSearchForTest({ query: 'web docs' }, nativeWebQuerySession, 'full'));
1887
- for (const name of ['search', 'web_fetch']) {
1888
- if (!nativeWebQueryResult.discoveredTools.includes(name)) {
1889
- throw new Error(`native tool_search web/docs query should discover ${name}: ${JSON.stringify(nativeWebQueryResult)}`);
1978
+ for (const q of ['run tests', 'web docs', 'memory previous', 'status']) {
1979
+ const r = JSON.parse(__renderToolSearchForTest({ query: q }, nativePlainQuerySession, 'full'));
1980
+ if (r.selected || r.discoveredTools.length) {
1981
+ throw new Error(`native tool_search plain query "${q}" must not auto-load/discover: ${JSON.stringify(r)}`);
1890
1982
  }
1891
1983
  }
1892
- const nativeRecallQuerySession = {
1893
- tools: smokeCatalog.filter((tool) => fullDefaults.has(tool?.name)),
1894
- deferredToolCatalog: smokeCatalog.slice(),
1895
- deferredSelectedTools: [...fullDefaults],
1896
- deferredDiscoveredTools: [],
1897
- deferredProviderMode: 'native',
1898
- deferredNativeTools: true,
1984
+ // Skill-style deferred manifest: `- name: description` lines, `<`/`>` sanitized,
1985
+ // bare names allowed, header instructs direct calls, empty pool → ''.
1986
+ const manifestText = buildDeferredToolManifest([
1987
+ { name: 'shell', description: 'Run commands.' },
1988
+ { name: 'search', description: 'Web <search> now.' },
1989
+ 'recall',
1990
+ ]);
1991
+ if (!/<available-deferred-tools>/.test(manifestText) || !/- shell: Run commands\./.test(manifestText)) {
1992
+ throw new Error(`deferred manifest must render "- name: description" lines: ${manifestText}`);
1993
+ }
1994
+ if (!/call any tool listed below directly/i.test(manifestText)) {
1995
+ throw new Error(`deferred manifest must tell the model it can call listed tools directly: ${manifestText}`);
1996
+ }
1997
+ if (!/^- recall$/m.test(manifestText)) {
1998
+ throw new Error(`deferred manifest must allow bare names without descriptions: ${manifestText}`);
1999
+ }
2000
+ if (/[<>]/.test(manifestText.replace(/<\/?available-deferred-tools>/g, ''))) {
2001
+ throw new Error(`deferred manifest must sanitize angle brackets in descriptions: ${manifestText}`);
2002
+ }
2003
+ if (buildDeferredToolManifest([]) !== '') {
2004
+ throw new Error('empty deferred pool must yield an empty manifest');
2005
+ }
2006
+ const bp1ManifestSession = {
2007
+ messages: [{ role: 'system', content: 'BASE PROMPT' }],
2008
+ deferredToolCatalog: [
2009
+ { name: 'shell', description: 'Run commands.' },
2010
+ { name: 'recall', description: 'Recall prior work.' },
2011
+ ],
1899
2012
  };
1900
- const nativeRecallQueryResult = JSON.parse(__renderToolSearchForTest({ query: 'memory previous' }, nativeRecallQuerySession, 'full'));
1901
- if (!nativeRecallQueryResult.discoveredTools.includes('recall') || nativeRecallQueryResult.discoveredTools.includes('memory')) {
1902
- throw new Error(`native tool_search memory previous should discover recall only: ${JSON.stringify(nativeRecallQueryResult)}`);
2013
+ applyInitialDeferredToolManifestToBp1(bp1ManifestSession, ['shell', 'recall']);
2014
+ const bp1ManifestText = bp1ManifestSession.messages[0].content;
2015
+ if (!/- shell: Run commands\./.test(bp1ManifestText) || !/- recall: Recall prior work\./.test(bp1ManifestText)) {
2016
+ throw new Error(`BP1 deferred manifest must carry catalog descriptions: ${bp1ManifestText}`);
1903
2017
  }
1904
- const ambiguousStatusSession = {
1905
- tools: smokeCatalog.filter((tool) => fullDefaults.has(tool?.name)),
1906
- deferredToolCatalog: smokeCatalog.slice(),
1907
- deferredSelectedTools: [...fullDefaults],
1908
- deferredDiscoveredTools: [],
1909
- deferredProviderMode: 'native',
1910
- deferredNativeTools: true,
1911
- };
1912
- const ambiguousStatusResult = JSON.parse(__renderToolSearchForTest({ query: 'status' }, ambiguousStatusSession, 'full'));
1913
- if (ambiguousStatusResult.selected || ambiguousStatusResult.discoveredTools.length) {
1914
- throw new Error(`tool_search ambiguous status query must not auto-load: ${JSON.stringify(ambiguousStatusResult)}`);
2018
+ if (bp1ManifestSession.deferredToolBp1Applied !== true) {
2019
+ throw new Error('BP1 deferred manifest injection must mark deferredToolBp1Applied');
1915
2020
  }
1916
2021
  const replyTool = CHANNEL_TOOL_DEFS.find((tool) => tool.name === 'reply');
1917
2022
  if (!/configured channel/i.test(replyTool?.description || '') || !/local .*paths/i.test(replyTool?.inputSchema?.properties?.files?.description || '')) {
package/src/help.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { stdout } from 'node:process';
2
2
 
3
- /** Help text shared by `--help` and the `/help` slash command. */
3
+ /** Help text printed by `--help`. */
4
4
  export const HELP_LINES = [
5
5
  'mixdog — standalone mixdog CLI/TUI coding agent.',
6
6
  '',
@@ -11,16 +11,13 @@ export const HELP_LINES = [
11
11
  ' mixdog --help',
12
12
  '',
13
13
  'Slash commands (inside mixdog):',
14
- ' /help show this help',
15
14
  ' /clear reset the conversation and clear the screen',
16
15
  ' /compact compact older conversation context',
17
16
  ' /model <name> switch model/preset for subsequent turns',
18
17
  ' /OutputStyle [name] show or switch Lead output style',
19
18
  ' /providers manage provider auth and local endpoints',
20
19
  ' /agents show available workflow agents',
21
- ' /agent manage active agents and async tasks',
22
- ' /mode <name> switch tool surface: full | readonly',
23
- ' /exit quit',
20
+ ' /quit quit (aliases: /exit, /q)',
24
21
  '',
25
22
  'History: use ↑ / ↓ to recall previous inputs.',
26
23
  ];
@@ -36,6 +36,10 @@ const CANONICAL_PLUGIN_LOG_NAMES = new Set([
36
36
  'session-start-critical.log',
37
37
  'webhook.log',
38
38
  'perf.log',
39
+ 'tool-events.log',
40
+ 'memory-runtime-proxy.log',
41
+ 'channels-worker-standalone.log',
42
+ 'mixdog-tui.stderr.log',
39
43
  ]);
40
44
 
41
45
  /**
@@ -113,6 +117,12 @@ function rotateBoundedLog(filePath, maxBytes, keepBytes) {
113
117
  } catch { /* missing file ok */ }
114
118
  }
115
119
 
120
+ // Shared bound for unbounded per-writer plugin logs (tool-events,
121
+ // memory-runtime-proxy, channels-worker-standalone). Keep a tail so recent
122
+ // context survives rotation while the file stays under the 10 MB cap.
123
+ const PLUGIN_LOG_MAX_BYTES = 10 * 1024 * 1024;
124
+ const PLUGIN_LOG_KEEP_BYTES = 2 * 1024 * 1024;
125
+
116
126
  /**
117
127
  * Ship-mode durable fail-open record (size-capped). No-op when line empty.
118
128
  */
@@ -131,4 +141,7 @@ module.exports = {
131
141
  pruneStalePluginDataLogSiblings,
132
142
  appendSessionStartCriticalLog,
133
143
  DEFAULT_STALE_LOG_SIBLING_MAX,
144
+ rotateBoundedLog,
145
+ PLUGIN_LOG_MAX_BYTES,
146
+ PLUGIN_LOG_KEEP_BYTES,
134
147
  };