mixdog 0.9.0 → 0.9.2

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 (240) hide show
  1. package/package.json +10 -3
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/background-task-meta-smoke.mjs +1 -1
  6. package/scripts/bench-run.mjs +262 -0
  7. package/scripts/compact-smoke.mjs +12 -0
  8. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  9. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  10. package/scripts/internal-comms-bench.mjs +727 -0
  11. package/scripts/internal-comms-smoke.mjs +75 -0
  12. package/scripts/lead-workflow-smoke.mjs +4 -4
  13. package/scripts/live-worker-smoke.mjs +9 -9
  14. package/scripts/output-style-bench.mjs +285 -0
  15. package/scripts/output-style-smoke.mjs +13 -10
  16. package/scripts/patch-replay.mjs +90 -0
  17. package/scripts/provider-stream-stall-test.mjs +276 -0
  18. package/scripts/provider-toolcall-test.mjs +599 -1
  19. package/scripts/routing-corpus.mjs +281 -0
  20. package/scripts/session-bench.mjs +1526 -0
  21. package/scripts/session-diag.mjs +595 -0
  22. package/scripts/session-ingest-smoke.mjs +2 -2
  23. package/scripts/task-bench.mjs +207 -0
  24. package/scripts/tool-failures.mjs +6 -6
  25. package/scripts/tool-smoke.mjs +306 -66
  26. package/scripts/toolcall-args-test.mjs +81 -0
  27. package/src/agents/debugger/AGENT.md +4 -4
  28. package/src/agents/heavy-worker/AGENT.md +4 -2
  29. package/src/agents/reviewer/AGENT.md +4 -4
  30. package/src/agents/worker/AGENT.md +4 -2
  31. package/src/app.mjs +10 -6
  32. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  33. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  34. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  35. package/src/headless-role.mjs +14 -14
  36. package/src/help.mjs +1 -0
  37. package/src/lib/mixdog-debug.cjs +0 -22
  38. package/src/lib/plugin-paths.cjs +1 -7
  39. package/src/lib/rules-builder.cjs +34 -56
  40. package/src/mixdog-session-runtime.mjs +710 -319
  41. package/src/output-styles/default.md +12 -7
  42. package/src/output-styles/minimal.md +25 -0
  43. package/src/output-styles/oneline.md +21 -0
  44. package/src/output-styles/simple.md +10 -9
  45. package/src/repl.mjs +12 -4
  46. package/src/rules/agent/00-common.md +7 -5
  47. package/src/rules/agent/30-explorer.md +7 -8
  48. package/src/rules/lead/01-general.md +3 -1
  49. package/src/rules/lead/lead-tool.md +7 -0
  50. package/src/rules/shared/01-tool.md +17 -12
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  52. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  53. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  54. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  55. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  56. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  57. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  58. package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
  59. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  60. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  61. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
  62. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  63. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  64. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  65. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +359 -106
  66. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
  67. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
  68. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  70. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
  75. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  76. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
  77. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  78. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
  79. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  80. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  81. package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
  82. package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
  83. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  84. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  85. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  86. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  87. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
  89. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
  91. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  92. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
  93. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  94. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  95. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
  99. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  100. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
  101. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  102. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  103. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
  104. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  105. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
  106. package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
  107. package/src/runtime/channels/backends/discord.mjs +99 -9
  108. package/src/runtime/channels/backends/telegram.mjs +501 -0
  109. package/src/runtime/channels/index.mjs +441 -1254
  110. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  111. package/src/runtime/channels/lib/config.mjs +54 -3
  112. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  113. package/src/runtime/channels/lib/executor.mjs +0 -3
  114. package/src/runtime/channels/lib/format.mjs +4 -2
  115. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  116. package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
  117. package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
  118. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  119. package/src/runtime/channels/lib/session-discovery.mjs +0 -4
  120. package/src/runtime/channels/lib/telegram-format.mjs +283 -0
  121. package/src/runtime/channels/lib/tool-format.mjs +1 -2
  122. package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
  123. package/src/runtime/channels/lib/webhook.mjs +59 -31
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  126. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  127. package/src/runtime/memory/index.mjs +187 -43
  128. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  129. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  130. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  131. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  132. package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
  133. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  134. package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
  135. package/src/runtime/memory/lib/memory.mjs +101 -4
  136. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  137. package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
  138. package/src/runtime/memory/lib/session-ingest.mjs +116 -7
  139. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  140. package/src/runtime/memory/tool-defs.mjs +6 -3
  141. package/src/runtime/search/index.mjs +2 -7
  142. package/src/runtime/search/lib/config.mjs +0 -4
  143. package/src/runtime/search/lib/state.mjs +1 -15
  144. package/src/runtime/search/lib/web-tools.mjs +0 -1
  145. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  146. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  147. package/src/runtime/shared/child-spawn-gate.mjs +0 -6
  148. package/src/runtime/shared/config.mjs +9 -0
  149. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  150. package/src/runtime/shared/schedules-store.mjs +21 -19
  151. package/src/runtime/shared/tool-surface.mjs +98 -13
  152. package/src/runtime/shared/transcript-writer.mjs +129 -0
  153. package/src/runtime/shared/update-checker.mjs +214 -0
  154. package/src/standalone/agent-tool.mjs +255 -109
  155. package/src/standalone/channel-admin.mjs +133 -40
  156. package/src/standalone/channel-worker.mjs +8 -291
  157. package/src/standalone/explore-tool.mjs +2 -2
  158. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  159. package/src/standalone/provider-admin.mjs +11 -0
  160. package/src/standalone/seeds.mjs +1 -11
  161. package/src/standalone/usage-dashboard.mjs +1 -1
  162. package/src/tui/App.jsx +2137 -750
  163. package/src/tui/components/ConfirmBar.jsx +47 -0
  164. package/src/tui/components/ContextPanel.jsx +5 -3
  165. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  166. package/src/tui/components/Markdown.jsx +22 -98
  167. package/src/tui/components/Message.jsx +14 -35
  168. package/src/tui/components/Picker.jsx +87 -12
  169. package/src/tui/components/PromptInput.jsx +146 -9
  170. package/src/tui/components/QueuedCommands.jsx +1 -1
  171. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  172. package/src/tui/components/Spinner.jsx +7 -7
  173. package/src/tui/components/StatusLine.jsx +40 -21
  174. package/src/tui/components/TextEntryPanel.jsx +51 -7
  175. package/src/tui/components/ToolExecution.jsx +177 -100
  176. package/src/tui/components/TurnDone.jsx +4 -4
  177. package/src/tui/components/UsagePanel.jsx +1 -1
  178. package/src/tui/components/tool-output-format.mjs +312 -40
  179. package/src/tui/components/tool-output-format.test.mjs +180 -1
  180. package/src/tui/display-width.mjs +69 -0
  181. package/src/tui/display-width.test.mjs +35 -0
  182. package/src/tui/dist/index.mjs +7324 -2393
  183. package/src/tui/engine.mjs +287 -126
  184. package/src/tui/index.jsx +117 -7
  185. package/src/tui/keyboard-protocol.mjs +42 -0
  186. package/src/tui/lib/voice-recorder.mjs +453 -0
  187. package/src/tui/markdown/format-token.mjs +354 -142
  188. package/src/tui/markdown/format-token.test.mjs +155 -17
  189. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  190. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  191. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  192. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  193. package/src/tui/markdown/table-layout.mjs +9 -9
  194. package/src/tui/paste-attachments.mjs +0 -11
  195. package/src/tui/prompt-history-store.mjs +129 -0
  196. package/src/tui/prompt-history-store.test.mjs +52 -0
  197. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  198. package/src/tui/theme.mjs +41 -647
  199. package/src/tui/themes/base.mjs +86 -0
  200. package/src/tui/themes/basic.mjs +85 -0
  201. package/src/tui/themes/catppuccin.mjs +72 -0
  202. package/src/tui/themes/dracula.mjs +70 -0
  203. package/src/tui/themes/everforest.mjs +71 -0
  204. package/src/tui/themes/gruvbox.mjs +71 -0
  205. package/src/tui/themes/index.mjs +71 -0
  206. package/src/tui/themes/indigo.mjs +78 -0
  207. package/src/tui/themes/kanagawa.mjs +80 -0
  208. package/src/tui/themes/light.mjs +81 -0
  209. package/src/tui/themes/nord.mjs +72 -0
  210. package/src/tui/themes/onedark.mjs +16 -0
  211. package/src/tui/themes/rosepine.mjs +70 -0
  212. package/src/tui/themes/teal.mjs +81 -0
  213. package/src/tui/themes/tokyonight.mjs +79 -0
  214. package/src/tui/themes/utils.mjs +106 -0
  215. package/src/tui/themes/warm.mjs +79 -0
  216. package/src/tui/transcript-tool-failures.mjs +13 -2
  217. package/src/ui/markdown.mjs +1 -1
  218. package/src/ui/model-display.mjs +2 -2
  219. package/src/ui/statusline.mjs +26 -27
  220. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
  221. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  222. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  223. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  224. package/src/workflows/default/WORKFLOW.md +39 -12
  225. package/src/workflows/sequential/WORKFLOW.md +46 -0
  226. package/src/workflows/solo/WORKFLOW.md +7 -0
  227. package/vendor/ink/build/display-width.js +62 -0
  228. package/vendor/ink/build/ink.js +154 -20
  229. package/vendor/ink/build/measure-text.js +4 -1
  230. package/vendor/ink/build/output.js +115 -9
  231. package/vendor/ink/build/render-node-to-output.js +4 -1
  232. package/vendor/ink/build/render.js +4 -0
  233. package/src/hooks/lib/permission-rules.cjs +0 -170
  234. package/src/hooks/lib/settings-loader.cjs +0 -112
  235. package/src/lib/hook-pipe-path.cjs +0 -10
  236. package/src/output-styles/extreme-simple.md +0 -20
  237. package/src/rules/lead/04-workflow.md +0 -51
  238. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  239. package/src/workflows/default/workflow.json +0 -13
  240. package/src/workflows/solo/workflow.json +0 -7
