@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.
- package/README.md +83 -16
- package/bizar-dash/.bizar/activity.log +3 -0
- package/bizar-dash/.bizar/graph/.graphify_analysis.json +1540 -0
- package/bizar-dash/.bizar/graph/.graphify_labels.json +1 -0
- package/bizar-dash/.bizar/graph/GRAPH_REPORT.md +404 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/0661b816358db84dfdb7f10443db8a61152fd20b3fee950caf8fbf32920a1790.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/16da06d774142a6e5d74829ad982531286a6193ef798de8f593db029e123ec32.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/27e5697c01254c69c64c5a33896669073b40afb5ba88375102ae77fa5404136d.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/532997898027254aa4c0932243dc71d2f55888181232b65974961934997dc85b.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/84e3451a64c7f99f0cd6a88b0748d7426780486f53ab4935b7476b28c08f8293.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/88e5ea5f3e78fa69353fe9e6a8d1a05391d54605e25600cb125531eb5a6432ff.json +1 -0
- package/bizar-dash/.bizar/graph/cache/stat-index.json +1 -0
- package/bizar-dash/.bizar/graph/graph.html +307 -0
- package/bizar-dash/.bizar/graph/graph.json +40267 -0
- package/bizar-dash/.bizar/graph/manifest.json +762 -0
- package/bizar-dash/.graphifyignore +40 -0
- package/bizar-dash/.obsidian/README.md +14 -0
- package/bizar-dash/.obsidian/daily/2026-06-25.md +9 -0
- package/bizar-dash/.obsidian/vault.json +7 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/SKILL.md +103 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/assets/template.html +338 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/example.html +81 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/open-design.json +172 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/checklist.md +44 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/layouts.md +247 -0
- package/bizar-dash/CHANGELOG.md +474 -0
- package/bizar-dash/DESIGN.md +136 -0
- package/bizar-dash/dist/assets/main-B3RgW6FS.js +331 -0
- package/bizar-dash/dist/assets/main-B3RgW6FS.js.map +1 -0
- package/bizar-dash/dist/assets/main-DlJ8wgLR.css +1 -0
- package/bizar-dash/dist/assets/mobile-CsZQAswA.css +1 -0
- package/bizar-dash/dist/assets/mobile-DbxIw8BG.js +2 -0
- package/bizar-dash/dist/assets/mobile-DbxIw8BG.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js +354 -0
- package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js.map +1 -0
- package/bizar-dash/dist/index.html +40 -0
- package/bizar-dash/dist/mobile.html +22 -0
- package/bizar-dash/plans/debug-plan/comments.json +1 -0
- package/bizar-dash/plans/debug-plan/meta.json +10 -0
- package/bizar-dash/plans/debug-plan/plan.json +174 -0
- package/bizar-dash/scripts/smoke-bg-retry.mjs +246 -0
- package/bizar-dash/src/cli/dashboard-ports.mjs +488 -0
- package/bizar-dash/src/cli.mjs +522 -0
- package/bizar-dash/src/server/activity-log.mjs +174 -0
- package/bizar-dash/src/server/agents-store.mjs +465 -0
- package/bizar-dash/src/server/api.mjs +138 -0
- package/bizar-dash/src/server/artifact-template.html +810 -0
- package/bizar-dash/src/server/artifacts-store.mjs +733 -0
- package/bizar-dash/src/server/auth.mjs +381 -0
- package/bizar-dash/src/server/background-store.mjs +447 -0
- package/bizar-dash/src/server/bg-poller.mjs +305 -0
- package/bizar-dash/src/server/bg-retry.mjs +574 -0
- package/bizar-dash/src/server/browser.mjs +40 -0
- package/bizar-dash/src/server/diagnostics-store.mjs +148 -0
- package/bizar-dash/src/server/dialog-poller.mjs +105 -0
- package/bizar-dash/src/server/dialog-store.mjs +177 -0
- package/bizar-dash/src/server/glyphs/mdx-compiler.mjs +583 -0
- package/bizar-dash/src/server/lib/path-safe.mjs +283 -0
- package/bizar-dash/src/server/memory-git.mjs +329 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +767 -0
- package/bizar-dash/src/server/memory-schema.mjs +145 -0
- package/bizar-dash/src/server/memory-secrets.mjs +84 -0
- package/bizar-dash/src/server/memory-store.mjs +471 -0
- package/bizar-dash/src/server/mod-security.mjs +398 -0
- package/bizar-dash/src/server/mods-loader.mjs +1067 -0
- package/bizar-dash/src/server/notifications-store.mjs +243 -0
- package/bizar-dash/src/server/obsidian-store.mjs +318 -0
- package/bizar-dash/src/server/opencode-runner.mjs +279 -0
- package/bizar-dash/src/server/pair-store.mjs +138 -0
- package/bizar-dash/src/server/projects-store.mjs +364 -0
- package/bizar-dash/src/server/providers-store.mjs +789 -0
- package/bizar-dash/src/server/routes/_shared.mjs +415 -0
- package/bizar-dash/src/server/routes/activity.mjs +110 -0
- package/bizar-dash/src/server/routes/agents.mjs +134 -0
- package/bizar-dash/src/server/routes/artifacts.mjs +279 -0
- package/bizar-dash/src/server/routes/auth.mjs +69 -0
- package/bizar-dash/src/server/routes/background.mjs +165 -0
- package/bizar-dash/src/server/routes/chat.mjs +485 -0
- package/bizar-dash/src/server/routes/config.mjs +129 -0
- package/bizar-dash/src/server/routes/diagnostics.mjs +50 -0
- package/bizar-dash/src/server/routes/dialogs.mjs +86 -0
- package/bizar-dash/src/server/routes/fs.mjs +429 -0
- package/bizar-dash/src/server/routes/history.mjs +78 -0
- package/bizar-dash/src/server/routes/memory.mjs +658 -0
- package/bizar-dash/src/server/routes/misc.mjs +89 -0
- package/bizar-dash/src/server/routes/mods.mjs +359 -0
- package/bizar-dash/src/server/routes/notifications.mjs +54 -0
- package/bizar-dash/src/server/routes/obsidian.mjs +239 -0
- package/bizar-dash/src/server/routes/opencode-sessions.mjs +55 -0
- package/bizar-dash/src/server/routes/overview.mjs +91 -0
- package/bizar-dash/src/server/routes/pair.mjs +67 -0
- package/bizar-dash/src/server/routes/projects.mjs +175 -0
- package/bizar-dash/src/server/routes/providers.mjs +47 -0
- package/bizar-dash/src/server/routes/schedules.mjs +104 -0
- package/bizar-dash/src/server/routes/settings.mjs +51 -0
- package/bizar-dash/src/server/routes/skills.mjs +76 -0
- package/bizar-dash/src/server/routes/tasks.mjs +468 -0
- package/bizar-dash/src/server/routes/themes.mjs +51 -0
- package/bizar-dash/src/server/routes-v2/auth.mjs +59 -0
- package/bizar-dash/src/server/routes-v2/events.mjs +107 -0
- package/bizar-dash/src/server/routes-v2/health.mjs +21 -0
- package/bizar-dash/src/server/routes-v2/index.mjs +90 -0
- package/bizar-dash/src/server/routes-v2/sessions.mjs +111 -0
- package/bizar-dash/src/server/schedules-runner.mjs +369 -0
- package/bizar-dash/src/server/schedules-store.mjs +268 -0
- package/bizar-dash/src/server/search-store.mjs +220 -0
- package/bizar-dash/src/server/serve-info.mjs +684 -0
- package/bizar-dash/src/server/server.mjs +796 -0
- package/bizar-dash/src/server/settings-store.mjs +45 -0
- package/bizar-dash/src/server/skills-store.mjs +300 -0
- package/bizar-dash/src/server/state.mjs +437 -0
- package/bizar-dash/src/server/tailscale-store.mjs +113 -0
- package/bizar-dash/src/server/task-delegator.mjs +824 -0
- package/bizar-dash/src/server/tasks-store.mjs +508 -0
- package/bizar-dash/src/server/tui.mjs +923 -0
- package/bizar-dash/src/server/update-store.mjs +168 -0
- package/bizar-dash/src/server/v2-auth-file.mjs +99 -0
- package/bizar-dash/src/server/v2-event-bus.mjs +128 -0
- package/bizar-dash/src/server/watcher.mjs +81 -0
- package/bizar-dash/src/server/yaml.mjs +238 -0
- package/bizar-dash/src/web/App.tsx +718 -0
- package/bizar-dash/src/web/MobileApp.tsx +408 -0
- package/bizar-dash/src/web/components/ArtifactCreateDialog.tsx +95 -0
- package/bizar-dash/src/web/components/ArtifactListDialog.tsx +55 -0
- package/bizar-dash/src/web/components/ArtifactViewer.tsx +143 -0
- package/bizar-dash/src/web/components/AuditDialog.tsx +79 -0
- package/bizar-dash/src/web/components/BgStatusBadge.tsx +32 -0
- package/bizar-dash/src/web/components/Button.tsx +56 -0
- package/bizar-dash/src/web/components/CanvasContextMenu.tsx +68 -0
- package/bizar-dash/src/web/components/Card.tsx +40 -0
- package/bizar-dash/src/web/components/CollapsibleSection.tsx +72 -0
- package/bizar-dash/src/web/components/CommandDialog.tsx +66 -0
- package/bizar-dash/src/web/components/EmptyState.tsx +30 -0
- package/bizar-dash/src/web/components/FileBrowser.tsx +633 -0
- package/bizar-dash/src/web/components/HelpDialog.tsx +55 -0
- package/bizar-dash/src/web/components/KillConfirmDialog.tsx +88 -0
- package/bizar-dash/src/web/components/Modal.tsx +215 -0
- package/bizar-dash/src/web/components/Notifications.tsx +241 -0
- package/bizar-dash/src/web/components/SearchModal.tsx +224 -0
- package/bizar-dash/src/web/components/Sidebar.tsx +66 -0
- package/bizar-dash/src/web/components/Spinner.tsx +19 -0
- package/bizar-dash/src/web/components/StatusBadge.tsx +25 -0
- package/bizar-dash/src/web/components/Tag.tsx +28 -0
- package/bizar-dash/src/web/components/Toast.tsx +150 -0
- package/bizar-dash/src/web/components/Topbar.tsx +368 -0
- package/bizar-dash/src/web/components/VisualPlanDialog.tsx +58 -0
- package/bizar-dash/src/web/components/chat/AgentChip.tsx +68 -0
- package/bizar-dash/src/web/components/chat/ChatBubble.tsx +60 -0
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +88 -0
- package/bizar-dash/src/web/components/chat/ChatTopBar.tsx +50 -0
- package/bizar-dash/src/web/components/chat/Composer.tsx +164 -0
- package/bizar-dash/src/web/components/chat/ConfirmModal.tsx +54 -0
- package/bizar-dash/src/web/components/chat/EmptyState.tsx +21 -0
- package/bizar-dash/src/web/components/chat/FirstRunGreeting.tsx +18 -0
- package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +32 -0
- package/bizar-dash/src/web/components/chat/InfoPanel.tsx +119 -0
- package/bizar-dash/src/web/components/chat/LoadingSkeleton.tsx +19 -0
- package/bizar-dash/src/web/components/chat/MessageBubble.tsx +60 -0
- package/bizar-dash/src/web/components/chat/SessionList.tsx +153 -0
- package/bizar-dash/src/web/components/chat/StreamingIndicator.tsx +9 -0
- package/bizar-dash/src/web/components/chat/SuggestionCards.tsx +55 -0
- package/bizar-dash/src/web/components/chat/WelcomeScreen.tsx +36 -0
- package/bizar-dash/src/web/components/chat/index.ts +20 -0
- package/bizar-dash/src/web/components/chat/useAutoGrowTextarea.ts +18 -0
- package/bizar-dash/src/web/components/chat/useChat.ts +219 -0
- package/bizar-dash/src/web/components/chat/useSlashCommands.ts +47 -0
- package/bizar-dash/src/web/index.html +37 -0
- package/bizar-dash/src/web/lib/api.ts +193 -0
- package/bizar-dash/src/web/lib/markdown.tsx +59 -0
- package/bizar-dash/src/web/lib/types.ts +668 -0
- package/bizar-dash/src/web/lib/utils.ts +114 -0
- package/bizar-dash/src/web/lib/ws.ts +176 -0
- package/bizar-dash/src/web/main.tsx +34 -0
- package/bizar-dash/src/web/mobile/MobileApp.tsx +278 -0
- package/bizar-dash/src/web/mobile/MobileBottomNav.tsx +38 -0
- package/bizar-dash/src/web/mobile/MobileTopbar.tsx +115 -0
- package/bizar-dash/src/web/mobile/components/MobileBottomSheet.tsx +182 -0
- package/bizar-dash/src/web/mobile/components/MobileListItem.tsx +64 -0
- package/bizar-dash/src/web/mobile/components/MobileModal.tsx +123 -0
- package/bizar-dash/src/web/mobile/views/MobileActivity.tsx +273 -0
- package/bizar-dash/src/web/mobile/views/MobileAgents.tsx +374 -0
- package/bizar-dash/src/web/mobile/views/MobileArtifactCanvas.tsx +439 -0
- package/bizar-dash/src/web/mobile/views/MobileArtifacts.tsx +143 -0
- package/bizar-dash/src/web/mobile/views/MobileChat.tsx +180 -0
- package/bizar-dash/src/web/mobile/views/MobileConfig.tsx +175 -0
- package/bizar-dash/src/web/mobile/views/MobileHistory.tsx +191 -0
- package/bizar-dash/src/web/mobile/views/MobileMods.tsx +149 -0
- package/bizar-dash/src/web/mobile/views/MobileMore.tsx +121 -0
- package/bizar-dash/src/web/mobile/views/MobileNotifications.tsx +137 -0
- package/bizar-dash/src/web/mobile/views/MobilePlans.tsx +26 -0
- package/bizar-dash/src/web/mobile/views/MobileSchedules.tsx +685 -0
- package/bizar-dash/src/web/mobile/views/MobileSearchModal.tsx +172 -0
- package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +388 -0
- package/bizar-dash/src/web/mobile/views/MobileSkills.tsx +153 -0
- package/bizar-dash/src/web/mobile/views/MobileTasks.tsx +564 -0
- package/bizar-dash/src/web/mobile.html +20 -0
- package/bizar-dash/src/web/mobile.tsx +13 -0
- package/bizar-dash/src/web/styles/chat.css +198 -0
- package/bizar-dash/src/web/styles/glyphs.css +960 -0
- package/bizar-dash/src/web/styles/main.css +8855 -0
- package/bizar-dash/src/web/styles/mobile-chat.css +5 -0
- package/bizar-dash/src/web/styles/mobile.css +1960 -0
- package/bizar-dash/src/web/views/Activity.tsx +1203 -0
- package/bizar-dash/src/web/views/Agents.tsx +701 -0
- package/bizar-dash/src/web/views/Artifacts.tsx +1480 -0
- package/bizar-dash/src/web/views/BackgroundAgents.tsx +450 -0
- package/bizar-dash/src/web/views/Chat.tsx +190 -0
- package/bizar-dash/src/web/views/Config.tsx +2063 -0
- package/bizar-dash/src/web/views/History.tsx +335 -0
- package/bizar-dash/src/web/views/ModView.tsx +207 -0
- package/bizar-dash/src/web/views/Mods.tsx +693 -0
- package/bizar-dash/src/web/views/Overview.tsx +812 -0
- package/bizar-dash/src/web/views/Providers.tsx +302 -0
- package/bizar-dash/src/web/views/Schedules.tsx +802 -0
- package/bizar-dash/src/web/views/Settings.tsx +1787 -0
- package/bizar-dash/src/web/views/Skills.tsx +431 -0
- package/bizar-dash/src/web/views/Tasks.tsx +1523 -0
- package/bizar-dash/src/web/views/glyphs/GlyphRenderer.tsx +613 -0
- package/bizar-dash/src/web/views/glyphs/components.tsx +1098 -0
- package/bizar-dash/templates/mod/FORMAT.md +76 -0
- package/bizar-dash/templates/mod/hello-mod/README.md +19 -0
- package/bizar-dash/templates/mod/hello-mod/agents/greeter.md +8 -0
- package/bizar-dash/templates/mod/hello-mod/commands/hello.md +6 -0
- package/bizar-dash/templates/mod/hello-mod/mod.json +20 -0
- package/bizar-dash/templates/mod/hello-mod/routes/ping.mjs +9 -0
- package/bizar-dash/templates/mod/hello-mod/views/HelloView.tsx +10 -0
- package/bizar-dash/tests/dashboard-ports.test.mjs +138 -0
- package/bizar-dash/tests/graphify-mod-spawn.node.test.mjs +90 -0
- package/bizar-dash/tests/memory-cli.test.mjs +542 -0
- package/bizar-dash/tests/memory-config.test.mjs +422 -0
- package/bizar-dash/tests/memory-git.test.mjs +246 -0
- package/bizar-dash/tests/memory-lightrag.test.mjs +153 -0
- package/bizar-dash/tests/memory-protocol-drift.test.mjs +45 -0
- package/bizar-dash/tests/memory-schema.test.mjs +198 -0
- package/bizar-dash/tests/memory-secrets.test.mjs +121 -0
- package/bizar-dash/tests/memory-store.test.mjs +390 -0
- package/bizar-dash/tests/memory-sync.test.mjs +141 -0
- package/bizar-dash/tests/mod-instructions.node.test.mjs +188 -0
- package/bizar-dash/tests/mod-security.test.mjs +273 -0
- package/bizar-dash/tests/mod-upgrade.node.test.mjs +357 -0
- package/bizar-dash/tests/no-agent-browser.node.test.mjs +98 -0
- package/bizar-dash/tests/obsidian-back-compat.test.mjs +199 -0
- package/bizar-dash/tests/opencode-runner.test.mjs +172 -0
- package/bizar-dash/tests/path-safe.test.mjs +108 -0
- package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +333 -0
- package/bizar-dash/tests/smoke-v2.mjs +212 -0
- package/bizar-dash/tests/strip-thinking.test.mjs +175 -0
- package/bizar-dash/tests/submit-feedback.test.mjs +353 -0
- package/bizar-dash/tests/tmux-wrap.test.mjs +145 -0
- package/bizar-dash/tests/yaml.test.mjs +128 -0
- package/bizar-dash/tsconfig.json +23 -0
- package/bizar-dash/vite.config.ts +27 -0
- package/cli/atomic.mjs +73 -0
- package/cli/banner.mjs +1 -1
- package/cli/bin.mjs +44 -47
- package/cli/bootstrap.mjs +1 -1
- package/cli/copy.mjs +25 -16
- package/cli/dev-link.mjs +17 -1
- package/cli/doctor.mjs +4 -4
- package/cli/doctor.test.mjs +2 -2
- package/cli/init.mjs +135 -1
- package/cli/install.mjs +31 -39
- package/cli/memory-constants.mjs +59 -0
- package/cli/memory.mjs +1436 -0
- package/cli/service.mjs +5 -6
- package/cli/utils.mjs +6 -3
- package/config/AGENTS.md +7 -7
- package/config/agents/_shared/AGENT_BASELINE.md +59 -61
- package/config/opencode.json +13 -38
- package/config/skills/memory-protocol/SKILL.md +105 -0
- package/config/skills/obsidian/SKILL.md +270 -65
- package/install.sh +59 -19
- package/package.json +49 -14
- package/packages/sdk/LICENSE +21 -0
- package/packages/sdk/README.md +80 -0
- package/packages/sdk/src/client.ts +183 -0
- package/packages/sdk/src/errors.ts +120 -0
- package/packages/sdk/src/events.ts +153 -0
- package/packages/sdk/src/index.ts +63 -0
- package/packages/sdk/src/types.ts +176 -0
- package/packages/sdk/src/version.ts +4 -0
- package/packages/sdk/tests/client.test.ts +217 -0
- package/packages/sdk/tests/errors.test.ts +108 -0
- package/packages/sdk/tests/events.test.ts +139 -0
- package/packages/sdk/tests/fixtures/fetch-mock.ts +152 -0
- package/packages/sdk/tests/fixtures/sse-mock.ts +30 -0
- package/packages/sdk/tsconfig.json +25 -0
- package/packages/sdk/vitest.config.ts +9 -0
- package/plugins/bizar/LICENSE +21 -0
- package/plugins/bizar/README.md +448 -0
- package/plugins/bizar/dist/index.js +29901 -0
- package/plugins/bizar/index.ts +1491 -0
- package/plugins/bizar/scripts/check-forbidden-imports.sh +33 -0
- package/plugins/bizar/src/background-state.ts +551 -0
- package/plugins/bizar/src/background.ts +1250 -0
- package/plugins/bizar/src/commands-impl.ts +464 -0
- package/plugins/bizar/src/commands.ts +1168 -0
- package/plugins/bizar/src/dashboard-client.ts +233 -0
- package/plugins/bizar/src/event-stream.ts +606 -0
- package/plugins/bizar/src/fingerprint.ts +120 -0
- package/plugins/bizar/src/handoff.ts +79 -0
- package/plugins/bizar/src/http-client.ts +467 -0
- package/plugins/bizar/src/key-rotation.ts +218 -0
- package/plugins/bizar/src/logger.ts +144 -0
- package/plugins/bizar/src/loop.ts +176 -0
- package/plugins/bizar/src/opencode-runner.ts +390 -0
- package/plugins/bizar/src/options.ts +421 -0
- package/plugins/bizar/src/plan-fs.ts +323 -0
- package/plugins/bizar/src/reasoning-clean.ts +454 -0
- package/plugins/bizar/src/report.ts +178 -0
- package/plugins/bizar/src/research-prompt.ts +35 -0
- package/plugins/bizar/src/serve-info.ts +228 -0
- package/plugins/bizar/src/serve.ts +496 -0
- package/plugins/bizar/src/settings.ts +349 -0
- package/plugins/bizar/src/state.ts +298 -0
- package/plugins/bizar/src/tools/bg-collect.ts +104 -0
- package/plugins/bizar/src/tools/bg-get-comments.ts +239 -0
- package/plugins/bizar/src/tools/bg-kill.ts +87 -0
- package/plugins/bizar/src/tools/bg-spawn.ts +401 -0
- package/plugins/bizar/src/tools/bg-status.ts +99 -0
- package/plugins/bizar/src/tools/open-kb.ts +191 -0
- package/plugins/bizar/src/tools/plan-action.ts +767 -0
- package/plugins/bizar/src/tools/read-glyph-feedback.ts +170 -0
- package/plugins/bizar/src/tools/wait-for-feedback.ts +402 -0
- package/plugins/bizar/tests/attach-handler-bug.test.ts +169 -0
- package/plugins/bizar/tests/background-state.test.ts +277 -0
- package/plugins/bizar/tests/background.test.ts +402 -0
- package/plugins/bizar/tests/block.test.ts +195 -0
- package/plugins/bizar/tests/canonical-key-order.test.ts +75 -0
- package/plugins/bizar/tests/commands-impl.test.ts +442 -0
- package/plugins/bizar/tests/commands.test.ts +584 -0
- package/plugins/bizar/tests/config.test.ts +122 -0
- package/plugins/bizar/tests/dashboard-client.test.ts +159 -0
- package/plugins/bizar/tests/dispose.test.ts +336 -0
- package/plugins/bizar/tests/event-stream.test.ts +409 -0
- package/plugins/bizar/tests/event.test.ts +262 -0
- package/plugins/bizar/tests/fingerprint.test.ts +162 -0
- package/plugins/bizar/tests/http-client.test.ts +404 -0
- package/plugins/bizar/tests/init-helpers.test.ts +203 -0
- package/plugins/bizar/tests/integration/slash-command.test.ts +348 -0
- package/plugins/bizar/tests/integration/tool-routing.test.ts +314 -0
- package/plugins/bizar/tests/key-rotation.test.ts +396 -0
- package/plugins/bizar/tests/loop.test.ts +397 -0
- package/plugins/bizar/tests/options.test.ts +276 -0
- package/plugins/bizar/tests/reasoning-clean.test.ts +422 -0
- package/plugins/bizar/tests/serve.test.ts +339 -0
- package/plugins/bizar/tests/settings.test.ts +351 -0
- package/plugins/bizar/tests/stall-think.test.ts +750 -0
- package/plugins/bizar/tests/state.test.ts +276 -0
- package/plugins/bizar/tests/tools/bg-collect.test.ts +337 -0
- package/plugins/bizar/tests/tools/bg-get-comments.test.ts +485 -0
- package/plugins/bizar/tests/tools/bg-kill.test.ts +235 -0
- package/plugins/bizar/tests/tools/bg-spawn-delegation.test.ts +147 -0
- package/plugins/bizar/tests/tools/bg-spawn.test.ts +311 -0
- package/plugins/bizar/tests/tools/bg-status.test.ts +217 -0
- package/plugins/bizar/tests/tools/opencode-runner.test.ts +115 -0
- package/plugins/bizar/tests/tools/plan-action.test.ts +599 -0
- package/plugins/bizar/tests/tools/read-glyph-feedback.test.ts +253 -0
- package/plugins/bizar/tests/tools/wait-for-feedback.test.ts +390 -0
- package/plugins/bizar/tests/update-deadlock.test.ts +145 -0
- 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
|
+
}
|