@polderlabs/bizar 3.23.1 → 4.2.0

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 (361) hide show
  1. package/README.md +83 -16
  2. package/bizar-dash/.bizar/activity.log +3 -0
  3. package/bizar-dash/.bizar/graph/.graphify_analysis.json +1540 -0
  4. package/bizar-dash/.bizar/graph/.graphify_labels.json +1 -0
  5. package/bizar-dash/.bizar/graph/GRAPH_REPORT.md +404 -0
  6. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/0661b816358db84dfdb7f10443db8a61152fd20b3fee950caf8fbf32920a1790.json +1 -0
  7. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/16da06d774142a6e5d74829ad982531286a6193ef798de8f593db029e123ec32.json +1 -0
  8. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/27e5697c01254c69c64c5a33896669073b40afb5ba88375102ae77fa5404136d.json +1 -0
  9. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/532997898027254aa4c0932243dc71d2f55888181232b65974961934997dc85b.json +1 -0
  10. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/84e3451a64c7f99f0cd6a88b0748d7426780486f53ab4935b7476b28c08f8293.json +1 -0
  11. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/88e5ea5f3e78fa69353fe9e6a8d1a05391d54605e25600cb125531eb5a6432ff.json +1 -0
  12. package/bizar-dash/.bizar/graph/cache/stat-index.json +1 -0
  13. package/bizar-dash/.bizar/graph/graph.html +307 -0
  14. package/bizar-dash/.bizar/graph/graph.json +40267 -0
  15. package/bizar-dash/.bizar/graph/manifest.json +762 -0
  16. package/bizar-dash/.graphifyignore +40 -0
  17. package/bizar-dash/.obsidian/README.md +14 -0
  18. package/bizar-dash/.obsidian/daily/2026-06-25.md +9 -0
  19. package/bizar-dash/.obsidian/vault.json +7 -0
  20. package/bizar-dash/.od-skills/web-prototype-e1429c1737/SKILL.md +103 -0
  21. package/bizar-dash/.od-skills/web-prototype-e1429c1737/assets/template.html +338 -0
  22. package/bizar-dash/.od-skills/web-prototype-e1429c1737/example.html +81 -0
  23. package/bizar-dash/.od-skills/web-prototype-e1429c1737/open-design.json +172 -0
  24. package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/checklist.md +44 -0
  25. package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/layouts.md +247 -0
  26. package/bizar-dash/CHANGELOG.md +474 -0
  27. package/bizar-dash/DESIGN.md +136 -0
  28. package/bizar-dash/dist/assets/main-B3RgW6FS.js +331 -0
  29. package/bizar-dash/dist/assets/main-B3RgW6FS.js.map +1 -0
  30. package/bizar-dash/dist/assets/main-DlJ8wgLR.css +1 -0
  31. package/bizar-dash/dist/assets/mobile-CsZQAswA.css +1 -0
  32. package/bizar-dash/dist/assets/mobile-DbxIw8BG.js +2 -0
  33. package/bizar-dash/dist/assets/mobile-DbxIw8BG.js.map +1 -0
  34. package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js +354 -0
  35. package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js.map +1 -0
  36. package/bizar-dash/dist/index.html +40 -0
  37. package/bizar-dash/dist/mobile.html +22 -0
  38. package/bizar-dash/plans/debug-plan/comments.json +1 -0
  39. package/bizar-dash/plans/debug-plan/meta.json +10 -0
  40. package/bizar-dash/plans/debug-plan/plan.json +174 -0
  41. package/bizar-dash/scripts/smoke-bg-retry.mjs +246 -0
  42. package/bizar-dash/src/cli/dashboard-ports.mjs +488 -0
  43. package/bizar-dash/src/cli.mjs +522 -0
  44. package/bizar-dash/src/server/activity-log.mjs +174 -0
  45. package/bizar-dash/src/server/agents-store.mjs +465 -0
  46. package/bizar-dash/src/server/api.mjs +138 -0
  47. package/bizar-dash/src/server/artifact-template.html +810 -0
  48. package/bizar-dash/src/server/artifacts-store.mjs +733 -0
  49. package/bizar-dash/src/server/auth.mjs +381 -0
  50. package/bizar-dash/src/server/background-store.mjs +447 -0
  51. package/bizar-dash/src/server/bg-poller.mjs +305 -0
  52. package/bizar-dash/src/server/bg-retry.mjs +574 -0
  53. package/bizar-dash/src/server/browser.mjs +40 -0
  54. package/bizar-dash/src/server/diagnostics-store.mjs +148 -0
  55. package/bizar-dash/src/server/dialog-poller.mjs +105 -0
  56. package/bizar-dash/src/server/dialog-store.mjs +177 -0
  57. package/bizar-dash/src/server/glyphs/mdx-compiler.mjs +583 -0
  58. package/bizar-dash/src/server/lib/path-safe.mjs +283 -0
  59. package/bizar-dash/src/server/memory-git.mjs +329 -0
  60. package/bizar-dash/src/server/memory-lightrag.mjs +767 -0
  61. package/bizar-dash/src/server/memory-schema.mjs +145 -0
  62. package/bizar-dash/src/server/memory-secrets.mjs +84 -0
  63. package/bizar-dash/src/server/memory-store.mjs +471 -0
  64. package/bizar-dash/src/server/mod-security.mjs +398 -0
  65. package/bizar-dash/src/server/mods-loader.mjs +1067 -0
  66. package/bizar-dash/src/server/notifications-store.mjs +243 -0
  67. package/bizar-dash/src/server/obsidian-store.mjs +318 -0
  68. package/bizar-dash/src/server/opencode-runner.mjs +279 -0
  69. package/bizar-dash/src/server/pair-store.mjs +138 -0
  70. package/bizar-dash/src/server/projects-store.mjs +364 -0
  71. package/bizar-dash/src/server/providers-store.mjs +789 -0
  72. package/bizar-dash/src/server/routes/_shared.mjs +415 -0
  73. package/bizar-dash/src/server/routes/activity.mjs +110 -0
  74. package/bizar-dash/src/server/routes/agents.mjs +134 -0
  75. package/bizar-dash/src/server/routes/artifacts.mjs +279 -0
  76. package/bizar-dash/src/server/routes/auth.mjs +69 -0
  77. package/bizar-dash/src/server/routes/background.mjs +165 -0
  78. package/bizar-dash/src/server/routes/chat.mjs +485 -0
  79. package/bizar-dash/src/server/routes/config.mjs +129 -0
  80. package/bizar-dash/src/server/routes/diagnostics.mjs +50 -0
  81. package/bizar-dash/src/server/routes/dialogs.mjs +86 -0
  82. package/bizar-dash/src/server/routes/fs.mjs +429 -0
  83. package/bizar-dash/src/server/routes/history.mjs +78 -0
  84. package/bizar-dash/src/server/routes/memory.mjs +658 -0
  85. package/bizar-dash/src/server/routes/misc.mjs +89 -0
  86. package/bizar-dash/src/server/routes/mods.mjs +359 -0
  87. package/bizar-dash/src/server/routes/notifications.mjs +54 -0
  88. package/bizar-dash/src/server/routes/obsidian.mjs +239 -0
  89. package/bizar-dash/src/server/routes/opencode-sessions.mjs +55 -0
  90. package/bizar-dash/src/server/routes/overview.mjs +91 -0
  91. package/bizar-dash/src/server/routes/pair.mjs +67 -0
  92. package/bizar-dash/src/server/routes/projects.mjs +175 -0
  93. package/bizar-dash/src/server/routes/providers.mjs +47 -0
  94. package/bizar-dash/src/server/routes/schedules.mjs +104 -0
  95. package/bizar-dash/src/server/routes/settings.mjs +51 -0
  96. package/bizar-dash/src/server/routes/skills.mjs +76 -0
  97. package/bizar-dash/src/server/routes/tasks.mjs +468 -0
  98. package/bizar-dash/src/server/routes/themes.mjs +51 -0
  99. package/bizar-dash/src/server/routes-v2/auth.mjs +59 -0
  100. package/bizar-dash/src/server/routes-v2/events.mjs +107 -0
  101. package/bizar-dash/src/server/routes-v2/health.mjs +21 -0
  102. package/bizar-dash/src/server/routes-v2/index.mjs +90 -0
  103. package/bizar-dash/src/server/routes-v2/sessions.mjs +111 -0
  104. package/bizar-dash/src/server/schedules-runner.mjs +369 -0
  105. package/bizar-dash/src/server/schedules-store.mjs +268 -0
  106. package/bizar-dash/src/server/search-store.mjs +220 -0
  107. package/bizar-dash/src/server/serve-info.mjs +684 -0
  108. package/bizar-dash/src/server/server.mjs +796 -0
  109. package/bizar-dash/src/server/settings-store.mjs +45 -0
  110. package/bizar-dash/src/server/skills-store.mjs +300 -0
  111. package/bizar-dash/src/server/state.mjs +437 -0
  112. package/bizar-dash/src/server/tailscale-store.mjs +113 -0
  113. package/bizar-dash/src/server/task-delegator.mjs +824 -0
  114. package/bizar-dash/src/server/tasks-store.mjs +508 -0
  115. package/bizar-dash/src/server/tui.mjs +923 -0
  116. package/bizar-dash/src/server/update-store.mjs +168 -0
  117. package/bizar-dash/src/server/v2-auth-file.mjs +99 -0
  118. package/bizar-dash/src/server/v2-event-bus.mjs +128 -0
  119. package/bizar-dash/src/server/watcher.mjs +81 -0
  120. package/bizar-dash/src/server/yaml.mjs +238 -0
  121. package/bizar-dash/src/web/App.tsx +718 -0
  122. package/bizar-dash/src/web/MobileApp.tsx +408 -0
  123. package/bizar-dash/src/web/components/ArtifactCreateDialog.tsx +95 -0
  124. package/bizar-dash/src/web/components/ArtifactListDialog.tsx +55 -0
  125. package/bizar-dash/src/web/components/ArtifactViewer.tsx +143 -0
  126. package/bizar-dash/src/web/components/AuditDialog.tsx +79 -0
  127. package/bizar-dash/src/web/components/BgStatusBadge.tsx +32 -0
  128. package/bizar-dash/src/web/components/Button.tsx +56 -0
  129. package/bizar-dash/src/web/components/CanvasContextMenu.tsx +68 -0
  130. package/bizar-dash/src/web/components/Card.tsx +40 -0
  131. package/bizar-dash/src/web/components/CollapsibleSection.tsx +72 -0
  132. package/bizar-dash/src/web/components/CommandDialog.tsx +66 -0
  133. package/bizar-dash/src/web/components/EmptyState.tsx +30 -0
  134. package/bizar-dash/src/web/components/FileBrowser.tsx +633 -0
  135. package/bizar-dash/src/web/components/HelpDialog.tsx +55 -0
  136. package/bizar-dash/src/web/components/KillConfirmDialog.tsx +88 -0
  137. package/bizar-dash/src/web/components/Modal.tsx +215 -0
  138. package/bizar-dash/src/web/components/Notifications.tsx +241 -0
  139. package/bizar-dash/src/web/components/SearchModal.tsx +224 -0
  140. package/bizar-dash/src/web/components/Sidebar.tsx +66 -0
  141. package/bizar-dash/src/web/components/Spinner.tsx +19 -0
  142. package/bizar-dash/src/web/components/StatusBadge.tsx +25 -0
  143. package/bizar-dash/src/web/components/Tag.tsx +28 -0
  144. package/bizar-dash/src/web/components/Toast.tsx +150 -0
  145. package/bizar-dash/src/web/components/Topbar.tsx +368 -0
  146. package/bizar-dash/src/web/components/VisualPlanDialog.tsx +58 -0
  147. package/bizar-dash/src/web/components/chat/AgentChip.tsx +68 -0
  148. package/bizar-dash/src/web/components/chat/ChatBubble.tsx +60 -0
  149. package/bizar-dash/src/web/components/chat/ChatThread.tsx +88 -0
  150. package/bizar-dash/src/web/components/chat/ChatTopBar.tsx +50 -0
  151. package/bizar-dash/src/web/components/chat/Composer.tsx +164 -0
  152. package/bizar-dash/src/web/components/chat/ConfirmModal.tsx +54 -0
  153. package/bizar-dash/src/web/components/chat/EmptyState.tsx +21 -0
  154. package/bizar-dash/src/web/components/chat/FirstRunGreeting.tsx +18 -0
  155. package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +32 -0
  156. package/bizar-dash/src/web/components/chat/InfoPanel.tsx +119 -0
  157. package/bizar-dash/src/web/components/chat/LoadingSkeleton.tsx +19 -0
  158. package/bizar-dash/src/web/components/chat/MessageBubble.tsx +60 -0
  159. package/bizar-dash/src/web/components/chat/SessionList.tsx +153 -0
  160. package/bizar-dash/src/web/components/chat/StreamingIndicator.tsx +9 -0
  161. package/bizar-dash/src/web/components/chat/SuggestionCards.tsx +55 -0
  162. package/bizar-dash/src/web/components/chat/WelcomeScreen.tsx +36 -0
  163. package/bizar-dash/src/web/components/chat/index.ts +20 -0
  164. package/bizar-dash/src/web/components/chat/useAutoGrowTextarea.ts +18 -0
  165. package/bizar-dash/src/web/components/chat/useChat.ts +219 -0
  166. package/bizar-dash/src/web/components/chat/useSlashCommands.ts +47 -0
  167. package/bizar-dash/src/web/index.html +37 -0
  168. package/bizar-dash/src/web/lib/api.ts +193 -0
  169. package/bizar-dash/src/web/lib/markdown.tsx +59 -0
  170. package/bizar-dash/src/web/lib/types.ts +668 -0
  171. package/bizar-dash/src/web/lib/utils.ts +114 -0
  172. package/bizar-dash/src/web/lib/ws.ts +176 -0
  173. package/bizar-dash/src/web/main.tsx +34 -0
  174. package/bizar-dash/src/web/mobile/MobileApp.tsx +278 -0
  175. package/bizar-dash/src/web/mobile/MobileBottomNav.tsx +38 -0
  176. package/bizar-dash/src/web/mobile/MobileTopbar.tsx +115 -0
  177. package/bizar-dash/src/web/mobile/components/MobileBottomSheet.tsx +182 -0
  178. package/bizar-dash/src/web/mobile/components/MobileListItem.tsx +64 -0
  179. package/bizar-dash/src/web/mobile/components/MobileModal.tsx +123 -0
  180. package/bizar-dash/src/web/mobile/views/MobileActivity.tsx +273 -0
  181. package/bizar-dash/src/web/mobile/views/MobileAgents.tsx +374 -0
  182. package/bizar-dash/src/web/mobile/views/MobileArtifactCanvas.tsx +439 -0
  183. package/bizar-dash/src/web/mobile/views/MobileArtifacts.tsx +143 -0
  184. package/bizar-dash/src/web/mobile/views/MobileChat.tsx +180 -0
  185. package/bizar-dash/src/web/mobile/views/MobileConfig.tsx +175 -0
  186. package/bizar-dash/src/web/mobile/views/MobileHistory.tsx +191 -0
  187. package/bizar-dash/src/web/mobile/views/MobileMods.tsx +149 -0
  188. package/bizar-dash/src/web/mobile/views/MobileMore.tsx +121 -0
  189. package/bizar-dash/src/web/mobile/views/MobileNotifications.tsx +137 -0
  190. package/bizar-dash/src/web/mobile/views/MobilePlans.tsx +26 -0
  191. package/bizar-dash/src/web/mobile/views/MobileSchedules.tsx +685 -0
  192. package/bizar-dash/src/web/mobile/views/MobileSearchModal.tsx +172 -0
  193. package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +388 -0
  194. package/bizar-dash/src/web/mobile/views/MobileSkills.tsx +153 -0
  195. package/bizar-dash/src/web/mobile/views/MobileTasks.tsx +564 -0
  196. package/bizar-dash/src/web/mobile.html +20 -0
  197. package/bizar-dash/src/web/mobile.tsx +13 -0
  198. package/bizar-dash/src/web/styles/chat.css +198 -0
  199. package/bizar-dash/src/web/styles/glyphs.css +960 -0
  200. package/bizar-dash/src/web/styles/main.css +8855 -0
  201. package/bizar-dash/src/web/styles/mobile-chat.css +5 -0
  202. package/bizar-dash/src/web/styles/mobile.css +1960 -0
  203. package/bizar-dash/src/web/views/Activity.tsx +1203 -0
  204. package/bizar-dash/src/web/views/Agents.tsx +701 -0
  205. package/bizar-dash/src/web/views/Artifacts.tsx +1480 -0
  206. package/bizar-dash/src/web/views/BackgroundAgents.tsx +450 -0
  207. package/bizar-dash/src/web/views/Chat.tsx +190 -0
  208. package/bizar-dash/src/web/views/Config.tsx +2063 -0
  209. package/bizar-dash/src/web/views/History.tsx +335 -0
  210. package/bizar-dash/src/web/views/ModView.tsx +207 -0
  211. package/bizar-dash/src/web/views/Mods.tsx +693 -0
  212. package/bizar-dash/src/web/views/Overview.tsx +812 -0
  213. package/bizar-dash/src/web/views/Providers.tsx +302 -0
  214. package/bizar-dash/src/web/views/Schedules.tsx +802 -0
  215. package/bizar-dash/src/web/views/Settings.tsx +1787 -0
  216. package/bizar-dash/src/web/views/Skills.tsx +431 -0
  217. package/bizar-dash/src/web/views/Tasks.tsx +1523 -0
  218. package/bizar-dash/src/web/views/glyphs/GlyphRenderer.tsx +613 -0
  219. package/bizar-dash/src/web/views/glyphs/components.tsx +1098 -0
  220. package/bizar-dash/templates/mod/FORMAT.md +76 -0
  221. package/bizar-dash/templates/mod/hello-mod/README.md +19 -0
  222. package/bizar-dash/templates/mod/hello-mod/agents/greeter.md +8 -0
  223. package/bizar-dash/templates/mod/hello-mod/commands/hello.md +6 -0
  224. package/bizar-dash/templates/mod/hello-mod/mod.json +20 -0
  225. package/bizar-dash/templates/mod/hello-mod/routes/ping.mjs +9 -0
  226. package/bizar-dash/templates/mod/hello-mod/views/HelloView.tsx +10 -0
  227. package/bizar-dash/tests/dashboard-ports.test.mjs +138 -0
  228. package/bizar-dash/tests/graphify-mod-spawn.node.test.mjs +90 -0
  229. package/bizar-dash/tests/memory-cli.test.mjs +542 -0
  230. package/bizar-dash/tests/memory-config.test.mjs +422 -0
  231. package/bizar-dash/tests/memory-git.test.mjs +246 -0
  232. package/bizar-dash/tests/memory-lightrag.test.mjs +153 -0
  233. package/bizar-dash/tests/memory-protocol-drift.test.mjs +45 -0
  234. package/bizar-dash/tests/memory-schema.test.mjs +198 -0
  235. package/bizar-dash/tests/memory-secrets.test.mjs +121 -0
  236. package/bizar-dash/tests/memory-store.test.mjs +390 -0
  237. package/bizar-dash/tests/memory-sync.test.mjs +141 -0
  238. package/bizar-dash/tests/mod-instructions.node.test.mjs +188 -0
  239. package/bizar-dash/tests/mod-security.test.mjs +273 -0
  240. package/bizar-dash/tests/mod-upgrade.node.test.mjs +357 -0
  241. package/bizar-dash/tests/no-agent-browser.node.test.mjs +98 -0
  242. package/bizar-dash/tests/obsidian-back-compat.test.mjs +199 -0
  243. package/bizar-dash/tests/opencode-runner.test.mjs +172 -0
  244. package/bizar-dash/tests/path-safe.test.mjs +108 -0
  245. package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +333 -0
  246. package/bizar-dash/tests/smoke-v2.mjs +212 -0
  247. package/bizar-dash/tests/strip-thinking.test.mjs +175 -0
  248. package/bizar-dash/tests/submit-feedback.test.mjs +353 -0
  249. package/bizar-dash/tests/tmux-wrap.test.mjs +145 -0
  250. package/bizar-dash/tests/yaml.test.mjs +128 -0
  251. package/bizar-dash/tsconfig.json +23 -0
  252. package/bizar-dash/vite.config.ts +27 -0
  253. package/cli/atomic.mjs +73 -0
  254. package/cli/banner.mjs +1 -1
  255. package/cli/bin.mjs +44 -47
  256. package/cli/bootstrap.mjs +1 -1
  257. package/cli/copy.mjs +25 -16
  258. package/cli/dev-link.mjs +17 -1
  259. package/cli/doctor.mjs +4 -4
  260. package/cli/doctor.test.mjs +2 -2
  261. package/cli/init.mjs +135 -1
  262. package/cli/install.mjs +31 -39
  263. package/cli/memory-constants.mjs +59 -0
  264. package/cli/memory.mjs +1436 -0
  265. package/cli/service.mjs +5 -6
  266. package/cli/utils.mjs +6 -3
  267. package/config/AGENTS.md +7 -7
  268. package/config/agents/_shared/AGENT_BASELINE.md +59 -61
  269. package/config/opencode.json +13 -38
  270. package/config/skills/memory-protocol/SKILL.md +105 -0
  271. package/config/skills/obsidian/SKILL.md +270 -65
  272. package/install.sh +59 -19
  273. package/package.json +49 -14
  274. package/packages/sdk/LICENSE +21 -0
  275. package/packages/sdk/README.md +80 -0
  276. package/packages/sdk/src/client.ts +183 -0
  277. package/packages/sdk/src/errors.ts +120 -0
  278. package/packages/sdk/src/events.ts +153 -0
  279. package/packages/sdk/src/index.ts +63 -0
  280. package/packages/sdk/src/types.ts +176 -0
  281. package/packages/sdk/src/version.ts +4 -0
  282. package/packages/sdk/tests/client.test.ts +217 -0
  283. package/packages/sdk/tests/errors.test.ts +108 -0
  284. package/packages/sdk/tests/events.test.ts +139 -0
  285. package/packages/sdk/tests/fixtures/fetch-mock.ts +152 -0
  286. package/packages/sdk/tests/fixtures/sse-mock.ts +30 -0
  287. package/packages/sdk/tsconfig.json +25 -0
  288. package/packages/sdk/vitest.config.ts +9 -0
  289. package/plugins/bizar/LICENSE +21 -0
  290. package/plugins/bizar/README.md +448 -0
  291. package/plugins/bizar/dist/index.js +29901 -0
  292. package/plugins/bizar/index.ts +1491 -0
  293. package/plugins/bizar/scripts/check-forbidden-imports.sh +33 -0
  294. package/plugins/bizar/src/background-state.ts +551 -0
  295. package/plugins/bizar/src/background.ts +1250 -0
  296. package/plugins/bizar/src/commands-impl.ts +464 -0
  297. package/plugins/bizar/src/commands.ts +1168 -0
  298. package/plugins/bizar/src/dashboard-client.ts +233 -0
  299. package/plugins/bizar/src/event-stream.ts +606 -0
  300. package/plugins/bizar/src/fingerprint.ts +120 -0
  301. package/plugins/bizar/src/handoff.ts +79 -0
  302. package/plugins/bizar/src/http-client.ts +467 -0
  303. package/plugins/bizar/src/key-rotation.ts +218 -0
  304. package/plugins/bizar/src/logger.ts +144 -0
  305. package/plugins/bizar/src/loop.ts +176 -0
  306. package/plugins/bizar/src/opencode-runner.ts +390 -0
  307. package/plugins/bizar/src/options.ts +421 -0
  308. package/plugins/bizar/src/plan-fs.ts +323 -0
  309. package/plugins/bizar/src/reasoning-clean.ts +454 -0
  310. package/plugins/bizar/src/report.ts +178 -0
  311. package/plugins/bizar/src/research-prompt.ts +35 -0
  312. package/plugins/bizar/src/serve-info.ts +228 -0
  313. package/plugins/bizar/src/serve.ts +496 -0
  314. package/plugins/bizar/src/settings.ts +349 -0
  315. package/plugins/bizar/src/state.ts +298 -0
  316. package/plugins/bizar/src/tools/bg-collect.ts +104 -0
  317. package/plugins/bizar/src/tools/bg-get-comments.ts +239 -0
  318. package/plugins/bizar/src/tools/bg-kill.ts +87 -0
  319. package/plugins/bizar/src/tools/bg-spawn.ts +401 -0
  320. package/plugins/bizar/src/tools/bg-status.ts +99 -0
  321. package/plugins/bizar/src/tools/open-kb.ts +191 -0
  322. package/plugins/bizar/src/tools/plan-action.ts +767 -0
  323. package/plugins/bizar/src/tools/read-glyph-feedback.ts +170 -0
  324. package/plugins/bizar/src/tools/wait-for-feedback.ts +402 -0
  325. package/plugins/bizar/tests/attach-handler-bug.test.ts +169 -0
  326. package/plugins/bizar/tests/background-state.test.ts +277 -0
  327. package/plugins/bizar/tests/background.test.ts +402 -0
  328. package/plugins/bizar/tests/block.test.ts +195 -0
  329. package/plugins/bizar/tests/canonical-key-order.test.ts +75 -0
  330. package/plugins/bizar/tests/commands-impl.test.ts +442 -0
  331. package/plugins/bizar/tests/commands.test.ts +584 -0
  332. package/plugins/bizar/tests/config.test.ts +122 -0
  333. package/plugins/bizar/tests/dashboard-client.test.ts +159 -0
  334. package/plugins/bizar/tests/dispose.test.ts +336 -0
  335. package/plugins/bizar/tests/event-stream.test.ts +409 -0
  336. package/plugins/bizar/tests/event.test.ts +262 -0
  337. package/plugins/bizar/tests/fingerprint.test.ts +162 -0
  338. package/plugins/bizar/tests/http-client.test.ts +404 -0
  339. package/plugins/bizar/tests/init-helpers.test.ts +203 -0
  340. package/plugins/bizar/tests/integration/slash-command.test.ts +348 -0
  341. package/plugins/bizar/tests/integration/tool-routing.test.ts +314 -0
  342. package/plugins/bizar/tests/key-rotation.test.ts +396 -0
  343. package/plugins/bizar/tests/loop.test.ts +397 -0
  344. package/plugins/bizar/tests/options.test.ts +276 -0
  345. package/plugins/bizar/tests/reasoning-clean.test.ts +422 -0
  346. package/plugins/bizar/tests/serve.test.ts +339 -0
  347. package/plugins/bizar/tests/settings.test.ts +351 -0
  348. package/plugins/bizar/tests/stall-think.test.ts +750 -0
  349. package/plugins/bizar/tests/state.test.ts +276 -0
  350. package/plugins/bizar/tests/tools/bg-collect.test.ts +337 -0
  351. package/plugins/bizar/tests/tools/bg-get-comments.test.ts +485 -0
  352. package/plugins/bizar/tests/tools/bg-kill.test.ts +235 -0
  353. package/plugins/bizar/tests/tools/bg-spawn-delegation.test.ts +147 -0
  354. package/plugins/bizar/tests/tools/bg-spawn.test.ts +311 -0
  355. package/plugins/bizar/tests/tools/bg-status.test.ts +217 -0
  356. package/plugins/bizar/tests/tools/opencode-runner.test.ts +115 -0
  357. package/plugins/bizar/tests/tools/plan-action.test.ts +599 -0
  358. package/plugins/bizar/tests/tools/read-glyph-feedback.test.ts +253 -0
  359. package/plugins/bizar/tests/tools/wait-for-feedback.test.ts +390 -0
  360. package/plugins/bizar/tests/update-deadlock.test.ts +145 -0
  361. package/plugins/bizar/tsconfig.json +29 -0