@@ -52,7 +52,9 @@ export const TOOL_DEFS = [
52
52
  includeArchived: { type: 'boolean', description: 'Include archived entries.' },
53
53
  sessionId: { type: 'string', description: 'Scoped session id.' },
54
54
  session_id: { type: 'string', description: 'Alias for sessionId.' },
55
- includeMembers: { type: 'boolean', description: 'Include chunk members.' },
55
+ includeMembers: { type: 'boolean', description: 'Include chunk members in output; does not widen the search pool.' },
56
+ includeRaw: { type: 'boolean', description: 'Include unchunked raw/episode rows.' },
57
+ sessionOnly: { type: 'boolean', description: 'Search this session only.' },
56
58
  projectScope: { type: 'string', description: 'Project pool selector: inferred from cwd, common, all, or slug.' },
57
59
  cwd: { type: 'string', description: 'Infer projectScope.' },
58
60
  },
@@ -74,8 +76,9 @@ export const TOOL_DEFS = [
74
76
  category: { anyOf: [{ type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'] }, { type: 'array', items: { type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'] }, minItems: 1 }], description: 'Category filter.' },
75
77
  limit: { type: 'number', default: 30, description: 'Max entries.' },
76
78
  offset: { type: 'number', default: 0, description: 'Skip entries.' },
