mixdog 0.7.1

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 (404) hide show
  1. package/.claude-plugin/marketplace.json +31 -0
  2. package/.claude-plugin/plugin.json +20 -0
  3. package/.gitattributes +34 -0
  4. package/.mcp.json +14 -0
  5. package/ARCHITECTURE.md +77 -0
  6. package/CHANGELOG.md +7 -0
  7. package/CONTRIBUTING.md +45 -0
  8. package/DATA-FLOW.md +79 -0
  9. package/LICENSE +21 -0
  10. package/README.md +389 -0
  11. package/SECURITY.md +138 -0
  12. package/UNINSTALL.md +112 -0
  13. package/agents/maintenance.md +5 -0
  14. package/agents/memory-classification.md +30 -0
  15. package/agents/scheduler-task.md +18 -0
  16. package/agents/webhook-handler.md +27 -0
  17. package/agents/worker.md +24 -0
  18. package/bin/bridge +133 -0
  19. package/bin/statusline-launcher.mjs +78 -0
  20. package/bin/statusline-lib.mjs +550 -0
  21. package/bin/statusline.mjs +607 -0
  22. package/bun.lock +802 -0
  23. package/commands/config.md +16 -0
  24. package/commands/doctor.md +13 -0
  25. package/commands/setup.md +17 -0
  26. package/defaults/cycle3-review-prompt.md +90 -0
  27. package/defaults/hidden-roles.json +65 -0
  28. package/defaults/memory-chunk-prompt.md +63 -0
  29. package/defaults/memory-promote-prompt.md +135 -0
  30. package/defaults/mixdog-config.template.json +27 -0
  31. package/defaults/user-workflow.json +8 -0
  32. package/defaults/user-workflow.md +12 -0
  33. package/hooks/hooks.json +73 -0
  34. package/hooks/lib/active-instance.cjs +77 -0
  35. package/hooks/lib/permission-evaluator.cjs +411 -0
  36. package/hooks/lib/permission-route.cjs +63 -0
  37. package/hooks/lib/permission-rules.cjs +170 -0
  38. package/hooks/lib/settings-loader.cjs +116 -0
  39. package/hooks/post-tool-use.cjs +84 -0
  40. package/hooks/pre-mcp-sandbox.cjs +158 -0
  41. package/hooks/pre-tool-subagent.cjs +253 -0
  42. package/hooks/session-start.cjs +1372 -0
  43. package/hooks/turn-timer.cjs +82 -0
  44. package/lib/claude-md-writer.cjs +386 -0
  45. package/lib/config-cjs.cjs +61 -0
  46. package/lib/hook-pipe-path.cjs +10 -0
  47. package/lib/keychain-cjs.cjs +263 -0
  48. package/lib/plugin-paths.cjs +61 -0
  49. package/lib/rules-builder.cjs +241 -0
  50. package/lib/text-utils.cjs +61 -0
  51. package/native/README.md +117 -0
  52. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  53. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  54. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  55. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  56. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  57. package/package.json +107 -0
  58. package/prompts/code-review.txt +16 -0
  59. package/prompts/security-audit.txt +17 -0
  60. package/rules/bridge/00-common.md +39 -0
  61. package/rules/bridge/20-skip-protocol.md +18 -0
  62. package/rules/bridge/30-explorer.md +33 -0
  63. package/rules/bridge/40-cycle1-agent.md +52 -0
  64. package/rules/bridge/41-cycle2-agent.md +62 -0
  65. package/rules/bridge/42-cycle3-agent.md +44 -0
  66. package/rules/lead/00-tool-lead.md +61 -0
  67. package/rules/lead/01-general.md +23 -0
  68. package/rules/lead/02-channels.md +49 -0
  69. package/rules/lead/03-team.md +27 -0
  70. package/rules/lead/04-workflow.md +20 -0
  71. package/rules/shared/00-language.md +14 -0
  72. package/rules/shared/01-tool.md +138 -0
  73. package/scripts/bootstrap.mjs +184 -0
  74. package/scripts/bridge-unify-smoke.mjs +308 -0
  75. package/scripts/build-runtime-linux.sh +348 -0
  76. package/scripts/build-runtime-macos.sh +217 -0
  77. package/scripts/build-runtime-windows.ps1 +242 -0
  78. package/scripts/builtin-utils-smoke.mjs +392 -0
  79. package/scripts/check-json.mjs +45 -0
  80. package/scripts/check-syntax-changed.mjs +102 -0
  81. package/scripts/check-syntax.mjs +58 -0
  82. package/scripts/code-graph-batch.test.mjs +33 -0
  83. package/scripts/config-preserve-smoke.mjs +180 -0
  84. package/scripts/doctor.mjs +484 -0
  85. package/scripts/edit-normalize-fuzz.mjs +130 -0
  86. package/scripts/edit-normalize-smoke.mjs +401 -0
  87. package/scripts/edit-operation-smoke.mjs +369 -0
  88. package/scripts/edit2-smoke.mjs +63 -0
  89. package/scripts/fuzzy-e2e.mjs +28 -0
  90. package/scripts/fuzzy-smoke.mjs +26 -0
  91. package/scripts/generate-runtime-manifest.mjs +166 -0
  92. package/scripts/guard-smoke.mjs +66 -0
  93. package/scripts/hidden-role-schema-smoke.mjs +162 -0
  94. package/scripts/hook-routing-smoke.mjs +29 -0
  95. package/scripts/inject-input.ps1 +204 -0
  96. package/scripts/io-complex-smoke.mjs +667 -0
  97. package/scripts/io-explore-bench.mjs +424 -0
  98. package/scripts/io-guardrails-smoke.mjs +205 -0
  99. package/scripts/io-mini-bench-baseline.json +11 -0
  100. package/scripts/io-mini-bench.mjs +216 -0
  101. package/scripts/io-route-harness.mjs +933 -0
  102. package/scripts/io-telemetry-report.mjs +691 -0
  103. package/scripts/mutation-bench.mjs +564 -0
  104. package/scripts/mutation-io-smoke.mjs +1081 -0
  105. package/scripts/native-patch-bridge-smoke.mjs +288 -0
  106. package/scripts/native-patch-smoke.mjs +304 -0
  107. package/scripts/patch-interior-context-smoke.mjs +49 -0
  108. package/scripts/patch-newline-utf8-smoke.mjs +157 -0
  109. package/scripts/perf-hook-smoke.mjs +71 -0
  110. package/scripts/permission-eval-smoke.mjs +426 -0
  111. package/scripts/prep-patch.mjs +53 -0
  112. package/scripts/prep-shim.mjs +96 -0
  113. package/scripts/provider-cache-smoke.mjs +687 -0
  114. package/scripts/report-runtime-health.mjs +132 -0
  115. package/scripts/run-mcp.mjs +1547 -0
  116. package/scripts/salvage-v4a-shatter.test.mjs +58 -0
  117. package/scripts/scoped-cache-io-smoke.mjs +103 -0
  118. package/scripts/shell-policy-round3-smoke.mjs +46 -0
  119. package/scripts/smoke-runtime-negative.ps1 +100 -0
  120. package/scripts/smoke-runtime-negative.sh +95 -0
  121. package/scripts/stall-policy-smoke.mjs +50 -0
  122. package/scripts/start-memory-worker.mjs +23 -0
  123. package/scripts/statusline-launcher-smoke.mjs +82 -0
  124. package/scripts/stress-atomic-write.mjs +1028 -0
  125. package/scripts/test-config-rmw-restore.mjs +122 -0
  126. package/scripts/test-fault-inject.mjs +164 -0
  127. package/scripts/test-large-file.mjs +174 -0
  128. package/scripts/tool-edge-smoke.mjs +209 -0
  129. package/scripts/uninstall.mjs +201 -0
  130. package/scripts/webhook-selfheal-smoke.mjs +29 -0
  131. package/scripts/write-overwrite-guard-smoke.mjs +56 -0
  132. package/server-main.mjs +3055 -0
  133. package/server.mjs +468 -0
  134. package/setup/config-merge.mjs +254 -0
  135. package/setup/install.mjs +120 -0
  136. package/setup/launch-core.mjs +507 -0
  137. package/setup/launch.mjs +101 -0
  138. package/setup/setup-server.mjs +3206 -0
  139. package/setup/setup.html +3693 -0
  140. package/skills/retro-skill-proposer/SKILL.md +92 -0
  141. package/skills/schedule-add/SKILL.md +77 -0
  142. package/skills/setup/SKILL.md +346 -0
  143. package/skills/webhook-add/SKILL.md +81 -0
  144. package/src/agent/bridge-stall-watchdog.mjs +337 -0
  145. package/src/agent/index.mjs +2138 -0
  146. package/src/agent/orchestrator/activity-bus.mjs +38 -0
  147. package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +1010 -0
  148. package/src/agent/orchestrator/bridge-retry.mjs +220 -0
  149. package/src/agent/orchestrator/bridge-trace.mjs +583 -0
  150. package/src/agent/orchestrator/cache-mtime.mjs +58 -0
  151. package/src/agent/orchestrator/config.mjs +358 -0
  152. package/src/agent/orchestrator/context/collect.mjs +651 -0
  153. package/src/agent/orchestrator/dispatch-persist.mjs +549 -0
  154. package/src/agent/orchestrator/drain-registry.mjs +50 -0
  155. package/src/agent/orchestrator/explore-validator.mjs +8 -0
  156. package/src/agent/orchestrator/internal-roles.mjs +118 -0
  157. package/src/agent/orchestrator/internal-tools.mjs +88 -0
  158. package/src/agent/orchestrator/jobs.mjs +116 -0
  159. package/src/agent/orchestrator/mcp/client.mjs +364 -0
  160. package/src/agent/orchestrator/providers/anthropic-betas.mjs +21 -0
  161. package/src/agent/orchestrator/providers/anthropic-oauth.mjs +1745 -0
  162. package/src/agent/orchestrator/providers/anthropic.mjs +437 -0
  163. package/src/agent/orchestrator/providers/gemini.mjs +1175 -0
  164. package/src/agent/orchestrator/providers/grok-oauth.mjs +782 -0
  165. package/src/agent/orchestrator/providers/model-catalog.mjs +241 -0
  166. package/src/agent/orchestrator/providers/openai-compat.mjs +1467 -0
  167. package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +1890 -0
  168. package/src/agent/orchestrator/providers/openai-oauth.mjs +1307 -0
  169. package/src/agent/orchestrator/providers/openai-ws.mjs +104 -0
  170. package/src/agent/orchestrator/providers/registry.mjs +192 -0
  171. package/src/agent/orchestrator/providers/retry-classifier.mjs +325 -0
  172. package/src/agent/orchestrator/session/abort-lookup.mjs +13 -0
  173. package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
  174. package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
  175. package/src/agent/orchestrator/session/cache/read-cache.mjs +319 -0
  176. package/src/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +11 -0
  177. package/src/agent/orchestrator/session/cache/scoped-cache.mjs +361 -0
  178. package/src/agent/orchestrator/session/cache/util.mjs +49 -0
  179. package/src/agent/orchestrator/session/loop.mjs +1478 -0
  180. package/src/agent/orchestrator/session/manager.mjs +1975 -0
  181. package/src/agent/orchestrator/session/read-dedup.mjs +6 -0
  182. package/src/agent/orchestrator/session/result-classification.mjs +65 -0
  183. package/src/agent/orchestrator/session/save-session-worker.mjs +18 -0
  184. package/src/agent/orchestrator/session/store.mjs +624 -0
  185. package/src/agent/orchestrator/session/stream-watchdog.mjs +130 -0
  186. package/src/agent/orchestrator/session/tool-result-offload.mjs +166 -0
  187. package/src/agent/orchestrator/session/trim.mjs +491 -0
  188. package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +115 -0
  189. package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +327 -0
  190. package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +150 -0
  191. package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +228 -0
  192. package/src/agent/orchestrator/smart-bridge/index.mjs +215 -0
  193. package/src/agent/orchestrator/smart-bridge/profiles.mjs +37 -0
  194. package/src/agent/orchestrator/smart-bridge/registry.mjs +348 -0
  195. package/src/agent/orchestrator/smart-bridge/session-builder.mjs +116 -0
  196. package/src/agent/orchestrator/stall-policy.mjs +195 -0
  197. package/src/agent/orchestrator/tool-loop-guard.mjs +75 -0
  198. package/src/agent/orchestrator/tools/bash-policy-scan.mjs +77 -0
  199. package/src/agent/orchestrator/tools/bash-session.mjs +721 -0
  200. package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
  201. package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +455 -0
  202. package/src/agent/orchestrator/tools/builtin/atomic-write.mjs +236 -0
  203. package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +480 -0
  204. package/src/agent/orchestrator/tools/builtin/binary-file.mjs +76 -0
  205. package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +256 -0
  206. package/src/agent/orchestrator/tools/builtin/cache-layers.mjs +386 -0
  207. package/src/agent/orchestrator/tools/builtin/cwd-utils.mjs +37 -0
  208. package/src/agent/orchestrator/tools/builtin/device-paths.mjs +154 -0
  209. package/src/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +292 -0
  210. package/src/agent/orchestrator/tools/builtin/diff-utils.mjs +109 -0
  211. package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +58 -0
  212. package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +240 -0
  213. package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +113 -0
  214. package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +74 -0
  215. package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +242 -0
  216. package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +211 -0
  217. package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +1364 -0
  218. package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +126 -0
  219. package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +141 -0
  220. package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +194 -0
  221. package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +60 -0
  222. package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +168 -0
  223. package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +173 -0
  224. package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +48 -0
  225. package/src/agent/orchestrator/tools/builtin/fs-reachability.mjs +48 -0
  226. package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +99 -0
  227. package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +170 -0
  228. package/src/agent/orchestrator/tools/builtin/grep-formatting.mjs +113 -0
  229. package/src/agent/orchestrator/tools/builtin/hash-utils.mjs +6 -0
  230. package/src/agent/orchestrator/tools/builtin/list-formatting.mjs +7 -0
  231. package/src/agent/orchestrator/tools/builtin/list-tool.mjs +593 -0
  232. package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +89 -0
  233. package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +300 -0
  234. package/src/agent/orchestrator/tools/builtin/open-config-tool.mjs +26 -0
  235. package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +152 -0
  236. package/src/agent/orchestrator/tools/builtin/path-locks.mjs +35 -0
  237. package/src/agent/orchestrator/tools/builtin/path-utils.mjs +201 -0
  238. package/src/agent/orchestrator/tools/builtin/read-args.mjs +103 -0
  239. package/src/agent/orchestrator/tools/builtin/read-batch.mjs +172 -0
  240. package/src/agent/orchestrator/tools/builtin/read-constants.mjs +40 -0
  241. package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
  242. package/src/agent/orchestrator/tools/builtin/read-image-resize.mjs +189 -0
  243. package/src/agent/orchestrator/tools/builtin/read-image.mjs +88 -0
  244. package/src/agent/orchestrator/tools/builtin/read-lines.mjs +12 -0
  245. package/src/agent/orchestrator/tools/builtin/read-mode-tool.mjs +455 -0
  246. package/src/agent/orchestrator/tools/builtin/read-open.mjs +190 -0
  247. package/src/agent/orchestrator/tools/builtin/read-range-index.mjs +271 -0
  248. package/src/agent/orchestrator/tools/builtin/read-ranges.mjs +26 -0
  249. package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +728 -0
  250. package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +173 -0
  251. package/src/agent/orchestrator/tools/builtin/read-special-files.mjs +268 -0
  252. package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
  253. package/src/agent/orchestrator/tools/builtin/read-tool.mjs +530 -0
  254. package/src/agent/orchestrator/tools/builtin/read-windows.mjs +107 -0
  255. package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +196 -0
  256. package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +422 -0
  257. package/src/agent/orchestrator/tools/builtin/search-builders.mjs +158 -0
  258. package/src/agent/orchestrator/tools/builtin/search-tool.mjs +869 -0
  259. package/src/agent/orchestrator/tools/builtin/shell-analysis.mjs +653 -0
  260. package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +936 -0
  261. package/src/agent/orchestrator/tools/builtin/shell-output.mjs +36 -0
  262. package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +214 -0
  263. package/src/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +143 -0
  264. package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +206 -0
  265. package/src/agent/orchestrator/tools/builtin/snapshot-validation.mjs +98 -0
  266. package/src/agent/orchestrator/tools/builtin/text-stats.mjs +69 -0
  267. package/src/agent/orchestrator/tools/builtin/windows-roots.mjs +23 -0
  268. package/src/agent/orchestrator/tools/builtin/write-tool.mjs +401 -0
  269. package/src/agent/orchestrator/tools/builtin.mjs +500 -0
  270. package/src/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +39 -0
  271. package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
  272. package/src/agent/orchestrator/tools/code-graph.mjs +4095 -0
  273. package/src/agent/orchestrator/tools/cwd-tool.mjs +298 -0
  274. package/src/agent/orchestrator/tools/destructive-warning.mjs +323 -0
  275. package/src/agent/orchestrator/tools/edit-normalize.mjs +603 -0
  276. package/src/agent/orchestrator/tools/env-scrub.mjs +100 -0
  277. package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +144 -0
  278. package/src/agent/orchestrator/tools/graph-manifest.json +26 -0
  279. package/src/agent/orchestrator/tools/host-input.mjs +204 -0
  280. package/src/agent/orchestrator/tools/mutation-content-cache.mjs +67 -0
  281. package/src/agent/orchestrator/tools/mutation-planner.mjs +75 -0
  282. package/src/agent/orchestrator/tools/next-call-utils.mjs +48 -0
  283. package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +133 -0
  284. package/src/agent/orchestrator/tools/patch-manifest.json +26 -0
  285. package/src/agent/orchestrator/tools/patch-tool-defs.mjs +20 -0
  286. package/src/agent/orchestrator/tools/patch.mjs +2754 -0
  287. package/src/agent/orchestrator/tools/progress-message.mjs +118 -0
  288. package/src/agent/orchestrator/tools/result-compression.mjs +279 -0
  289. package/src/agent/orchestrator/tools/shell-command.mjs +865 -0
  290. package/src/agent/orchestrator/tools/shell-exec-policy.mjs +89 -0
  291. package/src/agent/orchestrator/tools/shell-policy-danger-target.mjs +27 -0
  292. package/src/agent/orchestrator/tools/shell-policy-imports.mjs +7 -0
  293. package/src/agent/orchestrator/tools/shell-policy.mjs +345 -0
  294. package/src/agent/orchestrator/tools/shell-snapshot.mjs +313 -0
  295. package/src/agent/orchestrator/workflow-store.mjs +93 -0
  296. package/src/agent/tool-defs.mjs +103 -0
  297. package/src/channels/backends/discord.mjs +784 -0
  298. package/src/channels/data/voice-runtime-manifest.json +138 -0
  299. package/src/channels/index.mjs +3229 -0
  300. package/src/channels/lib/cli-worker-host.mjs +12 -0
  301. package/src/channels/lib/config-lock.mjs +13 -0
  302. package/src/channels/lib/config.mjs +292 -0
  303. package/src/channels/lib/drop-trace.mjs +71 -0
  304. package/src/channels/lib/event-pipeline.mjs +81 -0
  305. package/src/channels/lib/event-queue.mjs +345 -0
  306. package/src/channels/lib/executor.mjs +168 -0
  307. package/src/channels/lib/format.mjs +188 -0
  308. package/src/channels/lib/holidays.mjs +138 -0
  309. package/src/channels/lib/hook-pipe-server.mjs +802 -0
  310. package/src/channels/lib/interaction-workflows.mjs +184 -0
  311. package/src/channels/lib/memory-client.mjs +149 -0
  312. package/src/channels/lib/output-forwarder.mjs +765 -0
  313. package/src/channels/lib/runtime-paths.mjs +479 -0
  314. package/src/channels/lib/scheduler.mjs +723 -0
  315. package/src/channels/lib/session-control.mjs +36 -0
  316. package/src/channels/lib/session-discovery.mjs +103 -0
  317. package/src/channels/lib/settings.mjs +11 -0
  318. package/src/channels/lib/state-file.mjs +68 -0
  319. package/src/channels/lib/status-snapshot.mjs +219 -0
  320. package/src/channels/lib/tool-format.mjs +140 -0
  321. package/src/channels/lib/transcript-discovery.mjs +195 -0
  322. package/src/channels/lib/voice-runtime-fetcher.mjs +734 -0
  323. package/src/channels/lib/webhook.mjs +1179 -0
  324. package/src/channels/lib/whisper-server.mjs +477 -0
  325. package/src/channels/tool-defs.mjs +170 -0
  326. package/src/daemon/host.mjs +118 -0
  327. package/src/daemon/mcp-transport.mjs +47 -0
  328. package/src/daemon/session.mjs +100 -0
  329. package/src/daemon/thin-client.mjs +71 -0
  330. package/src/daemon/transport.mjs +163 -0
  331. package/src/memory/data/runtime-manifest.json +40 -0
  332. package/src/memory/index.mjs +3305 -0
  333. package/src/memory/lib/agent-ipc.mjs +93 -0
  334. package/src/memory/lib/bridge-trace-queries.mjs +120 -0
  335. package/src/memory/lib/core-memory-store.mjs +330 -0
  336. package/src/memory/lib/embedding-provider.mjs +269 -0
  337. package/src/memory/lib/embedding-worker.mjs +323 -0
  338. package/src/memory/lib/llm-worker-host.mjs +17 -0
  339. package/src/memory/lib/memory-cycle.mjs +11 -0
  340. package/src/memory/lib/memory-cycle1.mjs +641 -0
  341. package/src/memory/lib/memory-cycle2.mjs +1284 -0
  342. package/src/memory/lib/memory-cycle3.mjs +540 -0
  343. package/src/memory/lib/memory-embed.mjs +299 -0
  344. package/src/memory/lib/memory-extraction.mjs +5 -0
  345. package/src/memory/lib/memory-maintenance-store.mjs +32 -0
  346. package/src/memory/lib/memory-ops-policy.mjs +190 -0
  347. package/src/memory/lib/memory-recall-id-patch.mjs +15 -0
  348. package/src/memory/lib/memory-recall-read-query.mjs +7 -0
  349. package/src/memory/lib/memory-recall-scope-filter.mjs +63 -0
  350. package/src/memory/lib/memory-recall-store.mjs +621 -0
  351. package/src/memory/lib/memory-retrievers.mjs +112 -0
  352. package/src/memory/lib/memory-score.mjs +71 -0
  353. package/src/memory/lib/memory-text-utils.mjs +58 -0
  354. package/src/memory/lib/memory.mjs +412 -0
  355. package/src/memory/lib/model-profile.mjs +85 -0
  356. package/src/memory/lib/pg/adapter.mjs +308 -0
  357. package/src/memory/lib/pg/process.mjs +360 -0
  358. package/src/memory/lib/pg/supervisor.mjs +396 -0
  359. package/src/memory/lib/project-id-resolver.mjs +86 -0
  360. package/src/memory/lib/runtime-fetcher.mjs +442 -0
  361. package/src/memory/lib/trace-store.mjs +728 -0
  362. package/src/memory/tool-defs.mjs +79 -0
  363. package/src/search/index.mjs +1173 -0
  364. package/src/search/lib/backends/anthropic-oauth.mjs +98 -0
  365. package/src/search/lib/backends/exa.mjs +50 -0
  366. package/src/search/lib/backends/firecrawl.mjs +61 -0
  367. package/src/search/lib/backends/gemini-api.mjs +83 -0
  368. package/src/search/lib/backends/grok-oauth.mjs +86 -0
  369. package/src/search/lib/backends/index.mjs +150 -0
  370. package/src/search/lib/backends/openai-api.mjs +144 -0
  371. package/src/search/lib/backends/openai-oauth.mjs +98 -0
  372. package/src/search/lib/backends/openai-web-search.mjs +76 -0
  373. package/src/search/lib/backends/tavily.mjs +55 -0
  374. package/src/search/lib/backends/xai-api.mjs +113 -0
  375. package/src/search/lib/cache.mjs +131 -0
  376. package/src/search/lib/config.mjs +192 -0
  377. package/src/search/lib/formatter.mjs +115 -0
  378. package/src/search/lib/provider-usage.mjs +67 -0
  379. package/src/search/lib/providers.mjs +47 -0
  380. package/src/search/lib/search-intent.mjs +109 -0
  381. package/src/search/lib/setup-handler.mjs +261 -0
  382. package/src/search/lib/state.mjs +201 -0
  383. package/src/search/lib/web-tools.mjs +1207 -0
  384. package/src/search/tool-defs.mjs +83 -0
  385. package/src/setup/defender-exclusion.mjs +183 -0
  386. package/src/shared/abort-controller.mjs +15 -0
  387. package/src/shared/atomic-file.mjs +420 -0
  388. package/src/shared/config.mjs +350 -0
  389. package/src/shared/daemon-recycle.mjs +108 -0
  390. package/src/shared/disable-claude-builtins.mjs +88 -0
  391. package/src/shared/err-text.mjs +12 -0
  392. package/src/shared/llm/cost.mjs +66 -0
  393. package/src/shared/llm/http-agent.mjs +123 -0
  394. package/src/shared/llm/index.mjs +41 -0
  395. package/src/shared/llm/pid-cleanup.mjs +27 -0
  396. package/src/shared/llm/usage-log.mjs +47 -0
  397. package/src/shared/plugin-paths.mjs +58 -0
  398. package/src/shared/schedules-store.mjs +70 -0
  399. package/src/shared/seed.mjs +119 -0
  400. package/src/shared/user-cwd.mjs +213 -0
  401. package/src/shared/user-data-guard.mjs +238 -0
  402. package/src/status/aggregator.mjs +584 -0
  403. package/src/status/server.mjs +413 -0
  404. package/tools.json +1653 -0