@@ -0,0 +1,574 @@
1
+ /**
2
+ * src/server/bg-retry.mjs
3
+ *
4
+ * v3.11.0 — Periodic recovery for background-agent instances stuck
5
+ * in `dispatchPending: true` with `toolCallCount === 0`.
6
+ *
7
+ * Why this exists:
8
+ * Before v3.11.0, the task delegator's
9
+ * `dispatchToBackground(main, subtasks, …)` short-circuited when
10
+ * `pingOpencodeServe()` returned false. The bg state file was
11
+ * written with `dispatchPending: true` and a tmux session was
12
+ * never created. The only recovery path was the user manually
13
+ * hitting `POST /api/tasks/:id/start` (or restarting the
14
+ * dashboard). The user's "home folder project" got stuck this way
15
+ * with 7 instances in `dispatchPending: true` — 6 from a Jun 19
16
+ * E2E test fixture and 1 active (`bgr_738FFSKMAT5SP58SVF5HQW`,
17
+ * "Install vLLM as a Python package").
18
+ *
19
+ * What this module does:
20
+ * - On a 30s timer, walk every bg state file under BG_DIRS.
21
+ * - For any instance whose `dispatchPending === true` AND
22
+ * `toolCallCount === 0` AND `startedAt` is older than the
23
+ * 30-second grace window, attempt to re-dispatch by:
24
+ * 1. Reading serve-info (the v3.11.0 fix makes this lenient).
25
+ * 2. POSTing `/api/session` to the opencode serve child.
26
+ * 3. POSTing `/api/session/{id}/prompt` with the recorded
27
+ * prompt.
28
+ * 4. Wrapping the agent run in a tmux session via
29
+ * `backgroundStore.spawnTmuxFor`.
30
+ * - On success, clear `dispatchPending`, persist the real
31
+ * `sessionId`, bump `lastActivityAt`, and write the file back
32
+ * atomically.
33
+ * - On failure, increment `retryCount`. After `MAX_DISPATCH_RETRIES`
34
+ * the instance is marked `status: "failed"` with
35
+ * `error: "exceeded max dispatch retries"` and the loop stops
36
+ * touching it.
37
+ *
38
+ * This module also exposes `retryDispatchOnce(instanceId)` for the
39
+ * `POST /api/background/:id/retry` manual endpoint.
40
+ *
41
+ * Lifecycle:
42
+ * - `startBgRetryLoop()` is idempotent — calling it twice is a
43
+ * no-op.
44
+ * - `stopBgRetryLoop()` clears the interval. Safe to call when
45
+ * the loop was never started.
46
+ *
47
+ * Out of scope:
48
+ * - tmux cleanup. The retry does not destroy the existing (likely
49
+ * non-existent) tmux session — `spawnTmuxFor` already handles
50
+ * pre-existing sessions via its `note: 'session already existed'`
51
+ * return path.
52
+ * - The opencode plugin's own bg state. The retry only writes the
53
+ * dashboard's view of the bg instance. The plugin will pick up
54
+ * the new dispatch on its next `GET /api/session` poll.
55
+ */
56
+
57
+ import {
58
+ existsSync,
59
+ readFileSync,
60
+ writeFileSync,
61
+ renameSync,
62
+ mkdirSync,
63
+ readdirSync,
64
+ } from 'node:fs';
65
+ import { join } from 'node:path';
66
+ import { homedir } from 'node:os';
67
+
68
+ import { backgroundStore } from './background-store.mjs';
69
+ import {
70
+ readServeInfo,
71
+ pingOpencodeServe,
72
+ createOpencodeSession,
73
+ sendOpencodePrompt,
74
+ } from './serve-info.mjs';
75
+ import { tasksStore } from './tasks-store.mjs';
76
+ import {
77
+ deriveAbsoluteBgLogPath,
78
+ isBrokenBgLogPath,
79
+ getActualBgLogPath,
80
+ } from './lib/path-safe.mjs';
81
+
82
+ const HOME = homedir();
83
+
84
+ // Mirrors background-store.mjs BG_DIRS + task-delegator.mjs BG_DIRS.
85
+ const BG_DIRS = [
86
+ join(HOME, '.cache', 'bizar', 'bg'),
87
+ join(HOME, '.config', 'opencode', 'bg'),
88
+ join(HOME, '.bizar', 'bg'),
89
+ ];
90
+
91
+ const RETRY_INTERVAL_MS = 30_000;
92
+ const DISPATCH_GRACE_MS = 30_000;
93
+ const MAX_DISPATCH_RETRIES = 10;
94
+
95
+ let intervalHandle = null;
96
+ let lastTickAt = 0;
97
+ let inFlightTick = false;
98
+
99
+ function pickBgDir() {
100
+ for (const dir of BG_DIRS) {
101
+ if (existsSync(dir)) return dir;
102
+ }
103
+ return BG_DIRS[0];
104
+ }
105
+
106
+ function readBgFile(file) {
107
+ try {
108
+ const raw = readFileSync(file, 'utf8');
109
+ const parsed = JSON.parse(raw);
110
+ return parsed && typeof parsed === 'object' ? parsed : null;
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ function atomicWriteJson(file, payload) {
117
+ const tmp = `${file}.tmp`;
118
+ const text = JSON.stringify(payload, null, 2);
119
+ writeFileSync(tmp, text, 'utf8');
120
+ try {
121
+ renameSync(tmp, file);
122
+ } catch {
123
+ writeFileSync(file, text, 'utf8');
124
+ }
125
+ }
126
+
127
+ function findBgFile(instanceId) {
128
+ for (const dir of BG_DIRS) {
129
+ if (!existsSync(dir)) continue;
130
+ const file = join(dir, `${instanceId}.json`);
131
+ if (existsSync(file)) return { file, dir };
132
+ }
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * Decide whether the bg instance qualifies for a retry tick.
138
+ *
139
+ * Filter (must satisfy all):
140
+ * - status is `pending` or `failed` (skip terminal `done`/`killed`)
141
+ * - `toolCallCount === 0` (the agent never started running)
142
+ * - `startedAt < (now - DISPATCH_GRACE_MS)` (give the original
143
+ * dispatch 30s to settle before we interfere)
144
+ * - `retryCount < MAX_DISPATCH_RETRIES` (we don't retry past cap)
145
+ * - The instance is "stuck": either `dispatchPending === true`,
146
+ * OR `sessionId` is missing/empty (a sentinel for "spawn never
147
+ * returned an opencode session"). The user's actual stuck
148
+ * `bgr_738FFSKMAT5SP58SVF5HQW.json` does NOT have a
149
+ * `dispatchPending` field — it was written by the opencode
150
+ * plugin itself, not by the dashboard's task-delegator, so
151
+ * its stuckness is encoded as `sessionId: ""` instead.
152
+ *
153
+ * @param {object} inst
154
+ * @param {number} now
155
+ * @returns {boolean}
156
+ */
157
+ export function shouldRetryDispatch(inst, now = Date.now()) {
158
+ if (!inst || typeof inst !== 'object') return false;
159
+ if ((inst.toolCallCount ?? 0) !== 0) return false;
160
+ const terminal = new Set(['done', 'killed', 'timed_out']);
161
+ if (terminal.has(inst.status)) return false;
162
+ if (inst.status !== 'pending' && inst.status !== 'failed') return false;
163
+ const retryCount = inst.retryCount ?? 0;
164
+ if (retryCount >= MAX_DISPATCH_RETRIES) return false;
165
+ const startedAt = typeof inst.startedAt === 'number' ? inst.startedAt : 0;
166
+ if (startedAt <= 0) return false;
167
+ if (now - startedAt < DISPATCH_GRACE_MS) return false;
168
+ // Stuckness detector — either the dashboard marked this as
169
+ // dispatchPending, OR the plugin wrote an empty sessionId
170
+ // (meaning the opencode session was never created).
171
+ const sessionId = inst.sessionId;
172
+ const emptySession = sessionId === null || sessionId === undefined || sessionId === '';
173
+ if (inst.dispatchPending !== true && !emptySession) return false;
174
+ return true;
175
+ }
176
+
177
+ /**
178
+ * Repair a bg instance's `logPath` if it is missing or broken
179
+ * (empty / not absolute / contains `//`). The plugin writes the
180
+ * logPath as `${worktree}/.opencode/log/<id>.log` — when worktree
181
+ * was missing, the resulting path becomes `//.opencode/log/<id>.log`
182
+ * and is unusable. We rebuild from a sane source: serve.json's
183
+ * `worktree` first, then the instance's existing worktree field,
184
+ * then `~/.cache/bizar/logs` as the last-resort fallback.
185
+ *
186
+ * @param {object} inst
187
+ * @returns {{ repaired: boolean, logPath: string }}
188
+ */
189
+ function repairLogPath(inst) {
190
+ const current = inst.logPath;
191
+ if (!isBrokenBgLogPath(current)) {
192
+ return { repaired: false, logPath: typeof current === 'string' ? current : '' };
193
+ }
194
+ let worktree = '';
195
+ try {
196
+ const serve = readServeInfo();
197
+ if (serve && typeof serve.worktree === 'string' && serve.worktree.length > 0) {
198
+ worktree = serve.worktree;
199
+ }
200
+ } catch {
201
+ /* ignore */
202
+ }
203
+ if (!worktree && typeof inst.worktree === 'string') {
204
+ worktree = inst.worktree;
205
+ }
206
+ const fixed = deriveAbsoluteBgLogPath(worktree, inst.instanceId || '');
207
+ return { repaired: true, logPath: fixed };
208
+ }
209
+
210
+ /**
211
+ * Re-run the dispatch path for a single bg instance. Mirrors the
212
+ * happy-path of `task-delegator.dispatchToBackground` but operates
213
+ * on a state file instead of a fresh subtask.
214
+ *
215
+ * Returns a structured result so callers (the periodic loop, the
216
+ * manual-retry endpoint) can render a useful message.
217
+ *
218
+ * @param {string} instanceId
219
+ * @returns {Promise<{
220
+ * ok: boolean,
221
+ * reason?: string,
222
+ * retryCount?: number,
223
+ * sessionId?: string,
224
+ * logPath?: string,
225
+ * }>}
226
+ */
227
+ export async function retryDispatchOnce(instanceId) {
228
+ const found = findBgFile(instanceId);
229
+ if (!found) {
230
+ return { ok: false, reason: 'instance_not_found' };
231
+ }
232
+ const inst = readBgFile(found.file);
233
+ if (!inst) {
234
+ return { ok: false, reason: 'corrupt_state_file' };
235
+ }
236
+
237
+ const retryCount = (inst.retryCount ?? 0) + 1;
238
+ if (retryCount > MAX_DISPATCH_RETRIES) {
239
+ const failed = {
240
+ ...inst,
241
+ status: 'failed',
242
+ dispatchPending: false,
243
+ error: 'exceeded max dispatch retries',
244
+ retryCount,
245
+ lastActivityAt: Date.now(),
246
+ };
247
+ atomicWriteJson(found.file, failed);
248
+ return { ok: false, reason: 'max_retries_exceeded', retryCount };
249
+ }
250
+
251
+ // v3.11.0 — Repair a broken logPath so the new run can write logs.
252
+ const { repaired, logPath } = repairLogPath(inst);
253
+
254
+ // v3.11.0 — Resolve serve-info. The relaxed schema means a partial
255
+ // `{password, pid, port}` file is now usable: we derive baseUrl
256
+ // from the port and treat missing worktree as empty.
257
+ const serveInfo = readServeInfo();
258
+ if (!serveInfo) {
259
+ // Serve not running yet — bump retryCount but leave dispatchPending.
260
+ const updated = {
261
+ ...inst,
262
+ retryCount,
263
+ lastRetryAt: Date.now(),
264
+ lastActivityAt: Date.now(),
265
+ ...(repaired ? { logPath } : {}),
266
+ retryError: 'serve-info unavailable',
267
+ };
268
+ atomicWriteJson(found.file, updated);
269
+ return { ok: false, reason: 'serve_unavailable', retryCount };
270
+ }
271
+
272
+ const reachable = await pingOpencodeServe(serveInfo);
273
+ if (!reachable) {
274
+ const updated = {
275
+ ...inst,
276
+ retryCount,
277
+ lastRetryAt: Date.now(),
278
+ lastActivityAt: Date.now(),
279
+ ...(repaired ? { logPath } : {}),
280
+ retryError: 'serve_unreachable',
281
+ };
282
+ atomicWriteJson(found.file, updated);
283
+ return { ok: false, reason: 'serve_unreachable', retryCount };
284
+ }
285
+
286
+ // Attempt the actual session creation + prompt.
287
+ let createRes;
288
+ let sendRes;
289
+ try {
290
+ createRes = await createOpencodeSession(
291
+ serveInfo,
292
+ {
293
+ title: inst.promptPreview || `bg: ${instanceId}`,
294
+ agent: inst.agent || 'tyr',
295
+ parentID: undefined,
296
+ },
297
+ serveInfo.worktree || inst.worktree || '',
298
+ );
299
+ } catch (err) {
300
+ const message = err instanceof Error ? err.message : String(err);
301
+ const updated = {
302
+ ...inst,
303
+ retryCount,
304
+ lastRetryAt: Date.now(),
305
+ lastActivityAt: Date.now(),
306
+ ...(repaired ? { logPath } : {}),
307
+ retryError: `createSession threw: ${message}`,
308
+ };
309
+ atomicWriteJson(found.file, updated);
310
+ return { ok: false, reason: 'create_threw', retryCount };
311
+ }
312
+
313
+ if (!createRes.ok) {
314
+ const updated = {
315
+ ...inst,
316
+ retryCount,
317
+ lastRetryAt: Date.now(),
318
+ lastActivityAt: Date.now(),
319
+ ...(repaired ? { logPath } : {}),
320
+ retryError: createRes.error || 'createSession failed',
321
+ };
322
+ atomicWriteJson(found.file, updated);
323
+ return { ok: false, reason: 'create_failed', retryCount };
324
+ }
325
+
326
+ const sessionId = createRes.sessionId;
327
+ try {
328
+ const promptText = buildReplayPromptText(inst);
329
+ sendRes = await sendOpencodePrompt(
330
+ serveInfo,
331
+ {
332
+ sessionId,
333
+ agent: inst.agent || 'tyr',
334
+ text: promptText,
335
+ },
336
+ serveInfo.worktree || inst.worktree || '',
337
+ );
338
+ } catch (err) {
339
+ const message = err instanceof Error ? err.message : String(err);
340
+ // Session was created but prompt failed. Mark the instance as
341
+ // running-with-sessionId so the operator can interact with it via
342
+ // opencode directly; the next retry tick will re-issue the prompt
343
+ // only if toolCallCount stays at 0.
344
+ const updated = {
345
+ ...inst,
346
+ sessionId,
347
+ retryCount,
348
+ lastRetryAt: Date.now(),
349
+ lastActivityAt: Date.now(),
350
+ status: 'running',
351
+ dispatchPending: false,
352
+ ...(repaired ? { logPath } : {}),
353
+ retryError: `sendPrompt threw: ${message}`,
354
+ };
355
+ atomicWriteJson(found.file, updated);
356
+ return { ok: false, reason: 'send_threw', retryCount, sessionId };
357
+ }
358
+
359
+ if (!sendRes.ok) {
360
+ const updated = {
361
+ ...inst,
362
+ sessionId,
363
+ retryCount,
364
+ lastRetryAt: Date.now(),
365
+ lastActivityAt: Date.now(),
366
+ status: 'running',
367
+ dispatchPending: false,
368
+ ...(repaired ? { logPath } : {}),
369
+ retryError: sendRes.error || 'sendPrompt failed',
370
+ };
371
+ atomicWriteJson(found.file, updated);
372
+ return { ok: false, reason: 'send_failed', retryCount, sessionId };
373
+ }
374
+
375
+ // Success path. Re-write the instance as `running`, clear
376
+ // dispatchPending, refresh activity timestamps, and wrap the agent
377
+ // run in a tmux session (best-effort, matches `task-delegator`).
378
+ const updated = {
379
+ ...inst,
380
+ sessionId,
381
+ status: 'running',
382
+ dispatchPending: false,
383
+ retryCount,
384
+ lastRetryAt: Date.now(),
385
+ lastActivityAt: Date.now(),
386
+ ...(repaired ? { logPath } : {}),
387
+ retryError: null,
388
+ };
389
+ atomicWriteJson(found.file, updated);
390
+
391
+ try {
392
+ const tmuxName = `bg_${(sessionId || instanceId).slice(0, 16)}`;
393
+ // v3.11.1 — Bug fix: the previous code tailed `${worktree}/.opencode/log/<id>.log`,
394
+ // a path nothing writes to. The plugin's `LogWriter` writes to
395
+ // `${logDir}/${sessionId}.log` (default `~/.cache/bizar/logs`).
396
+ // Use the real path so the operator sees activity.
397
+ const logFile = getActualBgLogPath({ sessionId: sessionId || instanceId });
398
+ backgroundStore.spawnTmuxFor(
399
+ tmuxName,
400
+ { command: 'tail', args: ['-n', '200', '-F', logFile] },
401
+ serveInfo.worktree || inst.worktree || undefined,
402
+ );
403
+ } catch {
404
+ /* best-effort */
405
+ }
406
+
407
+ // Also flip the linked task back to `doing` so the UI un-sticks.
408
+ try {
409
+ if (inst.taskId) {
410
+ await tasksStore.update(null, inst.taskId, {
411
+ status: 'doing',
412
+ metadata: {
413
+ ...(typeof inst.mainTaskId === 'string' ? {} : {}),
414
+ progress: 5,
415
+ currentStep: `Re-dispatched (${sessionId})`,
416
+ dispatchedAt: Date.now(),
417
+ },
418
+ });
419
+ }
420
+ } catch {
421
+ /* best-effort */
422
+ }
423
+
424
+ return { ok: true, sessionId, retryCount, logPath: repaired ? logPath : undefined };
425
+ }
426
+
427
+ /**
428
+ * Reconstruct the prompt text for the retry from what the bg file
429
+ * stored. The original `task-delegator.dispatchToBackground` builds
430
+ * a multi-line prompt (`# Title`, body, `---`, IDs); we approximate
431
+ * that here using the promptPreview + instanceId context. If the
432
+ * upstream caller stored the full prompt in `metadata.promptText`,
433
+ * use that; otherwise fall back to the preview.
434
+ *
435
+ * @param {object} inst
436
+ * @returns {string}
437
+ */
438
+ function buildReplayPromptText(inst) {
439
+ const full = inst?.metadata?.promptText;
440
+ if (typeof full === 'string' && full.trim().length > 0) return full;
441
+ const preview = typeof inst.promptPreview === 'string' ? inst.promptPreview : '';
442
+ const parts = [];
443
+ parts.push(`# ${preview || `Background task ${inst.instanceId}`}`);
444
+ parts.push('');
445
+ parts.push('(Re-dispatched by the bg-retry loop — full prompt was not');
446
+ parts.push('persisted. The dashboard recovered this instance after the');
447
+ parts.push('plugin was reachable. The original prompt preview is above;');
448
+ parts.push('if more context is required, inspect the linked task.)');
449
+ parts.push('');
450
+ parts.push('---');
451
+ parts.push(`Subtask ID: ${inst.taskId || '(unknown)'}`);
452
+ parts.push(`Assigned agent: ${inst.agent || 'tyr'}`);
453
+ parts.push(`Parent task ID: ${inst.mainTaskId || '(none)'}`);
454
+ parts.push(`Instance ID: ${inst.instanceId}`);
455
+ return parts.join('\n');
456
+ }
457
+
458
+ /**
459
+ * One pass of the retry loop. Walks every bg state file, identifies
460
+ * candidates via `shouldRetryDispatch`, and calls `retryDispatchOnce`
461
+ * for each. Failures are caught so a single broken file cannot
462
+ * poison the rest of the pass.
463
+ *
464
+ * @returns {Promise<{ scanned: number, retried: number, succeeded: number, failed: number, skipped: number }>}
465
+ */
466
+ export async function tickRetryLoop() {
467
+ if (inFlightTick) {
468
+ return { scanned: 0, retried: 0, succeeded: 0, failed: 0, skipped: 0 };
469
+ }
470
+ inFlightTick = true;
471
+ const summary = { scanned: 0, retried: 0, succeeded: 0, failed: 0, skipped: 0 };
472
+ try {
473
+ const now = Date.now();
474
+ for (const dir of BG_DIRS) {
475
+ if (!existsSync(dir)) continue;
476
+ let files;
477
+ try {
478
+ files = readdirSync(dir).filter((f) => f.endsWith('.json'));
479
+ } catch {
480
+ continue;
481
+ }
482
+ for (const f of files) {
483
+ summary.scanned += 1;
484
+ const file = join(dir, f);
485
+ const inst = readBgFile(file);
486
+ if (!inst) {
487
+ summary.skipped += 1;
488
+ continue;
489
+ }
490
+ if (!shouldRetryDispatch(inst, now)) {
491
+ summary.skipped += 1;
492
+ continue;
493
+ }
494
+ summary.retried += 1;
495
+ let result;
496
+ try {
497
+ result = await retryDispatchOnce(inst.instanceId || f.replace(/\.json$/, ''));
498
+ } catch (err) {
499
+ summary.failed += 1;
500
+ // eslint-disable-next-line no-console
501
+ console.warn(
502
+ `[bg-retry] retry for ${inst.instanceId || f} threw: ${err instanceof Error ? err.message : String(err)}`,
503
+ );
504
+ continue;
505
+ }
506
+ if (result.ok) {
507
+ summary.succeeded += 1;
508
+ // eslint-disable-next-line no-console
509
+ console.log(
510
+ `[bg-retry] recovered ${inst.instanceId} → sessionId=${result.sessionId}` +
511
+ (result.logPath ? ` (repaired logPath)` : ''),
512
+ );
513
+ } else {
514
+ summary.failed += 1;
515
+ // eslint-disable-next-line no-console
516
+ console.log(
517
+ `[bg-retry] ${inst.instanceId} retry #${result.retryCount ?? '?'} failed: ${result.reason}`,
518
+ );
519
+ }
520
+ }
521
+ }
522
+ return summary;
523
+ } finally {
524
+ inFlightTick = false;
525
+ lastTickAt = Date.now();
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Start the periodic retry loop. Idempotent.
531
+ *
532
+ * @param {object} [opts]
533
+ * @param {number} [opts.intervalMs] override the default 30s tick
534
+ */
535
+ export function startBgRetryLoop({ intervalMs } = {}) {
536
+ if (intervalHandle) return { ok: true, alreadyRunning: true };
537
+ const ms = typeof intervalMs === 'number' && intervalMs > 0 ? intervalMs : RETRY_INTERVAL_MS;
538
+ intervalHandle = setInterval(() => {
539
+ tickRetryLoop().catch((err) => {
540
+ // eslint-disable-next-line no-console
541
+ console.warn(`[bg-retry] tick threw: ${err instanceof Error ? err.message : String(err)}`);
542
+ });
543
+ }, ms);
544
+ if (typeof intervalHandle.unref === 'function') intervalHandle.unref();
545
+ // Kick off a first tick on the next event-loop turn so the
546
+ // dashboard recovers any pre-existing stuck instances immediately
547
+ // after a restart, without waiting 30s.
548
+ setImmediate(() => {
549
+ tickRetryLoop().catch(() => { /* logged inside */ });
550
+ });
551
+ // eslint-disable-next-line no-console
552
+ console.log(`[bg-retry] started (interval=${ms}ms, maxRetries=${MAX_DISPATCH_RETRIES})`);
553
+ return { ok: true, alreadyRunning: false, intervalMs: ms };
554
+ }
555
+
556
+ /**
557
+ * Stop the periodic retry loop. Safe to call when never started.
558
+ */
559
+ export function stopBgRetryLoop() {
560
+ if (intervalHandle) {
561
+ clearInterval(intervalHandle);
562
+ intervalHandle = null;
563
+ }
564
+ }
565
+
566
+ export const _BG_RETRY_INTERNAL = {
567
+ BG_DIRS,
568
+ RETRY_INTERVAL_MS,
569
+ DISPATCH_GRACE_MS,
570
+ MAX_DISPATCH_RETRIES,
571
+ };
572
+
573
+ // Silence unused-import warnings for helpers used transitively.
574
+ void mkdirSync;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * cli/dashboard/browser.mjs
3
+ *
4
+ * Cross-platform best-effort browser launcher. On failure (no graphical
5
+ * session, headless server), we just log the URL so the user can open it
6
+ * manually.
7
+ */
8
+ import { spawn } from 'node:child_process';
9
+
10
+ export async function launchBrowser(url) {
11
+ const platform = process.platform;
12
+ let cmd;
13
+ let args;
14
+
15
+ if (platform === 'darwin') {
16
+ cmd = 'open';
17
+ args = [url];
18
+ } else if (platform === 'win32') {
19
+ // Windows `start` is a shell builtin; spawn it via cmd.exe.
20
+ cmd = 'cmd';
21
+ args = ['/c', 'start', '""', url];
22
+ } else {
23
+ cmd = 'xdg-open';
24
+ args = [url];
25
+ }
26
+
27
+ try {
28
+ const child = spawn(cmd, args, {
29
+ detached: true,
30
+ stdio: 'ignore',
31
+ });
32
+ child.on('error', () => {
33
+ /* swallowed — best effort */
34
+ });
35
+ child.unref();
36
+ } catch (_err) {
37
+ // Browser launch failed — print URL for manual opening
38
+ console.log(`Open ${url} in your browser`);
39
+ }
40
+ }