77
- includeMembers: { type: 'boolean', description: 'Include members.' },
78
- includeRaw: { type: 'boolean', description: 'Include raw rows.' },
79
+ includeMembers: { type: 'boolean', description: 'Include chunk members in output; does not widen the search pool.' },
80
+ includeRaw: { type: 'boolean', description: 'Include unchunked raw/episode rows.' },
81
+ sessionOnly: { type: 'boolean', description: 'Search this session only.' },
79
82
  includeArchived: { type: 'boolean', description: 'Include archived.' },
80
83
  sessionId: { type: 'string', description: 'Scoped session id.' },
81
84
  session_id: { type: 'string', description: 'Alias for sessionId.' },
@@ -4,7 +4,6 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
5
5
  import { z } from 'zod'
6
6
  import fs from 'fs'
7
- import path from 'path'
8
7
  import {
9
8
  ensureDataDir,
10
9
  getRequestTimeoutMs,
@@ -32,7 +31,7 @@ import {
32
31
  loadUsageState,
33
32
  updateProviderState,
34
33
  } from './lib/state.mjs'
35
- import { assertPublicUrl, crawlSite, getScrapeCapabilities, pinnedFetch, scrapeUrls } from './lib/web-tools.mjs'
34
+ import { getScrapeCapabilities, scrapeUrls } from './lib/web-tools.mjs'
36
35
  import { formatResponse } from './lib/formatter.mjs'
37
36
  ensureDataDir()
38
37
 
@@ -105,10 +104,6 @@ function formattedText(tool, payload) {
105
104
  }
106
105
  }
107
106
 