@@ -0,0 +1,584 @@
1
+ /**
2
+ * Bridge-status aggregator.
3
+ *
4
+ * Builds the JSON / text payload consumed by statusline.sh and setup.html.
5
+ * Extracted from setup-server.mjs (0.1.25 and earlier) so both the setup
6
+ * server (on-demand, port 3458) and the MCP-embedded status server (always
7
+ * on, ephemeral port) can serve the same response without drifting.
8
+ *
9
+ * All reads are best-effort; any single source failing leaves that segment
10
+ * empty rather than failing the whole response.
11
+ */
12
+
13
+ import http from 'http';
14
+ import { listSchedules } from '../shared/schedules-store.mjs';
15
+
16
+ import {
17
+ existsSync,
18
+ readFileSync,
19
+ readdirSync,
20
+ } from 'fs';
21
+ import { join } from 'path';
22
+
23
+ const RECENT_MS = 30 * 60 * 1000;
24
+ const SNAPSHOT_STALE_MS = 30_000;
25
+
26
+ // Negative cache for ngrok offline state. Invariant: a /api/tunnels probe
27
+ // that fails or returns no tunnel proves ngrok is offline; re-probing
28
+ // within NGROK_OFFLINE_CACHE_MS adds zero information but pays a ~300ms
29
+ // HTTP timeout per statusline tick. Cache the negative result; any
30
+ // positive result (snapshot or successful probe) bypasses the cache.
31
+ let _ngrokOfflineUntilMs = 0;
32
+ const NGROK_OFFLINE_CACHE_MS = 30_000;
33
+
34
+ // Whole-result cache for buildBridgeStatus. statusline polls at ~1s; a
35
+ // short TTL collapses bursts (paired GET + setup-server tick + occasional
36
+ // duplicate frame) into one IO pass. Sidecar files / sessions scan don't
37
+ // change meaningfully within 800ms, so callers see consistent state.
38
+ // Any caller that needs strictly-fresh data should pass options.noCache.
39
+ const _buildCache = new Map();
40
+ const BUILD_CACHE_MS = 800;
41
+ const TWELVE_H = 12 * 60 * 60 * 1000;
42
+
43
+ // Sessions directory scan cache. statusline polls aggregator at a tight
44
+ // cadence (≤1s); without memoisation the readdirSync + per-file readFileSync +
45
+ // JSON.parse pass burns a single core when sessions/ accumulates hundreds of
46
+ // closed-session JSONs. TTL aligned with statusline polling cadence so the
47
+ // CLOSED_GRACE_MS jitter window below is visible without strobing.
48
+ const SESSIONS_CACHE_TTL_MS = 1_000;
49
+ // Ephemeral maintenance roles (cycle1-agent, cycle2-agent, etc.) close
50
+ // inside ~15-20s, faster than two statusline ticks can render. Keeping a
51
+ // closed bridge session visible for this grace window past its terminal
52
+ // updatedAt smooths the badge so short-lived workers don't strobe in and
53
+ // out invisibly. STREAM_FRESH_MS still drops genuinely stalled sessions.
54
+ const CLOSED_GRACE_MS = 5_000;
55
+ let _sessionsCache = null;
56
+ let _sessionsCacheAt = 0;
57
+
58
+ function readSessionsScan(sessionsDir, now) {
59
+ if (_sessionsCache && (now - _sessionsCacheAt) < SESSIONS_CACHE_TTL_MS) {
60
+ return _sessionsCache;
61
+ }
62
+ const allSessions = [];
63
+ const hbMap = new Map();
64
+ let files = [];
65
+ try {
66
+ files = readdirSync(sessionsDir);
67
+ } catch {
68
+ // ENOENT (fresh install, no sessions/ yet) / EACCES (locked dir) ->
69
+ // empty scan; caller treats as no sessions. Avoids the check-then-
70
+ // readdir race where existsSync says yes but the dir vanishes before
71
+ // readdir, propagating an error into statusline.
72
+ files = [];
73
+ }
74
+ for (const f of files) {
75
+ if (f.endsWith('.json')) {
76
+ try {
77
+ const parsed = JSON.parse(readFileSync(join(sessionsDir, f), 'utf-8'));
78
+ // Reject non-plain-object payloads (null, arrays, strings, numbers).
79
+ // isSessionRunning + downstream consumers do bare property access
80
+ // (s.owner, s.closed, s.id) which throws on null and misbehaves on
81
+ // arrays/primitives — a single malformed file would otherwise sink
82
+ // the whole status build.
83
+ if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
84
+ allSessions.push(parsed);
85
+ }
86
+ }
87
+ catch { /* skip corrupt */ }
88
+ } else if (f.endsWith('.hb')) {
89
+ const id = f.slice(0, -3);
90
+ try {
91
+ const ts = parseInt(readFileSync(join(sessionsDir, f), 'utf-8').trim(), 10);
92
+ if (Number.isFinite(ts)) hbMap.set(id, ts);
93
+ } catch { /* skip */ }
94
+ }
95
+ }
96
+ _sessionsCache = { allSessions, hbMap };
97
+ _sessionsCacheAt = now;
98
+ return _sessionsCache;
99
+ }
100
+
101
+ function normalizeTimestamp(value) {
102
+ if (value === null || value === undefined || value === '') return null;
103
+ const n = Number(value);
104
+ return Number.isFinite(n) ? n : null;
105
+ }
106
+
107
+ function buildRecapSegment(recap = {}) {
108
+ const validStates = new Set(['idle', 'running', 'injected', 'empty', 'error']);
109
+ const rawState = typeof recap.state === 'string' && validStates.has(recap.state) ? recap.state : 'idle';
110
+ return {
111
+ state: rawState,
112
+ running: recap.running === true,
113
+ startedAt: normalizeTimestamp(recap.startedAt),
114
+ lastCompletedAt: normalizeTimestamp(recap.lastCompletedAt),
115
+ updatedAt: normalizeTimestamp(recap.updatedAt),
116
+ errorMessage: typeof recap.errorMessage === 'string' ? recap.errorMessage.slice(0, 200) : null,
117
+ };
118
+ }
119
+
120
+ // Hidden maintenance roles (see defaults/hidden-roles.json kind="maintenance").
121
+ // Plugin-internal background workers without per-terminal ownership: cycle1/2/3,
122
+ // scheduler-task, webhook-handler. Spawned via
123
+ // callBridgeLlm WITHOUT an explicit ownerSessionId, so the session-builder
124
+ // fallback inherits the spawning instance's MIXDOG_OWNER_SESSION_ID rather
125
+ // than leaving it null. The set is still used by the closed-grace / last-
126
+ // completed visibility gates below; owner-scoping (matchesOwnerSession) now
127
+ // keeps each terminal's statusline limited to the workers it actually spawned.
128
+ const SHARED_BACKGROUND_ROLES = new Set([
129
+ 'cycle1-agent',
130
+ 'cycle2-agent',
131
+ 'cycle3-agent',
132
+ 'scheduler-task',
133
+ 'webhook-handler',
134
+ ]);
135
+
136
+ function matchesOwnerSession(session, ownerSessionId) {
137
+ if (!ownerSessionId) return true;
138
+ return session?.ownerSessionId === ownerSessionId;
139
+ }
140
+
141
+ function matchesClientHostPid(session, clientHostPid) {
142
+ if (!clientHostPid) return true;
143
+ return session?.clientHostPid === clientHostPid;
144
+ }
145
+
146
+ function keepClosedSessionVisible(session) {
147
+ // Only grace a genuine just-finished close. idle-sweep / abort batch-closes
148
+ // bump updatedAt on many stale tombstones at once; without this reason gate
149
+ // they all re-enter the 5s closed-grace simultaneously and strobe the badge
150
+ // (the SHARED_BACKGROUND_ROLES "phantom flash" at idle). Genuine completions
151
+ // (e.g. ephemeral-done) are not suppressed, so real finishes still show.
152
+ return session?.role
153
+ && SHARED_BACKGROUND_ROLES.has(session.role)
154
+ && shouldShowLastCompleted(session);
155
+ }
156
+
157
+ const SUPPRESSED_LAST_COMPLETED_REASONS = new Set([
158
+ 'manual',
159
+ 'request-abort',
160
+ 'request-aborted',
161
+ 'retry-replaced',
162
+ 'idle-sweep',
163
+ ]);
164
+
165
+ function shouldShowLastCompleted(session) {
166
+ const reason = session?.closedReason ? String(session.closedReason) : '';
167
+ return !SUPPRESSED_LAST_COMPLETED_REASONS.has(reason);
168
+ }
169
+
170
+ // Active-session filter anchored on stream-recency via the heartbeat
171
+ // file. .hb is the single source of truth — no disk session.status /
172
+ // lastStreamDeltaAt fallback. The 5min window keeps the badge alive
173
+ // across long reasoning blocks (xhigh effort, deep tool chains) where
174
+ // the gap between SSE chunks routinely exceeds 30s. Genuine stalls
175
+ // eventually trip the bridge-stall-watchdog or the RUNNING_STALL_MS
176
+ // sweep at 10min; closed:true plants land on disk synchronously so the
177
+ // badge drops immediately on real completion.
178
+ const STREAM_FRESH_MS = 5 * 60 * 1000;
179
+
180
+ // Mirror of the store-side running-stall window (store.mjs:489). A not-yet-
181
+ // closed session whose heartbeat lapsed during a long non-streaming tool call
182
+ // stays visible until this window — matching when the store sweep would
183
+ // actually close it — so deep tool chains don't blink out mid-run.
184
+ const RUNNING_STALL_MS = 10 * 60 * 1000;
185
+
186
+ // Shared running-session predicate used by both the full and compact
187
+ // builders. Multi-CC isolation: owner-scoped status servers must not
188
+ // show bridge sessions from another terminal — untagged legacy
189
+ // sessions are hidden once the request is owner-scoped. Heartbeat-less
190
+ // grace is reserved for shared background roles; user worker sessions
191
+ // must have a fresh heartbeat, otherwise stale tombstones can flash in
192
+ // unrelated statuslines during spawn/claim races.
193
+ function isSessionRunning(s, { hbMap, now, ownerSessionId, clientHostPid, ownerHostPid, includeClosedGrace = true }) {
194
+ if (s.owner !== 'bridge') return false;
195
+ if (!matchesOwnerSession(s, ownerSessionId)) return false;
196
+ const isSharedBackground = s.role && SHARED_BACKGROUND_ROLES.has(s.role);
197
+ // Scoping unit differs by role kind (invariant, not a fallback):
198
+ // - Maintenance/background roles (cycle1/2/3-agent, scheduler-task,
199
+ // webhook-handler) are spawned by the memory daemon via
200
+ // callBridgeLlm WITHOUT a clientHostPid, so their session JSON has
201
+ // none. They belong to the ACTIVE OWNER TERMINAL (owner = one live
202
+ // session, NOT the daemon): on a shared daemon every attached
203
+ // terminal carries the same ownerSessionId, so owner-scoping alone
204
+ // would leak maintenance chips onto EVERY terminal. Gate them
205
+ // on the requesting terminal being the active-instance owner —
206
+ // clientHostPid === ownerHostPid (active-instance.json SSOT). When
207
+ // ownerHostPid or the request clientHostPid is absent (legacy /
208
+ // single-instance), fall through to owner-scoping unchanged.
209
+ // - User worker sessions (worker/reviewer/debugger/tester) carry a
210
+ // per-terminal clientHostPid and stay strictly isolated to the
211
+ // terminal that spawned them.
212
+ if (isSharedBackground) {
213
+ if (ownerHostPid && clientHostPid && clientHostPid !== ownerHostPid) return false;
214
+ } else if (!matchesClientHostPid(s, clientHostPid)) {
215
+ return false;
216
+ }
217
+ if (s.closed === true) {
218
+ if (!includeClosedGrace) return false;
219
+ return keepClosedSessionVisible(s) && (now - (s.updatedAt || 0)) <= CLOSED_GRACE_MS;
220
+ }
221
+ const hb = hbMap.get(s.id);
222
+ if (hb && (now - hb) <= STREAM_FRESH_MS) return true;
223
+ if (s.status && s.status !== 'running') return false;
224
+ const statusIsRunning = s.status === 'running';
225
+ // Heartbeat-less grace is only for plugin-owned background work. Public
226
+ // worker/reviewer/debugger sessions clear their heartbeat on completion but
227
+ // may remain on disk as idle tombstones, so treating updatedAt as liveness
228
+ // makes finished workers linger in L2 and leak across terminals.
229
+ if (isSharedBackground && statusIsRunning && (now - (s.updatedAt || 0)) <= RUNNING_STALL_MS) return true;
230
+ return false;
231
+ }
232
+
233
+ // Idle bridge-worker predicate. A worker that has finished its turn clears
234
+ // its heartbeat (so isSessionRunning is false) but may remain on disk as an
235
+ // idle tombstone until the store reaps it. Reaped sessions are deleted from
236
+ // sessions/ entirely, so they fall out of allSessions and vanish from L2 with
237
+ // no extra logic here. We surface the surviving idle workers (greyed) so the
238
+ // user can see a worker is parked-but-present, distinct from RUNNING.
239
+ // Maintenance/background roles are excluded — only owner-scoped user workers.
240
+ function isIdleWorkerSession(s, { ownerSessionId, clientHostPid }) {
241
+ if (s.owner !== 'bridge') return false;
242
+ if (!matchesOwnerSession(s, ownerSessionId)) return false;
243
+ if (!matchesClientHostPid(s, clientHostPid)) return false;
244
+ if (s.closed === true) return false; // closed → handled by closed-grace / lastCompleted
245
+ if (s.role && SHARED_BACKGROUND_ROLES.has(s.role)) return false; // maintenance, not a user worker
246
+ return true; // on-disk, not running (callers exclude running ids), not closed → idle
247
+ }
248
+
249
+ // Build the unified worker list consumed by L2: running workers carry
250
+ // status 'running', surviving idle tombstones carry 'idle'. Each entry keeps
251
+ // its bridgeTag so the statusline can render tag + status. This is the single
252
+ // thread that carries running/idle status from aggregator → statusline-lib.
253
+ function buildWorkerList(running, idle) {
254
+ return [
255
+ ...running.map((s) => ({ tag: s.bridgeTag || s.role || 'agent', status: 'running' })),
256
+ ...idle.map((s) => ({ tag: s.bridgeTag || s.role || 'agent', status: 'idle' })),
257
+ ].filter((w) => w.tag);
258
+ }
259
+
260
+ // Snapshot → nextSchedule parser. Used by both builders. Returns null
261
+ // when the snapshot has no schedules.next entry or the fireAt is not a
262
+ // finite number; otherwise returns { name, fireAt }.
263
+ function parseNextScheduleFromSnap(snap) {
264
+ if (!snap?.schedules?.next) return null;
265
+ const fireAt = Number(snap.schedules.next.fireAt);
266
+ if (!Number.isFinite(fireAt)) return null;
267
+ // ECMAScript caps valid Date instants at ±8.64e15 ms from epoch; finite
268
+ // numbers outside that range yield an Invalid Date whose toISOString()
269
+ // throws RangeError. Treat out-of-range snapshots as best-effort-absent
270
+ // so a malformed schedules.next does not sink buildBridgeStatus.
271
+ if (Math.abs(fireAt) > 8.64e15) return null;
272
+ return { name: snap.schedules.next.name, fireAt };
273
+ }
274
+
275
+ export async function buildBridgeStatus(dataDir, options = {}) {
276
+ const now = Date.now();
277
+ // Short-TTL whole-result cache. statusline tick is ~1s, so an 800ms TTL
278
+ // turns repeated GETs (paired status + setup duplicate) into zero-cost
279
+ // returns while keeping per-tick freshness for the user.
280
+ const _cacheKey = `${options.ownerSessionId || ''}|${options.clientHostPid ?? ''}`;
281
+ if (!options.noCache) {
282
+ const _cached = _buildCache.get(_cacheKey);
283
+ if (_cached && now - _cached.at < BUILD_CACHE_MS) return _cached.result;
284
+ }
285
+
286
+ const SESSIONS_DIR = join(dataDir, 'sessions');
287
+ const STATUS_SNAPSHOT_PATH = join(dataDir, 'channels', 'status-snapshot.json');
288
+ const JOBS_STATE_PATH = join(dataDir, 'jobs', 'state.json');
289
+ const CACHE_STATS_PATH = join(dataDir, 'cache-stats.json');
290
+
291
+ // ── 1. Active + recently-completed bridge sessions ────────────────
292
+ // Single readdir over sessions/ separates `<id>.json` (full session
293
+ // payload) from `<id>.hb` (lightweight heartbeat published by
294
+ // markSessionStreamDelta on a ≤5s self-throttle). The .hb file is the
295
+ // authoritative fresh signal because the heavy session.json save is
296
+ // throttled to 60s.
297
+ const { allSessions, hbMap } = readSessionsScan(SESSIONS_DIR, now);
298
+ const running = allSessions.filter(s => isSessionRunning(s, {
299
+ hbMap, now, ownerSessionId: options.ownerSessionId, clientHostPid: options.clientHostPid, ownerHostPid: options.ownerHostPid,
300
+ }));
301
+ const runningRoles = running.map(s => s.bridgeTag || s.role || 'agent').filter(Boolean);
302
+ // Surviving idle worker tombstones (running ones excluded so a session is
303
+ // never counted twice). Reaped sessions are gone from allSessions already.
304
+ const runningIds = new Set(running.map(s => s.id));
305
+ const idle = allSessions.filter(s => !runningIds.has(s.id)
306
+ && isIdleWorkerSession(s, { ownerSessionId: options.ownerSessionId, clientHostPid: options.clientHostPid }));
307
+ const workers = buildWorkerList(running, idle);
308
+
309
+ const recentClosed = allSessions
310
+ .filter(s => s.owner === 'bridge'
311
+ && s.closed === true
312
+ && shouldShowLastCompleted(s)
313
+ && matchesOwnerSession(s, options.ownerSessionId)
314
+ && matchesClientHostPid(s, options.clientHostPid)
315
+ && (now - (s.updatedAt || 0)) <= RECENT_MS)
316
+ .sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
317
+ const lastCompleted = recentClosed[0] || null;
318
+
319
+ // ── 2. Scheduler state ────────────────────────────────────────────
320
+ let scheduleActive = 0;
321
+ let scheduleDeferred = 0;
322
+ let nextSchedule = null;
323
+ let discordTotalUnread = null;
324
+ let ngrokTunnelUrl = null;
325
+ let snapshotFresh = false;
326
+ try {
327
+ if (existsSync(STATUS_SNAPSHOT_PATH)) {
328
+ const snap = JSON.parse(readFileSync(STATUS_SNAPSHOT_PATH, 'utf-8'));
329
+ if (snap && typeof snap.writtenAt === 'number' && (now - snap.writtenAt) <= SNAPSHOT_STALE_MS) {
330
+ snapshotFresh = true;
331
+ // Schedules truth source: `${CLAUDE_PLUGIN_DATA}/schedules/<name>/`.
332
+ // listSchedules() is shared with channels/lib/config.mjs and
333
+ // setup-server so all three see the same entry list.
334
+ const allSchedules = listSchedules().filter(s => s.enabled !== false && s.name);
335
+ scheduleActive = allSchedules.length;
336
+ scheduleDeferred = snap.schedules?.deferredCount ?? 0;
337
+ nextSchedule = parseNextScheduleFromSnap(snap);
338
+ if (typeof snap.discord?.totalUnread === 'number') {
339
+ discordTotalUnread = snap.discord.totalUnread;
340
+ }
341
+ if (snap.ngrok?.tunnelUrl) {
342
+ ngrokTunnelUrl = snap.ngrok.tunnelUrl;
343
+ }
344
+ }
345
+ }
346
+ } catch { /* snapshot unreadable */ }
347
+
348
+ if (!snapshotFresh) {
349
+ try {
350
+ const allSchedules = listSchedules().filter(s => s.enabled !== false && s.name);
351
+ scheduleActive = allSchedules.length;
352
+
353
+ const candidates = [];
354
+ for (const s of allSchedules) {
355
+ if (!s.time || !/^\d{2}:\d{2}$/.test(s.time)) continue;
356
+ // Skip entries the snapshot-less fallback cannot faithfully
357
+ // evaluate: cron expressions, non-local timezones, day-of-week
358
+ // restrictions, and any deferrals (only carried in the snapshot).
359
+ // Emitting a falsely-imminent upcoming entry is worse than
360
+ // showing none — the statusline simply omits the badge.
361
+ if (s.cron) continue;
362
+ if (s.timezone) continue;
363
+ if (Array.isArray(s.days) && s.days.length > 0) continue;
364
+ const [hh, mm] = s.time.split(':').map(Number);
365
+ for (const offsetDays of [0, 1]) {
366
+ const candidate = new Date(now);
367
+ candidate.setDate(candidate.getDate() + offsetDays);
368
+ candidate.setHours(hh, mm, 0, 0);
369
+ const diff = candidate.getTime() - now;
370
+ if (diff > 0 && diff <= TWELVE_H) {
371
+ candidates.push({ name: s.name, fireAt: candidate.getTime(), diff });
372
+ }
373
+ }
374
+ }
375
+ candidates.sort((a, b) => a.diff - b.diff);
376
+ if (candidates.length > 0) nextSchedule = candidates[0];
377
+ } catch { /* config unreadable */ }
378
+ }
379
+
380
+ // ── 3. Jobs count ────────────────────────────────────────────────
381
+ let jobsCount = 0;
382
+ if (existsSync(JOBS_STATE_PATH)) {
383
+ try {
384
+ const jobsState = JSON.parse(readFileSync(JOBS_STATE_PATH, 'utf-8'));
385
+ if (Array.isArray(jobsState)) {
386
+ jobsCount = jobsState.filter(j => j.status === 'running').length;
387
+ }
388
+ } catch { /* unreadable */ }
389
+ }
390
+
391
+ // ── 5. Ngrok online ──────────────────────────────────────────────
392
+ let ngrokOnline = false;
393
+ if (ngrokTunnelUrl) {
394
+ ngrokOnline = true;
395
+ _ngrokOfflineUntilMs = 0; // positive evidence resets negative cache
396
+ } else if (Date.now() < _ngrokOfflineUntilMs) {
397
+ // Negative cache hit — skip the ~300ms HTTP probe.
398
+ ngrokOnline = false;
399
+ } else {
400
+ await new Promise((resolve) => {
401
+ const timer = setTimeout(() => { try { req_ng && req_ng.destroy(); } catch {} resolve(); }, 300);
402
+ let req_ng;
403
+ try {
404
+ req_ng = http.get('http://127.0.0.1:4040/api/tunnels', (r) => {
405
+ clearTimeout(timer);
406
+ let body = '';
407
+ r.on('data', d => { body += d; });
408
+ r.on('end', () => {
409
+ try {
410
+ const parsed = JSON.parse(body);
411
+ const tunnel = (parsed.tunnels || []).find(t => t.public_url);
412
+ if (tunnel) {
413
+ ngrokOnline = true;
414
+ ngrokTunnelUrl = tunnel.public_url;
415
+ }
416
+ } catch { /* ignore */ }
417
+ resolve();
418
+ });
419
+ });
420
+ req_ng.on('error', () => { clearTimeout(timer); resolve(); });
421
+ req_ng.setTimeout(300, () => { clearTimeout(timer); try { req_ng.destroy(); } catch {} resolve(); });
422
+ } catch { clearTimeout(timer); resolve(); }
423
+ });
424
+ if (ngrokOnline) {
425
+ _ngrokOfflineUntilMs = 0;
426
+ } else {
427
+ _ngrokOfflineUntilMs = Date.now() + NGROK_OFFLINE_CACHE_MS;
428
+ }
429
+ }
430
+
431
+ // ── Assemble payload ─────────────────────────────────────────────
432
+ const sessionSegment = running.length > 0
433
+ ? { active: running.length, roles: runningRoles, workers }
434
+ : { active: 0, roles: [], workers };
435
+
436
+ let lastCompletedSegment = null;
437
+ if (lastCompleted) {
438
+ const ageMs = now - (lastCompleted.updatedAt || 0);
439
+ lastCompletedSegment = {
440
+ role: lastCompleted.role || 'agent',
441
+ agoMinutes: Math.round(ageMs / 60000),
442
+ };
443
+ }
444
+
445
+ const scheduleSegment = {
446
+ active: scheduleActive,
447
+ deferred: scheduleDeferred,
448
+ next: nextSchedule ? {
449
+ name: nextSchedule.name,
450
+ fireAt: nextSchedule.fireAt,
451
+ fireAtISO: new Date(nextSchedule.fireAt).toISOString(),
452
+ } : null,
453
+ };
454
+ const recapSegment = buildRecapSegment(options.recap);
455
+
456
+ // ── 6. Cache stats ───────────────────────────────────────────────
457
+ let cacheStats = null;
458
+ if (existsSync(CACHE_STATS_PATH)) {
459
+ try {
460
+ const raw = JSON.parse(readFileSync(CACHE_STATS_PATH, 'utf-8'));
461
+ if (raw && typeof raw.writtenAt === 'number' && typeof raw.totals === 'object') {
462
+ const t = raw.totals;
463
+ cacheStats = {
464
+ writtenAt: raw.writtenAt,
465
+ totals: {
466
+ sets: t.sets ?? 0,
467
+ hits: t.hits ?? 0,
468
+ misses: t.misses ?? 0,
469
+ clears: t.clears ?? 0,
470
+ },
471
+ perSession: Array.isArray(raw.perSession) ? raw.perSession : [],
472
+ };
473
+ }
474
+ } catch (e) { try { process.stderr.write(`[status-aggregator] partial-fetch swallow: ${e?.message ?? e}\n`); } catch {} }
475
+ }
476
+
477
+ const _result = {
478
+ sessions: sessionSegment,
479
+ lastCompleted: lastCompletedSegment,
480
+ schedule: scheduleSegment,
481
+ jobs: { count: jobsCount },
482
+ recap: recapSegment,
483
+ ngrok: { online: ngrokOnline, tunnelUrl: ngrokTunnelUrl ?? undefined },
484
+ ...(discordTotalUnread !== null ? { discord: { totalUnread: discordTotalUnread } } : {}),
485
+ ...(cacheStats !== null ? { cacheStats } : {}),
486
+ snapshotFresh,
487
+ generatedAt: new Date(now).toISOString(),
488
+ };
489
+ if (!options.noCache) {
490
+ _buildCache.set(_cacheKey, { result: _result, at: now });
491
+ }
492
+ return _result;
493
+ }
494
+
495
+ // Compact payload for the statusline 1-5 s polling cadence. Builds only
496
+ // the fields bin/statusline.mjs actually consumes (sessions.roles +
497
+ // schedule.next.{name,fireAt}); skips ngrok probe (~300 ms IO),
498
+ // cache-stats, lastCompleted, jobs.count, discord, recap. Reuses the
499
+ // 1 s readSessionsScan cache so repeated ticks see one disk scan
500
+ // amortised across all statusline-json polls within the window. Compact L2
501
+ // intentionally excludes closed-grace workers; completed work belongs in the
502
+ // full `lastCompleted` segment, not the running badge.
503
+ export async function buildBridgeStatusCompact(dataDir, options = {}) {
504
+ const now = Date.now();
505
+ const SESSIONS_DIR = join(dataDir, 'sessions');
506
+ const STATUS_SNAPSHOT_PATH = join(dataDir, 'channels', 'status-snapshot.json');
507
+
508
+ const { allSessions, hbMap } = readSessionsScan(SESSIONS_DIR, now);
509
+ const running = allSessions.filter((s) => isSessionRunning(s, {
510
+ hbMap,
511
+ now,
512
+ ownerSessionId: options.ownerSessionId,
513
+ clientHostPid: options.clientHostPid,
514
+ ownerHostPid: options.ownerHostPid,
515
+ includeClosedGrace: false,
516
+ }));
517
+ const runningRoles = running.map((s) => s.bridgeTag || s.role || 'agent').filter(Boolean);
518
+ // Mirror buildBridgeStatus: surface surviving idle worker tombstones too.
519
+ // Note compact deliberately excludes closed-grace from `running`; idle here
520
+ // means on-disk, not running, not closed — reaped sessions are already gone.
521
+ const runningIds = new Set(running.map((s) => s.id));
522
+ const idle = allSessions.filter((s) => !runningIds.has(s.id)
523
+ && isIdleWorkerSession(s, { ownerSessionId: options.ownerSessionId, clientHostPid: options.clientHostPid }));
524
+ const workers = buildWorkerList(running, idle);
525
+
526
+ let nextSchedule = null;
527
+ try {
528
+ if (existsSync(STATUS_SNAPSHOT_PATH)) {
529
+ const snap = JSON.parse(readFileSync(STATUS_SNAPSHOT_PATH, 'utf-8'));
530
+ if (snap && typeof snap.writtenAt === 'number' && (now - snap.writtenAt) <= SNAPSHOT_STALE_MS) {
531
+ nextSchedule = parseNextScheduleFromSnap(snap);
532
+ }
533
+ }
534
+ } catch { /* snapshot unreadable */ }
535
+
536
+ return {
537
+ sessions: { active: running.length, roles: runningRoles, workers },
538
+ schedule: { next: nextSchedule },
539
+ };
540
+ }
541
+
542
+ export function renderBridgeStatusText(payload) {
543
+ const parts = [];
544
+ const running = payload.sessions?.active || 0;
545
+ const roles = payload.sessions?.roles || [];
546
+
547
+ if (running > 0) {
548
+ const roleList = [...new Set(roles)].join(',');
549
+ parts.push(`⚙ ${running} running (${roleList})`);
550
+ } else {
551
+ parts.push('idle');
552
+ }
553
+
554
+ if (payload.lastCompleted) {
555
+ const ageMins = payload.lastCompleted.agoMinutes || 0;
556
+ const timeAgo = ageMins <= 0 ? 'just now' : `${ageMins}m`;
557
+ parts.push(`✓ ${payload.lastCompleted.role} ${timeAgo}`);
558
+ }
559
+
560
+ if (payload.schedule?.next) {
561
+ const d = new Date(payload.schedule.next.fireAt);
562
+ const hh = String(d.getHours()).padStart(2, '0');
563
+ const mm = String(d.getMinutes()).padStart(2, '0');
564
+ parts.push(`⏰ ${hh}:${mm} ${payload.schedule.next.name}`);
565
+ }
566
+
567
+ if (payload.schedule?.active > 0) {
568
+ const def = payload.schedule.deferred || 0;
569
+ parts.push(def > 0
570
+ ? `📋 ${payload.schedule.active}/${def}def`
571
+ : `📋 ${payload.schedule.active}`);
572
+ }
573
+
574
+ if (payload.cacheStats?.totals) {
575
+ const { hits, misses } = payload.cacheStats.totals;
576
+ const total = hits + misses;
577
+ if (total >= 1) {
578
+ const rate = Math.round((hits / total) * 100);
579
+ parts.push(`cache ${hits}/${total} (${rate}%)`);
580
+ }
581
+ }
582
+
583
+ return parts.join(' · ');
584
+ }