@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,750 @@
1
+ /**
2
+ * stall-think.test.ts
3
+ *
4
+ * v0.3.0 stall timeout + thinking-loop protection tests.
5
+ *
6
+ * Groups:
7
+ * 1. researchInterventionPrompt() — 5 tests
8
+ * 2. normalizeOptions() v0.3.0 fields — 6 tests
9
+ * 3. BackgroundState schema backfill — 3 tests
10
+ * 4. Stall + thinking-loop detection logic — 7 tests
11
+ * 5. bg-status toView v0.3.0 fields — 2 tests
12
+ *
13
+ * Total: 23 tests
14
+ */
15
+
16
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
17
+ import { writeFileSync, mkdirSync, unlinkSync, rmSync } from "node:fs";
18
+ import path from "node:path";
19
+ import os from "node:os";
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Group 1 — researchInterventionPrompt
23
+ // ---------------------------------------------------------------------------
24
+
25
+ import { researchInterventionPrompt } from "../src/research-prompt.js";
26
+
27
+ describe("researchInterventionPrompt", () => {
28
+ it("returns a string containing '[SYSTEM REMINDER — Thinking Loop Detected]'", () => {
29
+ const result = researchInterventionPrompt(60_000);
30
+ expect(result).toContain("[SYSTEM REMINDER — Thinking Loop Detected]");
31
+ });
32
+
33
+ it("includes the duration formatted as 'Xm Ys' for multi-minute durations", () => {
34
+ const result = researchInterventionPrompt(330_000); // 5m 30s
35
+ expect(result).toContain("5m 30s");
36
+ });
37
+
38
+ it("formats under-a-minute durations as 'Ys' (not '0m Ys')", () => {
39
+ const result = researchInterventionPrompt(45_000); // 45s
40
+ expect(result).toContain("45s");
41
+ // Must NOT contain "0m" in the duration portion
42
+ expect(result).not.toMatch(/0m \d+s/);
43
+ });
44
+
45
+ it("clamps negative or zero duration to 0s", () => {
46
+ const neg = researchInterventionPrompt(-99_000);
47
+ const zero = researchInterventionPrompt(0);
48
+ // Both should produce "0s" (Math.max(0, …) in the function)
49
+ expect(neg).toContain("0s");
50
+ expect(zero).toContain("0s");
51
+ });
52
+
53
+ it("mentions 'task tool', 'Mimir', and 'bash' — the three action options", () => {
54
+ const result = researchInterventionPrompt(60_000);
55
+ expect(result).toContain("task tool");
56
+ expect(result).toContain("Mimir");
57
+ expect(result).toContain("bash");
58
+ });
59
+ });
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Group 2 — normalizeOptions v0.3.0 fields
63
+ // ---------------------------------------------------------------------------
64
+
65
+ import { normalizeOptions } from "../src/options.js";
66
+
67
+ describe("normalizeOptions — v0.3.0 fields", () => {
68
+ it("defaults: backgroundStallTimeoutMs === 180_000, backgroundThinkingLoopTimeoutMs === 300_000, backgroundMaxInterventions === 1", () => {
69
+ const { options } = normalizeOptions(undefined);
70
+ expect(options.backgroundStallTimeoutMs).toBe(180_000);
71
+ expect(options.backgroundThinkingLoopTimeoutMs).toBe(300_000);
72
+ expect(options.backgroundMaxInterventions).toBe(1);
73
+ });
74
+
75
+ it("clamps backgroundStallTimeoutMs < 10000 to 10000 and pushes a note", () => {
76
+ const { options, notes } = normalizeOptions({ backgroundStallTimeoutMs: 999 });
77
+ expect(options.backgroundStallTimeoutMs).toBe(10_000);
78
+ expect(notes.some((n) => n.includes("backgroundStallTimeoutMs") && n.includes("clamped"))).toBe(true);
79
+ });
80
+
81
+ it("clamps backgroundStallTimeoutMs > 600000 to 600000 and pushes a note", () => {
82
+ const { options, notes } = normalizeOptions({ backgroundStallTimeoutMs: 999_999 });
83
+ expect(options.backgroundStallTimeoutMs).toBe(600_000);
84
+ expect(notes.some((n) => n.includes("backgroundStallTimeoutMs") && n.includes("clamped"))).toBe(true);
85
+ });
86
+
87
+ it("clamps backgroundMaxInterventions < 1 to 1", () => {
88
+ const { options, notes } = normalizeOptions({ backgroundMaxInterventions: 0 });
89
+ expect(options.backgroundMaxInterventions).toBe(1);
90
+ expect(notes.some((n) => n.includes("backgroundMaxInterventions") && n.includes("clamped"))).toBe(true);
91
+ });
92
+
93
+ it("clamps backgroundMaxInterventions > 3 to 3", () => {
94
+ const { options, notes } = normalizeOptions({ backgroundMaxInterventions: 99 });
95
+ expect(options.backgroundMaxInterventions).toBe(3);
96
+ expect(notes.some((n) => n.includes("backgroundMaxInterventions") && n.includes("clamped"))).toBe(true);
97
+ });
98
+
99
+ it("honors BIZAR_STALL_TIMEOUT_MS env var when no option is set", async () => {
100
+ // Set the env var before importing/evaluating normalizeOptions.
101
+ // Static imports are hoisted and evaluated before test code runs,
102
+ // so we must use dynamic import AFTER setting the env var.
103
+ process.env.BIZAR_STALL_TIMEOUT_MS = "42000";
104
+ try {
105
+ // Dynamically import AFTER the env var is set so the module's
106
+ // function closure picks up the updated process.env at evaluation time.
107
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
108
+ const mod = await import("../src/options.js") as any;
109
+ const { options } = mod.normalizeOptions(undefined);
110
+ expect(options.backgroundStallTimeoutMs).toBe(42000);
111
+ } finally {
112
+ delete process.env.BIZAR_STALL_TIMEOUT_MS;
113
+ }
114
+ });
115
+ });
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Group 3 — BackgroundState schema backfill
119
+ // ---------------------------------------------------------------------------
120
+
121
+ import { BackgroundStateStore } from "../src/background-state.js";
122
+ import { TERMINAL_STATUSES } from "../src/background-state.js";
123
+
124
+ // Minimal Logger for tests
125
+ const silentLogger = {
126
+ log(_opts: { level: "debug" | "info" | "warn" | "error"; message: string }) {},
127
+ debug(_m: string) {},
128
+ info(_m: string) {},
129
+ warn(_m: string) {},
130
+ error(_m: string) {},
131
+ };
132
+
133
+ function makeTempDir(prefix: string): string {
134
+ const dir = path.join(os.tmpdir(), `bizar-stall-test-${prefix}-${process.pid}`);
135
+ mkdirSync(dir, { recursive: true });
136
+ return dir;
137
+ }
138
+
139
+ function writeOldStateFile(dir: string, instanceId: string, state: Record<string, unknown>): void {
140
+ const bgDir = path.join(dir, "bg");
141
+ mkdirSync(bgDir, { recursive: true });
142
+ writeFileSync(path.join(bgDir, `${instanceId}.json`), JSON.stringify(state), "utf8");
143
+ }
144
+
145
+ describe("BackgroundState schema backfill", () => {
146
+ let tmpDir: string;
147
+
148
+ beforeEach(() => {
149
+ tmpDir = makeTempDir("bg-state-backfill");
150
+ });
151
+
152
+ afterEach(() => {
153
+ rmSync(tmpDir, { recursive: true, force: true });
154
+ });
155
+
156
+ it("loads an old-format state file (no lastEventAt) and backfills it to startedAt", async () => {
157
+ const instanceId = "bgr_old_no_lastEventAt";
158
+ const startedAt = 1_700_000_000_000;
159
+ writeOldStateFile(tmpDir, instanceId, {
160
+ instanceId,
161
+ sessionId: "sess_abc",
162
+ agent: "mimir",
163
+ status: "running",
164
+ startedAt,
165
+ model: "minimax/minimax-m3",
166
+ promptPreview: "Do the thing",
167
+ toolCallCount: 0,
168
+ parentAgent: "odin",
169
+ logPath: path.join(os.tmpdir(), "test.log"),
170
+ timeoutMs: 300_000,
171
+ // lastEventAt is intentionally absent
172
+ lastToolOrTextAt: startedAt,
173
+ interventionCount: 0,
174
+ });
175
+
176
+ const store = new BackgroundStateStore(tmpDir, silentLogger);
177
+ const loaded = await store.load(instanceId);
178
+
179
+ expect(loaded).not.toBeNull();
180
+ expect(loaded!.lastEventAt).toBe(startedAt);
181
+ });
182
+
183
+ it("loads an old-format state file (no lastToolOrTextAt) and backfills it to startedAt", async () => {
184
+ const instanceId = "bgr_old_no_lastToolOrTextAt";
185
+ const startedAt = 1_700_000_000_000;
186
+ writeOldStateFile(tmpDir, instanceId, {
187
+ instanceId,
188
+ sessionId: "sess_def",
189
+ agent: "mimir",
190
+ status: "running",
191
+ startedAt,
192
+ model: "minimax/minimax-m3",
193
+ promptPreview: "Do the thing",
194
+ toolCallCount: 0,
195
+ parentAgent: "odin",
196
+ logPath: path.join(os.tmpdir(), "test.log"),
197
+ timeoutMs: 300_000,
198
+ lastEventAt: startedAt,
199
+ // lastToolOrTextAt is intentionally absent
200
+ interventionCount: 0,
201
+ });
202
+
203
+ const store = new BackgroundStateStore(tmpDir, silentLogger);
204
+ const loaded = await store.load(instanceId);
205
+
206
+ expect(loaded).not.toBeNull();
207
+ expect(loaded!.lastToolOrTextAt).toBe(startedAt);
208
+ });
209
+
210
+ it("loads an old-format state file (no interventionCount) and backfills it to 0", async () => {
211
+ const instanceId = "bgr_old_no_interventionCount";
212
+ const startedAt = 1_700_000_000_000;
213
+ writeOldStateFile(tmpDir, instanceId, {
214
+ instanceId,
215
+ sessionId: "sess_ghi",
216
+ agent: "mimir",
217
+ status: "running",
218
+ startedAt,
219
+ model: "minimax/minimax-m3",
220
+ promptPreview: "Do the thing",
221
+ toolCallCount: 0,
222
+ parentAgent: "odin",
223
+ logPath: path.join(os.tmpdir(), "test.log"),
224
+ timeoutMs: 300_000,
225
+ lastEventAt: startedAt,
226
+ lastToolOrTextAt: startedAt,
227
+ // interventionCount is intentionally absent
228
+ });
229
+
230
+ const store = new BackgroundStateStore(tmpDir, silentLogger);
231
+ const loaded = await store.load(instanceId);
232
+
233
+ expect(loaded).not.toBeNull();
234
+ expect(loaded!.interventionCount).toBe(0);
235
+ });
236
+ });
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // Group 4 — Stall + thinking-loop detection logic
240
+ //
241
+ // FakeInstanceManager replicates the v0.3.0 stall and thinking-loop
242
+ // detection algorithm from the real InstanceManager. It mirrors the
243
+ // real class surface and implements the exact same logic.
244
+ // ---------------------------------------------------------------------------
245
+
246
+ import type { BackgroundState, BackgroundStatus } from "../src/background-state.js";
247
+ import { TERMINAL_STATUSES as TERMINAL } from "../src/background-state.js";
248
+
249
+ /** Format duration as the checker does internally. */
250
+ function formatDuration(ms: number): string {
251
+ const safeMs = Math.max(0, Math.floor(ms));
252
+ const minutes = Math.floor(safeMs / 60_000);
253
+ const seconds = Math.floor((safeMs % 60_000) / 1000);
254
+ return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
255
+ }
256
+
257
+ /** Minimal Logger for tests */
258
+ const noopLogger = {
259
+ log(_opts: { level: "debug" | "info" | "warn" | "error"; message: string }) {},
260
+ debug(_m: string) {},
261
+ info(_m: string) {},
262
+ warn(_m: string) {},
263
+ error(_m: string) {},
264
+ };
265
+
266
+ /**
267
+ * FakeInstanceManager — mirrors the v0.3.0 InstanceManager surface with
268
+ * full stall and thinking-loop detection logic. Uses in-memory state only
269
+ * (no HTTP, no EventStream, no serve child). Exposes sentPrompts and
270
+ * abortedSessions for test assertions.
271
+ */
272
+ class FakeInstanceManagerForStall {
273
+ /** Exposed for test assertions */
274
+ sentPrompts: Array<{ sessionId: string; text: string }> = [];
275
+ abortedSessions: string[] = [];
276
+
277
+ private instances = new Map<string, BackgroundState>();
278
+ private stallTimeoutMs: number;
279
+ private thinkingLoopTimeoutMs: number;
280
+ private maxInterventions: number;
281
+ private stallCheckerDisabled = false;
282
+
283
+ constructor(opts: {
284
+ stallTimeoutMs?: number;
285
+ thinkingLoopTimeoutMs?: number;
286
+ maxInterventions?: number;
287
+ } = {}) {
288
+ this.stallTimeoutMs = opts.stallTimeoutMs ?? 180_000;
289
+ this.thinkingLoopTimeoutMs = opts.thinkingLoopTimeoutMs ?? 300_000;
290
+ this.maxInterventions = opts.maxInterventions ?? 1;
291
+ }
292
+
293
+ get stallTimeoutMsValue(): number {
294
+ return this.stallTimeoutMs;
295
+ }
296
+
297
+ get thinkingLoopTimeoutMsValue(): number {
298
+ return this.thinkingLoopTimeoutMs;
299
+ }
300
+
301
+ get maxInterventionsValue(): number {
302
+ return this.maxInterventions;
303
+ }
304
+
305
+ disablePeriodicChecks(): void {
306
+ this.stallCheckerDisabled = true;
307
+ }
308
+
309
+ // ---------------------------------------------------------------------------
310
+ // Fake public API used by tests to set up instance state
311
+ // ---------------------------------------------------------------------------
312
+
313
+ /** Add a fake instance directly to the in-memory map */
314
+ addInstance(state: BackgroundState): void {
315
+ this.instances.set(state.instanceId, { ...state });
316
+ }
317
+
318
+ getInstance(instanceId: string): BackgroundState | undefined {
319
+ return this.instances.get(instanceId);
320
+ }
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // runStallAndLoopChecks — exact replica of InstanceManager.runStallAndLoopChecks
324
+ // ---------------------------------------------------------------------------
325
+
326
+ async runStallAndLoopChecks(): Promise<void> {
327
+ if (this.stallCheckerDisabled) return;
328
+
329
+ const ids: string[] = [];
330
+ for (const inst of this.instances.values()) {
331
+ if (TERMINAL.has(inst.status)) continue;
332
+ ids.push(inst.instanceId);
333
+ }
334
+
335
+ for (const id of ids) {
336
+ const inst = this.instances.get(id);
337
+ if (!inst || TERMINAL.has(inst.status)) continue;
338
+
339
+ const now = Date.now();
340
+ const lastEventAt = inst.lastEventAt ?? 0;
341
+ const lastToolOrTextAt = inst.lastToolOrTextAt ?? 0;
342
+
343
+ // Stall check
344
+ if (now - lastEventAt > this.stallTimeoutMs) {
345
+ await this._abortAsStalled(inst);
346
+ continue;
347
+ }
348
+
349
+ // Thinking-loop check — only for running instances
350
+ if (inst.status === "running") {
351
+ const since = now - lastToolOrTextAt;
352
+ if (since > this.thinkingLoopTimeoutMs) {
353
+ const currentCount = inst.interventionCount ?? 0;
354
+ if (currentCount < this.maxInterventions) {
355
+ await this._sendIntervention(inst, since);
356
+ } else {
357
+ await this._abortAsThinkingLoop(inst, since);
358
+ }
359
+ }
360
+ }
361
+ }
362
+ }
363
+
364
+ // ---------------------------------------------------------------------------
365
+ // simulatePartUpdated — replicates InstanceManager.onPartUpdated
366
+ // ---------------------------------------------------------------------------
367
+
368
+ simulatePartUpdated(
369
+ instanceId: string,
370
+ partType: "tool" | "text" | "thinking",
371
+ ): void {
372
+ const inst = this.instances.get(instanceId);
373
+ if (!inst) return;
374
+
375
+ // Every event advances lastEventAt (heartbeat)
376
+ inst.lastEventAt = Date.now();
377
+
378
+ if (partType === "tool" || partType === "text") {
379
+ inst.lastToolOrTextAt = Date.now();
380
+ // Reset intervention counter after progress
381
+ if ((inst.interventionCount ?? 0) > 0) {
382
+ inst.interventionCount = 0;
383
+ delete inst.interventionAt;
384
+ delete inst.interventionReason;
385
+ }
386
+ }
387
+ // 'thinking' parts do NOT advance lastToolOrTextAt — that is intentional
388
+ }
389
+
390
+ // ---------------------------------------------------------------------------
391
+ // Private helpers — exact replicas of InstanceManager private methods
392
+ // ---------------------------------------------------------------------------
393
+
394
+ private async _abortAsStalled(inst: BackgroundState): Promise<void> {
395
+ const lastEventAt = inst.lastEventAt ?? 0;
396
+ const sinceMs = Date.now() - lastEventAt;
397
+ noopLogger.warn(
398
+ `bizar: instance ${inst.instanceId} stalled (no event for ${sinceMs}ms); aborting`,
399
+ );
400
+ this.abortedSessions.push(inst.sessionId);
401
+ inst.status = "failed";
402
+ inst.error = `No activity for ${this.stallTimeoutMs}ms — LLM appears stalled`;
403
+ inst.completedAt = Date.now();
404
+ }
405
+
406
+ private async _sendIntervention(inst: BackgroundState, sinceMs: number): Promise<void> {
407
+ const prompt = researchInterventionPrompt(sinceMs);
408
+ const currentCount = inst.interventionCount ?? 0;
409
+ noopLogger.warn(
410
+ `bizar: instance ${inst.instanceId} thinking loop (${sinceMs}ms without tool/text); sending intervention #${currentCount + 1}/${this.maxInterventions}`,
411
+ );
412
+ this.sentPrompts.push({ sessionId: inst.sessionId, text: prompt });
413
+ const reason = `thinking loop (${formatDuration(sinceMs)} without tool/text)`;
414
+ inst.interventionCount = currentCount + 1;
415
+ inst.interventionAt = Date.now();
416
+ inst.interventionReason = reason;
417
+ // Bumping lastEventAt here is intentional (mirrors real impl)
418
+ inst.lastEventAt = Date.now();
419
+ }
420
+
421
+ private async _abortAsThinkingLoop(inst: BackgroundState, sinceMs: number): Promise<void> {
422
+ noopLogger.warn(
423
+ `bizar: instance ${inst.instanceId} thinking loop exhausted ${this.maxInterventions} intervention(s) over ${sinceMs}ms; aborting`,
424
+ );
425
+ this.abortedSessions.push(inst.sessionId);
426
+ inst.status = "failed";
427
+ inst.error = `Thinking loop detected: ${formatDuration(sinceMs)} of thinking without tool calls or output. Spawn a Mimir agent for research.`;
428
+ inst.completedAt = Date.now();
429
+ }
430
+ }
431
+
432
+ function makeBgState(overrides: Partial<BackgroundState> = {}): BackgroundState {
433
+ const now = Date.now();
434
+ return {
435
+ instanceId: `bgr_${Math.random().toString(36).slice(2, 10)}`,
436
+ sessionId: `sess_${Math.random().toString(36).slice(2, 10)}`,
437
+ agent: "mimir",
438
+ status: "running",
439
+ startedAt: now,
440
+ model: "minimax/minimax-m3",
441
+ promptPreview: "Do the thing",
442
+ resultPreview: undefined,
443
+ resultMessageIds: [],
444
+ error: undefined,
445
+ parentAgent: "odin",
446
+ parentInstanceId: undefined,
447
+ logPath: "~/.cache/bizar/logs/test.log",
448
+ timeoutMs: 300_000,
449
+ toolCallCount: 0,
450
+ loopGuardTool: undefined,
451
+ lastEventAt: now,
452
+ lastToolOrTextAt: now,
453
+ interventionCount: 0,
454
+ ...overrides,
455
+ };
456
+ }
457
+
458
+ describe("Stall + thinking-loop detection logic", () => {
459
+ // Test 1: Stall timeout fires when no events
460
+ it("stall timeout fires when no events (lastEventAt is old)", async () => {
461
+ const mgr = new FakeInstanceManagerForStall({
462
+ stallTimeoutMs: 180_000,
463
+ thinkingLoopTimeoutMs: 300_000,
464
+ maxInterventions: 1,
465
+ });
466
+
467
+ const oldTime = Date.now() - (180_000 + 1000); // past the stall threshold
468
+ mgr.addInstance(
469
+ makeBgState({
470
+ instanceId: "bgr_stall_fire",
471
+ status: "running",
472
+ lastEventAt: oldTime,
473
+ lastToolOrTextAt: oldTime,
474
+ }),
475
+ );
476
+
477
+ await mgr.runStallAndLoopChecks();
478
+
479
+ expect(mgr.abortedSessions).toContain(
480
+ mgr.getInstance("bgr_stall_fire")!.sessionId,
481
+ );
482
+ const inst = mgr.getInstance("bgr_stall_fire")!;
483
+ expect(inst.status).toBe("failed");
484
+ expect(inst.error!).toContain("No activity for");
485
+ });
486
+
487
+ // Test 2: Stall timeout does NOT fire when events are recent
488
+ it("stall timeout does NOT fire when events are recent", async () => {
489
+ const mgr = new FakeInstanceManagerForStall({ stallTimeoutMs: 180_000 });
490
+
491
+ const recentTime = Date.now();
492
+ mgr.addInstance(
493
+ makeBgState({
494
+ instanceId: "bgr_stall_recent",
495
+ status: "running",
496
+ lastEventAt: recentTime,
497
+ lastToolOrTextAt: recentTime,
498
+ }),
499
+ );
500
+
501
+ await mgr.runStallAndLoopChecks();
502
+
503
+ expect(mgr.abortedSessions).toHaveLength(0);
504
+ const inst = mgr.getInstance("bgr_stall_recent")!;
505
+ expect(inst.status).toBe("running");
506
+ });
507
+
508
+ // Test 3: Stall timeout does NOT fire for terminal instances
509
+ it("stall timeout does NOT fire for terminal instances", async () => {
510
+ const mgr = new FakeInstanceManagerForStall({ stallTimeoutMs: 180_000 });
511
+
512
+ const oldTime = Date.now() - (180_000 + 1000);
513
+ mgr.addInstance(
514
+ makeBgState({
515
+ instanceId: "bgr_stall_terminal",
516
+ status: "done",
517
+ lastEventAt: oldTime, // would fire if not terminal
518
+ lastToolOrTextAt: oldTime,
519
+ }),
520
+ );
521
+
522
+ await mgr.runStallAndLoopChecks();
523
+
524
+ expect(mgr.abortedSessions).toHaveLength(0);
525
+ const inst = mgr.getInstance("bgr_stall_terminal")!;
526
+ expect(inst.status).toBe("done");
527
+ });
528
+
529
+ // Test 4: Thinking loop detection fires after threshold
530
+ it("thinking loop detection fires after threshold, sends intervention, increments counter", async () => {
531
+ const mgr = new FakeInstanceManagerForStall({
532
+ stallTimeoutMs: 180_000,
533
+ thinkingLoopTimeoutMs: 300_000,
534
+ maxInterventions: 1,
535
+ });
536
+
537
+ // lastEventAt must be within stall timeout so we reach the thinking-loop
538
+ // check. lastToolOrTextAt must be beyond the thinking-loop threshold.
539
+ const now = Date.now();
540
+ const lastEventAt = now - 60_000; // 60s ago — within 180s stall timeout
541
+ const lastToolOrTextAt = now - 301_000; // 301s ago — beyond 300s loop threshold
542
+
543
+ mgr.addInstance(
544
+ makeBgState({
545
+ instanceId: "bgr_thinking_loop",
546
+ status: "running",
547
+ lastEventAt,
548
+ lastToolOrTextAt,
549
+ interventionCount: 0,
550
+ }),
551
+ );
552
+
553
+ await mgr.runStallAndLoopChecks();
554
+
555
+ expect(mgr.sentPrompts).toHaveLength(1);
556
+ expect(mgr.sentPrompts[0]!.text).toContain("[SYSTEM REMINDER — Thinking Loop Detected]");
557
+
558
+ const inst = mgr.getInstance("bgr_thinking_loop")!;
559
+ expect(inst.interventionCount).toBe(1);
560
+ expect(inst.interventionAt).toBeDefined();
561
+ expect(inst.interventionReason).toContain("thinking loop");
562
+ expect(inst.status).toBe("running"); // still running — not failed yet
563
+ });
564
+
565
+ // Test 5: Thinking loop intervention respects maxInterventions
566
+ it("when interventionCount >= maxInterventions, instance is aborted as thinking loop", async () => {
567
+ const mgr = new FakeInstanceManagerForStall({
568
+ stallTimeoutMs: 180_000,
569
+ thinkingLoopTimeoutMs: 300_000,
570
+ maxInterventions: 1,
571
+ });
572
+
573
+ // lastEventAt recent (within stall timeout) so we reach the thinking-loop check
574
+ const now = Date.now();
575
+ const lastEventAt = now - 60_000; // 60s ago — within 180s stall timeout
576
+ const lastToolOrTextAt = now - 301_000; // 301s ago — beyond 300s loop threshold
577
+
578
+ mgr.addInstance(
579
+ makeBgState({
580
+ instanceId: "bgr_max_interventions",
581
+ status: "running",
582
+ lastEventAt,
583
+ lastToolOrTextAt,
584
+ // Already at max interventions — should abort immediately without sending prompt
585
+ interventionCount: 1,
586
+ }),
587
+ );
588
+
589
+ await mgr.runStallAndLoopChecks();
590
+
591
+ expect(mgr.sentPrompts).toHaveLength(0); // No new prompt sent
592
+ expect(mgr.abortedSessions).toContain(
593
+ mgr.getInstance("bgr_max_interventions")!.sessionId,
594
+ );
595
+ const inst = mgr.getInstance("bgr_max_interventions")!;
596
+ expect(inst.status).toBe("failed");
597
+ expect(inst.error!).toMatch(/thinking loop/i); // case-insensitive
598
+ });
599
+
600
+ // Test 6: Tool/text events reset intervention counter
601
+ it("a tool or text part after intervention resets interventionCount to 0", async () => {
602
+ const mgr = new FakeInstanceManagerForStall({
603
+ stallTimeoutMs: 180_000,
604
+ thinkingLoopTimeoutMs: 300_000,
605
+ maxInterventions: 1,
606
+ });
607
+
608
+ const oldTime = Date.now() - (300_000 + 1000);
609
+ mgr.addInstance(
610
+ makeBgState({
611
+ instanceId: "bgr_reset_counter",
612
+ status: "running",
613
+ lastEventAt: oldTime,
614
+ lastToolOrTextAt: oldTime,
615
+ interventionCount: 1,
616
+ interventionAt: oldTime,
617
+ interventionReason: "thinking loop (1m 0s without tool/text)",
618
+ }),
619
+ );
620
+
621
+ // Simulate a text part arriving (progress signal)
622
+ mgr.simulatePartUpdated("bgr_reset_counter", "text");
623
+
624
+ const inst = mgr.getInstance("bgr_reset_counter")!;
625
+ expect(inst.interventionCount).toBe(0);
626
+ expect(inst.interventionAt).toBeUndefined();
627
+ expect(inst.interventionReason).toBeUndefined();
628
+ expect(inst.lastToolOrTextAt).toBeGreaterThanOrEqual(oldTime);
629
+ });
630
+
631
+ // Test 7: Thinking-only events do NOT reset intervention counter
632
+ it("a thinking part does NOT update lastToolOrTextAt or reset interventionCount", async () => {
633
+ const mgr = new FakeInstanceManagerForStall({
634
+ stallTimeoutMs: 180_000,
635
+ thinkingLoopTimeoutMs: 300_000,
636
+ maxInterventions: 1,
637
+ });
638
+
639
+ const baseTime = Date.now() - 60_000;
640
+ mgr.addInstance(
641
+ makeBgState({
642
+ instanceId: "bgr_thinking_only",
643
+ status: "running",
644
+ lastEventAt: baseTime,
645
+ lastToolOrTextAt: baseTime, // set to 60s ago
646
+ interventionCount: 0,
647
+ }),
648
+ );
649
+
650
+ // Simulate a thinking part arriving
651
+ mgr.simulatePartUpdated("bgr_thinking_only", "thinking");
652
+
653
+ const inst = mgr.getInstance("bgr_thinking_only")!;
654
+ // lastToolOrTextAt should NOT have been updated by the thinking part
655
+ expect(inst.lastToolOrTextAt).toBe(baseTime);
656
+ // interventionCount stays at 0 (was already 0 in this case)
657
+ expect(inst.interventionCount).toBe(0);
658
+ });
659
+ });
660
+
661
+ // ---------------------------------------------------------------------------
662
+ // Group 5 — bg-status toView includes v0.3.0 fields
663
+ // ---------------------------------------------------------------------------
664
+
665
+ import type { InstanceView } from "../src/background.js";
666
+
667
+ function toViewForTest(inst: import("../src/background-state.js").BackgroundState): InstanceView {
668
+ const v: InstanceView = {
669
+ instanceId: inst.instanceId,
670
+ agent: inst.agent,
671
+ status: inst.status,
672
+ startedAt: inst.startedAt,
673
+ toolCallCount: inst.toolCallCount,
674
+ promptPreview: inst.promptPreview,
675
+ parentAgent: inst.parentAgent,
676
+ sessionId: inst.sessionId,
677
+ lastEventAt: inst.lastEventAt,
678
+ };
679
+ if (inst.completedAt !== undefined) v.completedAt = inst.completedAt;
680
+ if (inst.resultPreview !== undefined) v.resultPreview = inst.resultPreview;
681
+ if (inst.error !== undefined) v.error = inst.error;
682
+ if (inst.parentInstanceId !== undefined) v.parentInstanceId = inst.parentInstanceId;
683
+ // v0.3.0: only surface intervention metadata when at least one
684
+ // intervention has actually been sent.
685
+ const interventionCount = inst.interventionCount ?? 0;
686
+ if (interventionCount > 0) {
687
+ v.interventionCount = interventionCount;
688
+ if (inst.interventionAt !== undefined) v.interventionAt = inst.interventionAt;
689
+ if (inst.interventionReason !== undefined) v.interventionReason = inst.interventionReason;
690
+ }
691
+ return v;
692
+ }
693
+
694
+ describe("bg-status toView — v0.3.0 fields", () => {
695
+ it("toView includes lastEventAt, interventionCount, interventionAt, interventionReason after intervention", () => {
696
+ const now = Date.now();
697
+ const inst: import("../src/background-state.js").BackgroundState = {
698
+ instanceId: "bgr_view_test",
699
+ sessionId: "sess_view",
700
+ agent: "mimir",
701
+ status: "running",
702
+ startedAt: now - 600_000,
703
+ model: "minimax/minimax-m3",
704
+ promptPreview: "Research X",
705
+ toolCallCount: 0,
706
+ parentAgent: "odin",
707
+ logPath: path.join(os.tmpdir(), "test.log"),
708
+ timeoutMs: 300_000,
709
+ lastEventAt: now - 60_000,
710
+ lastToolOrTextAt: now - 60_000,
711
+ interventionCount: 2,
712
+ interventionAt: now - 60_000,
713
+ interventionReason: "thinking loop (5m 0s without tool/text)",
714
+ };
715
+
716
+ const view = toViewForTest(inst);
717
+
718
+ expect(view.lastEventAt).toBe(now - 60_000);
719
+ expect(view.interventionCount).toBe(2);
720
+ expect(view.interventionAt).toBe(now - 60_000);
721
+ expect(view.interventionReason).toBe("thinking loop (5m 0s without tool/text)");
722
+ });
723
+
724
+ it("toView omits intervention fields (not null) when interventionCount is 0 or undefined", () => {
725
+ const now = Date.now();
726
+ const inst: import("../src/background-state.js").BackgroundState = {
727
+ instanceId: "bgr_view_no_intervention",
728
+ sessionId: "sess_view2",
729
+ agent: "mimir",
730
+ status: "running",
731
+ startedAt: now,
732
+ model: "minimax/minimax-m3",
733
+ promptPreview: "Research Y",
734
+ toolCallCount: 0,
735
+ parentAgent: "odin",
736
+ logPath: path.join(os.tmpdir(), "test.log"),
737
+ timeoutMs: 300_000,
738
+ // v0.3.0 fields are absent (no intervention yet)
739
+ };
740
+
741
+ const view = toViewForTest(inst);
742
+
743
+ // lastEventAt should still be present (always surfaced)
744
+ expect(view.lastEventAt).toBeUndefined(); // not set in this instance
745
+ // Intervention fields should be absent — not null, not 0, not present
746
+ expect("interventionCount" in view).toBe(false);
747
+ expect("interventionAt" in view).toBe(false);
748
+ expect("interventionReason" in view).toBe(false);
749
+ });
750
+ });