108
- function okText(text) {
109
- return { content: [{ type: 'text', text }], isError: false }
110
- }
111
-
112
107
  function isInvalidSearchResult(result) {
113
108
  const title = String(result?.title || '').trim()
114
109
  return /\bpage not found\b|\b404\b.*\bnot found\b/i.test(title)
@@ -529,7 +524,7 @@ async function handleToolCall(name, rawArgs, options = {}) {
529
524
  throw e
530
525
  }
531
526
  try {
532
- const result = await _fetchCore(urlArgs, { config, usageState, cacheState, timeoutMs, signal })
527
+ const result = await _fetchCore(urlArgs, { usageState, cacheState, timeoutMs, signal })
533
528
  flushCacheState()
534
529
  flushUsageState()
535
530
  return {
@@ -44,10 +44,6 @@ export function writeJson(filePath, value) {
44
44
  writeJsonAtomicSync(filePath, value, { lock: true, fsyncDir: true })
45
45
  }
46
46
 
47
- export function saveConfig(config) {
48
- return { ...DEFAULT_CONFIG, ...(config || {}) }
49
- }
50
-
51
47
  export function loadConfig() {
52
48
  ensureDataDir()
53
49
  if (Object.keys(readSection('search') || {}).length > 0) {
@@ -88,10 +88,6 @@ export function loadUsageState() {
88
88
  return state
89
89
  }
90
90
 
91
- export function saveUsageState(state) {
92
- scheduleUsageFlush(state)
93
- }
94
-
95
91
  export function updateProviderState(state, provider, patch) {
96
92
  let normalizedPatch = { ...patch }
97
93
  const remaining =
@@ -125,7 +121,7 @@ export function noteProviderSuccess(state, provider, extra = {}) {
125
121
  })
126
122
  }
127
123
 
128
- export const PROVIDER_ERROR_KIND = {
124
+ const PROVIDER_ERROR_KIND = {
129
125
  AUTH: 'auth',
130
126
  QUOTA: 'quota',
131
127
  PAYMENT: 'payment',
@@ -151,16 +147,6 @@ export function classifyProviderError(error) {
151
147
  return PROVIDER_ERROR_KIND.UNKNOWN
152
148
  }
153
149
 
154
- /** Structured HTTP error for search backends (enables cooldown via classifyProviderError). */
155
- export function providerHttpError(provider, status, detail = '') {
156
- const code = Number(status)
157
- const snippet = detail ? `: ${String(detail).slice(0, 200)}` : ''
158
- const err = new Error(`[search:${provider}] HTTP ${code}${snippet}`)
159
- err.status = code
160
- err.provider = provider
161
- return err
162
- }
163
-
164
150
  const PROVIDER_DISABLE_TTL_MS = {
165
151
  auth: 24 * 3600 * 1000,
166
152
  quota: 24 * 3600 * 1000,
@@ -24,7 +24,6 @@ async function loadPuppeteer() {
24
24
  return _puppeteer
25
25
  }
26
26
 
27
-
28
27
  const PKG_VERSION = (() => { try { return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version } catch { return '0.0.1' } })()
29
28
  import {
30
29
  noteProviderFailure,
@@ -0,0 +1,12 @@
1
+ export function channelNotificationModelContent(params = {}) {
2
+ const meta = params?.meta && typeof params.meta === 'object' ? params.meta : {};
3
+ if (meta.silent_to_agent === true || meta.silent_to_agent === 'true') return '';
4
+ const instruction = typeof meta.instruction === 'string' ? meta.instruction.trim() : '';
5
+ const content = String(params?.content || '').trim();
6
+ return instruction || content;
7
+ }
8
+
9
+ export function shouldMirrorChannelNotificationToPending(meta = {}) {
10
+ const type = String(meta?.type || '').trim().toLowerCase();
11
+ return type === 'schedule';
12
+ }
@@ -0,0 +1,45 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ channelNotificationModelContent,
5
+ shouldMirrorChannelNotificationToPending,
6
+ } from './channel-notification-routing.mjs';
7
+
8
+ test('channel schedule notification uses instruction as model content', () => {
9
+ const content = channelNotificationModelContent({
10
+ content: ' ',
11
+ meta: {
12
+ type: 'schedule',
13
+ instruction: 'scheduled prompt body',
14
+ },
15
+ });
16
+ assert.equal(content, 'scheduled prompt body');
17
+ });
18
+
19
+ test('silent channel notification has no model content', () => {
20
+ const content = channelNotificationModelContent({
21
+ content: 'hidden',
22
+ meta: {
23
+ type: 'schedule',
24
+ silent_to_agent: true,
25
+ },
26
+ });
27
+ assert.equal(content, '');
28
+ });
29
+
30
+ test('non-schedule notification preserves instruction-first routing', () => {
31
+ const content = channelNotificationModelContent({
32
+ content: 'worker result body',
33
+ meta: {
34
+ type: 'webhook',
35
+ instruction: 'relay instruction',
36
+ },
37
+ });
38
+ assert.equal(content, 'relay instruction');
39
+ });
40
+
41
+ test('only schedule channel notifications are mirrored to pending fallback', () => {
42
+ assert.equal(shouldMirrorChannelNotificationToPending({ type: 'schedule' }), true);
43
+ assert.equal(shouldMirrorChannelNotificationToPending({ type: 'webhook' }), false);
44
+ assert.equal(shouldMirrorChannelNotificationToPending({}), false);
45
+ });
@@ -137,9 +137,3 @@ export async function withGate(fn, signal = null) {
137
137
  release();
138
138
  }
139
139
  }
140
-
141
- export function gateStats() {
142
- return { inflight: _inflight, queued: _queue.length, maxInflight: MAX_INFLIGHT };
143
- }
144
-
145
- export const CHILD_SPAWN_MAX_INFLIGHT = MAX_INFLIGHT;
@@ -246,6 +246,7 @@ export function getCapabilities() {
246
246
  // the migration logic in seed.mjs.
247
247
  export const SECRET_ACCOUNTS = Object.freeze({
248
248
  discordToken: 'discord.token',
249
+ telegramToken: 'telegram.token',
249
250
  webhookAuth: 'webhook.authtoken',
250
251
  agentApiKey: (provider) => `agent.${provider}.apiKey`,
251
252
  openaiUsageSessionKey: 'agent.openai.usageSessionKey',
@@ -293,6 +294,14 @@ export function getDiscordToken() {
293
294
  return _readSecret(SECRET_ACCOUNTS.discordToken)
294
295
  }
295
296
 
297
+ /**
298
+ * Returns the Telegram bot token.
299
+ * Priority: MIXDOG_TELEGRAM_TOKEN → keychain('telegram.token') → null
300
+ */
301
+ export function getTelegramToken() {
302
+ return _readSecret(SECRET_ACCOUNTS.telegramToken)
303
+ }
304
+
296
305
  /**
297
306
  * Returns the ngrok/webhook authtoken.
298
307
  * Priority: MIXDOG_WEBHOOK_AUTHTOKEN → keychain('webhook.authtoken') → null
@@ -19,7 +19,14 @@
19
19
  * so a shared long-lived pool is appropriate here.
20
20
  */
21
21
 
22
- import { Agent, getGlobalDispatcher, setGlobalDispatcher, request as undiciRequest } from 'undici'
22
+ import { createRequire } from 'node:module'
23
+
24
+ const require = createRequire(import.meta.url)
25
+ let _undici = null
26
+ function undici() {
27
+ if (!_undici) _undici = require('undici')
28
+ return _undici
29
+ }
23
30
 
24
31
  let _agent = null
25
32
  let _globalInstalled = false
@@ -53,7 +60,7 @@ function proxyConfigured() {
53
60
  if (env.HTTP_PROXY || env.HTTPS_PROXY || env.http_proxy || env.https_proxy) return true
54
61
  if (env.NODE_USE_ENV_PROXY) return true
55
62
  try {
56
- const g = getGlobalDispatcher?.()
63
+ const g = undici().getGlobalDispatcher?.()
57
64
  // Any non-default global dispatcher (constructor name other than the plain
58
65
  // `Agent` undici installs by default) is treated as custom — ProxyAgent,
59
66
  // EnvHttpProxyAgent, MockAgent, or a user subclass — and we step aside.
@@ -77,7 +84,7 @@ function proxyConfigured() {
77
84
  export function getLlmDispatcher() {
78
85
  if (proxyConfigured()) return undefined
79
86
  if (!_agent) {
80
- _agent = new Agent({
87
+ _agent = new (undici().Agent)({
81
88
  keepAliveTimeout: envInt('MIXDOG_LLM_KEEPALIVE_MS', 60_000),
82
89
  keepAliveMaxTimeout: envInt('MIXDOG_LLM_KEEPALIVE_MAX_MS', 90_000),
83
90
  connections: envInt('MIXDOG_LLM_CONNECTIONS', 16),
@@ -87,7 +94,7 @@ export function getLlmDispatcher() {
87
94
  // a per-request dispatcher throws UND_ERR_INVALID_ARG. Install globally once
88
95
  // and omit the per-request dispatcher. See port-plan D7.
89
96
  if (!_globalInstalled) {
90
- try { setGlobalDispatcher(_agent); _globalInstalled = true } catch { /* fall back */ }
97
+ try { undici().setGlobalDispatcher(_agent); _globalInstalled = true } catch { /* fall back */ }
91
98
  }
92
99
  return _globalInstalled ? undefined : _agent
93
100
  }
@@ -130,7 +137,7 @@ export function preconnect(origin) {
130
137
  // A throwaway HEAD lands a pooled socket without fetching a body. Any
131
138
  // failure (offline, DNS, 4xx/5xx) is irrelevant — the handshake is the
132
139
  // point, and the real request will surface genuine errors.
133
- undiciRequest(key, {
140
+ undici().request(key, {
134
141
  method: 'HEAD',
135
142
  dispatcher: getLlmDispatcher(),
136
143
  signal: AbortSignal.timeout(10_000),
@@ -2,32 +2,29 @@
2
2
  * Canonical reader for registered schedules.
3
3
  *
4
4
  * `<Mixdog data dir>/schedules/<name>/` is the single source of truth.
5
- * Each schedule directory contains `config.json` (metadata) and
6
- * `instructions.md` (prompt body). Both the setup UI (POST /schedules)
7
- * and the `schedule-add` skill write the same two files; every reader
5
+ * Each schedule directory contains a single `SCHEDULE.md` YAML-ish
6
+ * frontmatter (metadata: time/timezone/days/channel/model/enabled) plus a
7
+ * markdown body (the prompt). Both the setup UI (POST /schedules) and the
8
+ * `schedule-add` skill write the same one file; every reader —
8
9
  * setup-server (GET /schedules), channels/lib/config.mjs (loadConfig),
9
10
  * status/aggregator.mjs — must go through listSchedules() so a single
10
11
  * entry shape is presented everywhere.
11
12
  *
12
13
  * The legacy `mixdog-config.json` `channels.schedules.items` /
13
14
  * `channels.nonInteractive` / `channels.interactive` arrays are no longer
14
- * consulted. Migration: rename any legacy `prompt.md` to `instructions.md`
15
- * — no in-code fallback.
15
+ * consulted. Clean cut: the old `config.json` + `instructions.md` pair is
16
+ * no longer read — no in-code fallback.
16
17
  */
17
18
 
18
19
  import { readFileSync, readdirSync } from 'fs';
19
20
  import { join } from 'path';
20
21
  import { resolvePluginData } from './plugin-paths.mjs';
22
+ import { readMarkdownDocument } from './markdown-frontmatter.mjs';
21
23
 
22
24
  function schedulesDir() {
23
25
  return join(resolvePluginData(), 'schedules');
24
26
  }
25
27
 
26
- function readJsonFile(path) {
27
- try { return JSON.parse(readFileSync(path, 'utf8')); }
28
- catch { return null; }
29
- }
30
-
31
28
  function readTextFile(path) {
32
29
  try { return readFileSync(path, 'utf8'); }
33
30
  catch { return ''; }
@@ -36,13 +33,13 @@ function readTextFile(path) {
36
33
  /**
37
34
  * List every registered schedule.
38
35
  *
39
- * Return shape per entry: `{ name, ...config, prompt }`.
36
+ * Return shape per entry: `{ name, ...frontmatter, prompt }`.
40
37
  * - `name` is the directory name (slug).
41
- * - Spread of `config.json` keys (time/days/type/channel/model/enabled
42
- * when written by the setup UI; cron/timezone/mode/role when written
43
- * by the schedule-add skill).
44
- * - `prompt` carries `instructions.md` content (empty string when the
45
- * file is missing — caller decides whether that is a hard error).
38
+ * - Spread of `SCHEDULE.md` frontmatter keys (time/timezone/days/channel/
39
+ * model/enabled). All frontmatter values arrive as strings; `enabled` is
40
+ * cast to a boolean here so downstream `s.enabled !== false` filters work.
41
+ * - `prompt` carries the `SCHEDULE.md` body (empty string when the file is
42
+ * missing — caller decides whether that is a hard error).
46
43
  *
47
44
  * Returns an empty array when the directory does not exist (fresh
48
45
  * install with no schedules registered yet).
@@ -62,9 +59,14 @@ export function listSchedules() {
62
59
  for (const ent of entries) {
63
60
  if (!ent.isDirectory()) continue;
64
61
  const name = ent.name;
65
- const cfg = readJsonFile(join(dir, name, 'config.json')) || {};
66
- const prompt = readTextFile(join(dir, name, 'instructions.md'));
67
- out.push({ name, ...cfg, prompt });
62
+ const { frontmatter, body } = readMarkdownDocument(readTextFile(join(dir, name, 'SCHEDULE.md')));
63
+ const cfg = { ...frontmatter };
64
+ // Frontmatter is all-strings; cast enabled so `s.enabled !== false`
65
+ // filters (config.mjs, scheduler.mjs) treat `enabled: false` correctly.
66
+ if (Object.prototype.hasOwnProperty.call(cfg, 'enabled')) {
67
+ cfg.enabled = cfg.enabled !== 'false' && cfg.enabled !== false;
68
+ }
69
+ out.push({ name, ...cfg, prompt: body });
68
70
  }
69
71
  return out;
70
72
  }
@@ -1,8 +1,15 @@
1
1
  import { displayModelName as sharedDisplayModelName } from '../../ui/model-display.mjs';
2
2
 
3
- const DEFAULT_SUMMARY_MAX = 160;
4
- // Semantic cap for collapsed agent/task card one-liners (spawn, send, response).
5
- export const AGENT_SURFACE_BRIEF_MAX = 120;
3
+ // Default cap for tool-arg summaries (header parenthetical, channel tool lines).
4
+ // Unified at 80 so every tool surface line header arg summary and collapsed
5
+ // detail alike shares one width ceiling; per-call sites still clamp lower
6
+ // (header 48, channel 50) where a tighter line is wanted.
7
+ const DEFAULT_SUMMARY_MAX = 80;
8
+ // Semantic cap for collapsed tool/agent/task card one-liners — the second row
9
+ // under the ⎿ gutter (spawn, send, response, generic/shell result summaries).
10
+ // Kept at 80 so every collapsed detail line is truncated to the same width
11
+ // regardless of terminal columns; ctrl+o expand still shows the full body.
12
+ export const AGENT_SURFACE_BRIEF_MAX = 80;
6
13
  const STATUS_SEPARATOR = ' · ';
7
14
 
8
15
  export function stripToolPrefix(name) {
@@ -205,7 +212,7 @@ function bridgeAgentModelSummary(args) {
205
212
  const modelId = firstText(args.model);
206
213
  const displayHint = firstText(args.modelDisplay, args.model_display, args.displayModel);
207
214
  return compactParts([
208
- displayAgentName(firstText(args.agent, args.role, args.name, args.subagent_type)),
215
+ displayAgentName(firstText(args.agent, args.name, args.subagent_type)),
209
216
  displayModelName(modelId, provider, displayHint),
210
217
  ]);
211
218
  }
@@ -584,6 +591,22 @@ function countNonEmptyLines(text) {
584
591
  .filter((line) => line.trim()).length;
585
592
  }
586
593
 
594
+ // Zero-result recognizer (audit HIGH): result text that SAYS "nothing found"
595
+ // must summarize as an explicit zero, not be line-counted into "1 match".
596
+ // Matches the shapes emitted by grep/glob/find/list/recall backends:
597
+ // "(no matches)", "no matches found", "(no results)", "No results",
598
+ // "(no fuzzy match for \"...\")", "0 matches", "(empty)", "(no entries)".
599
+ function looksLikeZeroResultText(text) {
600
+ const trimmed = String(text ?? '').trim();
601
+ if (!trimmed) return false;
602
+ // Only trust short, single-line-ish payloads — a real listing that merely
603
+ // CONTAINS the words "no matches" somewhere must not be zeroed.
604
+ if (trimmed.length > 200 || trimmed.includes('\n')) return false;
605
+ return /^\(?\s*(?:no|0)\s+(?:fuzzy\s+)?(?:match(?:es)?|results?|files?|entries|candidates?|hits?)\b/i.test(trimmed)
606
+ || /^\(?\s*(?:empty|none)\s*\)?$/i.test(trimmed)
607
+ || /^no\s+\S+\s+(?:found|matched)\b/i.test(trimmed);
608
+ }
609
+
587
610
  function splitPathAndDelta(value, explicitDelta = '') {
588
611
  let path = String(value ?? '').trim();
589
612
  let delta = String(explicitDelta ?? '').trim();
@@ -734,8 +757,8 @@ function summarizeGenericResult(text) {
734
757
  if (Array.isArray(parsed)) return `${parsed.length} ${pluralize(parsed.length, 'item')}`;
735
758
  if (parsed && typeof parsed === 'object') {
736
759
  const status = firstText(parsed.status, parsed.state, parsed.result, parsed.message);
737
- if (status) return truncateSingleLine(titleStatus(status), 120);
738
- if (parsed.cwd) return truncateSingleLine(parsed.cwd, 120);
760
+ if (status) return truncateSingleLine(titleStatus(status), AGENT_SURFACE_BRIEF_MAX);
761
+ if (parsed.cwd) return truncateSingleLine(parsed.cwd, AGENT_SURFACE_BRIEF_MAX);
739
762
  if (typeof parsed.ok === 'boolean') return parsed.ok ? 'Ok' : 'Failed';
740
763
  for (const key of ['items', 'results', 'resources', 'templates', 'providers', 'schedules', 'channels', 'tools']) {
741
764
  if (Array.isArray(parsed[key])) return `${parsed[key].length} ${pluralize(parsed[key].length, (key.slice(0, -1) || 'item').toLowerCase())}`;
@@ -751,7 +774,36 @@ function summarizeGenericResult(text) {
751
774
  if (/^(ok|done|success|saved|sent|updated|reloaded|connected|enabled|disabled|active|inactive)$/i.test(line)) {
752
775
  return titleStatus(line);
753
776
  }
754
- return truncateSingleLine(line, 120);
777
+ return truncateSingleLine(line, AGENT_SURFACE_BRIEF_MAX);
778
+ }
779
+
780
+ // Error-cause extractor (audit HIGH): when a tool result is an error, surface
781
+ // the actual cause line instead of a bare "Failed"/raw first line. Handles
782
+ // JSON error envelopes ({error|message|cause|detail}) and plain-text bodies
783
+ // (first line that carries error-ish signal, else first non-empty line).
784
+ // Exported for ToolExecution's collapsed detail row.
785
+ export function extractErrorCause(resultText) {
786
+ const text = String(resultText ?? '').trim();
787
+ if (!text) return '';
788
+ // JSON envelope: prefer explicit error-ish keys.
789
+ if (text.startsWith('{') || text.startsWith('[')) {
790
+ try {
791
+ const parsed = JSON.parse(text);
792
+ const obj = Array.isArray(parsed) ? parsed[0] : parsed;
793
+ if (obj && typeof obj === 'object') {
794
+ const cause = firstText(
795
+ typeof obj.error === 'string' ? obj.error : obj.error?.message,
796
+ obj.message, obj.cause, obj.detail, obj.reason, obj.status,
797
+ );
798
+ if (cause) return truncateSingleLine(String(cause), AGENT_SURFACE_BRIEF_MAX);
799
+ }
800
+ } catch { /* fall through to text scan */ }
801
+ }
802
+ const lines = text.split('\n').map((l) => l.trim()).filter(Boolean);
803
+ // Prefer the first line that looks like an error statement.
804
+ const errorish = lines.find((l) => /\b(error|failed|failure|denied|refused|timed?\s*out|timeout|not\s+found|missing|invalid|cannot|can't|exception|exit\s+(?:code\s+)?[1-9])\b/i.test(l));
805
+ const picked = errorish || lines[0] || '';
806
+ return truncateSingleLine(stripInlineMarkdown(picked), AGENT_SURFACE_BRIEF_MAX);
755
807
  }
756
808
 
757
809
  /**
@@ -760,7 +812,13 @@ function summarizeGenericResult(text) {
760
812
  * reliable can be derived, so the caller falls back to the raw result block.
761
813
  */
762
814
  export function summarizeToolResult(name, args, resultText, isError = false) {
763
- if (isError) return null;
815
+ if (isError) {
816
+ // Audit HIGH: errors used to disable semantic summaries entirely, leaving
817
+ // the UI with a raw first line or a bare "Failed". Surface the extracted
818
+ // cause so the collapsed card answers "why" without ctrl+o.
819
+ const cause = extractErrorCause(resultText);
820
+ return cause || null;
821
+ }
764
822
  const text = String(resultText ?? '');
765
823
  const trimmed = text.trim();
766
824
  if (/^(?:undefined|null)$/i.test(trimmed)) return null;
@@ -800,18 +858,21 @@ export function summarizeToolResult(name, args, resultText, isError = false) {
800
858
  }
801
859
  case 'grep': {
802
860
  if (!trimmed || !looksLineOriented(text)) return null;
861
+ if (looksLikeZeroResultText(text)) return '0 matches';
803
862
  const n = countNonEmptyLines(text);
804
863
  if (n === 0) return null;
805
864
  return `${n} ${pluralize(n, 'match', 'matches')}`;
806
865
  }
807
866
  case 'glob': {
808
867
  if (!trimmed || !looksLineOriented(text)) return null;
868
+ if (looksLikeZeroResultText(text)) return '0 files';
809
869
  const n = countNonEmptyLines(text);
810
870
  if (n === 0) return null;
811
871
  return `${n} ${pluralize(n, 'file')}`;
812
872
  }
813
873
  case 'find': {
814
874
  if (!trimmed || !looksLineOriented(text)) return null;
875
+ if (looksLikeZeroResultText(text)) return '0 candidates';
815
876
  const n = countNonEmptyLines(text);
816
877
  if (n === 0) return null;
817
878
  return `${n} ${pluralize(n, 'candidate')}`;
@@ -819,6 +880,7 @@ export function summarizeToolResult(name, args, resultText, isError = false) {
819
880
  case 'list':
820
881
  case 'ls': {
821
882
  if (!trimmed || !looksLineOriented(text)) return null;
883
+ if (looksLikeZeroResultText(text)) return '0 entries';
822
884
  const n = countNonEmptyLines(text);
823
885
  if (n === 0) return null;
824
886
  return `${n} ${pluralize(n, 'entry', 'entries')}`;
@@ -840,15 +902,30 @@ export function summarizeToolResult(name, args, resultText, isError = false) {
840
902
  ]);
841
903
  }
842
904
  const firstLine = trimmed.split('\n').map((line) => line.trim()).find(Boolean) || trimmed;
843
- return truncateSingleLine(firstLine, 120);
905
+ return truncateSingleLine(firstLine, AGENT_SURFACE_BRIEF_MAX);
844
906
  }
845
907
  case 'code_graph': {
846
908
  const match = /(\d+)\s+(references|definitions|symbols|callers|callees|results|matches)/i.exec(text);
847
909
  if (match) return `${match[1]} ${String(match[2]).toLowerCase()}`;
910
+ if (looksLikeZeroResultText(text)) return 'No results';
848
911
  return null;
849
912
  }
850
913
  case 'web_fetch':
851
914
  case 'fetch': {
915
+ // Audit HIGH: channel `fetch` (Discord message fetch — args carry
916
+ // channel/messageId/limit, never url/uri) was summarized as a WEB fetch,
917
+ // so its result missed both the status/size probes and fell to raw JSON.
918
+ // Route it to the generic JSON/text summarizer instead.
919
+ if (normalized === 'fetch') {
920
+ const a = parseToolArgs(args);
921
+ const isChannelFetch = !firstText(a.url, a.uri)
922
+ && Boolean(firstText(a.channel, a.channelId, a.chatId, a.messageId) || a.limit != null);
923
+ if (isChannelFetch) {
924
+ const n = countNonEmptyLines(text);
925
+ if (trimmed && looksLineOriented(text) && n > 0) return `${n} ${pluralize(n, 'message')}`;
926
+ return summarizeGenericResult(text);
927
+ }
928
+ }
852
929
  // Status: require a status-like context (HTTP NNN, "Status: NNN",
853
930
  // or "NNN OK"/"NNN Not Found") rather than any bare 3-digit number.
854
931
  const status = /(?:HTTP[\s/]*\d?\.?\d?\s*|status[:\s]+)([1-5]\d{2})\b/i.exec(text)
@@ -885,7 +962,15 @@ export function summarizeToolResult(name, args, resultText, isError = false) {
885
962
  }
886
963
  case 'recall':
887
964
  case 'search_memories':
888
- case 'memory':
965
+ case 'memory': {
966
+ if (!trimmed || trimmed === '(no results)' || looksLikeZeroResultText(text)) return 'No Results';
967
+ let n = 0;
968
+ for (const line of text.split('\n')) {
969
+ if (/#\d+\s*$/.test(line)) n += 1;
970
+ }
971
+ if (n > 0) return `${n} ${pluralize(n, 'Memory', 'Memories')}`;
972
+ return summarizeGenericResult(text);
973
+ }
889
974
  case 'remember':
890
975
  case 'save_memory':
891
976
  case 'update_memory':
@@ -948,18 +1033,18 @@ export function summarizeToolResult(name, args, resultText, isError = false) {
948
1033
  if (answerLine) return truncateSingleLine(stripInlineMarkdown(answerLine), AGENT_SURFACE_BRIEF_MAX);
949
1034
  const task = /^agent task:\s*(\S+)/mi.exec(text);
950
1035
  const status = /^status:\s*([^\s(]+)/mi.exec(text);
951
- const role = /^role:\s*(.+)$/mi.exec(text);
1036
+ const agent = /^agent:\s*(.+)$/mi.exec(text);
952
1037
  const preset = /^preset:\s*(.+)$/mi.exec(text);
953
1038
  const model = /^model:\s*(.+)$/mi.exec(text);
954
1039
  const limits = /^limits:\s*(.+)$/mi.exec(text);
955
1040
  const agentModel = compactParts([
956
- displayAgentName(role ? role[1] : ''),
1041
+ displayAgentName(agent ? agent[1] : ''),
957
1042
  displayModelName(model ? model[1] : ''),
958
1043
  ]);
959
1044
  if (agentModel) return agentModel;
960
1045
  const parts = [
961
1046
  task ? task[1] : '',
962
- role ? role[1] : '',
1047
+ agent ? agent[1] : '',
963
1048
  preset ? preset[1] : '',
964
1049
  model ? model[1] : '',
965
1050
  status ? titleStatus(status[1]) : '',