@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,606 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* event-stream.ts
|
|
3
|
+
*
|
|
4
|
+
* Global SSE subscription for opencode serve events (v0.4.2 spec §2.1, §4).
|
|
5
|
+
*
|
|
6
|
+
* Design contract:
|
|
7
|
+
* - ONE `GET /event?directory=<worktree>` connection per plugin process.
|
|
8
|
+
* - Events are filtered in-memory by `sessionID` and dispatched to
|
|
9
|
+
* per-session handlers registered via `onSessionEvent`.
|
|
10
|
+
* - The connection auto-reconnects with exponential backoff
|
|
11
|
+
* (1s, 2s, 4s, 8s, 16s, capped at 30s) on unexpected drops.
|
|
12
|
+
* - During the gap between drop and reconnect, in-flight instances are
|
|
13
|
+
* NOT marked failed (they may still complete once we reconnect).
|
|
14
|
+
* - `disconnect()` closes the stream and prevents further reconnects.
|
|
15
|
+
*
|
|
16
|
+
* v0.4.3 — CloudEvents-style schema support (see
|
|
17
|
+
* `.bizar/opencode-sse-investigation.md`):
|
|
18
|
+
* - The actual event schema on opencode serve 1.17.7 has two flavors:
|
|
19
|
+
*
|
|
20
|
+
* 1) **Direct events** (flat JSON, native `type` field):
|
|
21
|
+
* ```
|
|
22
|
+
* { id, type: "session.idle", properties: { sessionID } }
|
|
23
|
+
* { id, type: "message.part.delta", properties: { sessionID, ... } }
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* 2) **Sync events** (CloudEvents-style, wrapped):
|
|
27
|
+
* ```
|
|
28
|
+
* { type: "sync", syncEvent: {
|
|
29
|
+
* type: "session.created.1", // `.1` version suffix
|
|
30
|
+
* id, seq, aggregateID,
|
|
31
|
+
* data: { sessionID, info }
|
|
32
|
+
* }}
|
|
33
|
+
* ```
|
|
34
|
+
* Sync events are the primary delivery mechanism for session and
|
|
35
|
+
* message lifecycle events. Event names like `session.created.1`,
|
|
36
|
+
* `message.part.updated.1` carry a numeric version suffix.
|
|
37
|
+
*
|
|
38
|
+
* - This module unwraps sync events and strips the version suffix so
|
|
39
|
+
* downstream consumers (InstanceManager) see the same logical
|
|
40
|
+
* `StreamEvent` shape regardless of which wire format arrived.
|
|
41
|
+
*
|
|
42
|
+
* This module is a pure transport. The `InstanceManager` registers a
|
|
43
|
+
* handler per instance that does the actual state work (tool counting,
|
|
44
|
+
* loop-guard detection, status transitions, awaiting collect).
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import type { HttpClient, HttpResult } from "./http-client.js";
|
|
48
|
+
|
|
49
|
+
// --- Logger interface -----------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Minimal Logger interface — matches the shape in `state.ts` / `logger.ts`.
|
|
53
|
+
*/
|
|
54
|
+
export interface Logger {
|
|
55
|
+
log(opts: { level: "debug" | "info" | "warn" | "error"; message: string }): void;
|
|
56
|
+
debug(message: string): void;
|
|
57
|
+
info(message: string): void;
|
|
58
|
+
warn(message: string): void;
|
|
59
|
+
error(message: string): void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --- Public event types ---------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The subset of session lifecycle events the plugin cares about. Each
|
|
66
|
+
* variant carries a `sessionID` plus the event-specific payload.
|
|
67
|
+
*
|
|
68
|
+
* The `type` field follows opencode's event namespacing
|
|
69
|
+
* (`session.created`, `session.idle`, `message.part.updated`, …).
|
|
70
|
+
*
|
|
71
|
+
* Note: there is no catch-all variant on purpose. The catch-all would
|
|
72
|
+
* have `type: string` which would defeat type narrowing on
|
|
73
|
+
* `ev.type === "session.idle"` checks. Untyped events are dropped
|
|
74
|
+
* silently in `dispatchEvent`.
|
|
75
|
+
*/
|
|
76
|
+
export type StreamEvent =
|
|
77
|
+
| { type: "session.created"; sessionID: string; raw: unknown }
|
|
78
|
+
| { type: "session.updated"; sessionID: string; raw: unknown }
|
|
79
|
+
| { type: "session.deleted"; sessionID: string; raw: unknown }
|
|
80
|
+
| { type: "session.idle"; sessionID: string; raw: unknown }
|
|
81
|
+
| { type: "session.error"; sessionID: string; error?: string; raw: unknown }
|
|
82
|
+
| { type: "message.updated"; sessionID: string; messageID: string; raw: unknown }
|
|
83
|
+
| {
|
|
84
|
+
type: "message.part.updated";
|
|
85
|
+
sessionID: string;
|
|
86
|
+
messageID: string;
|
|
87
|
+
partID: string;
|
|
88
|
+
part: { type: string; text?: string; error?: string; state?: { status?: string; error?: string } };
|
|
89
|
+
raw: unknown;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A per-session event handler. Called for every event whose `sessionID`
|
|
94
|
+
* matches the registration. Handlers are called synchronously; long
|
|
95
|
+
* work should be scheduled (await / setImmediate) inside the handler.
|
|
96
|
+
*/
|
|
97
|
+
export type SessionEventHandler = (event: StreamEvent) => void;
|
|
98
|
+
|
|
99
|
+
// --- Stream class ---------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Owns the single global SSE connection. The plugin constructs one
|
|
103
|
+
* instance at init time and disposes it on plugin shutdown.
|
|
104
|
+
*
|
|
105
|
+
* Public surface (interface contract for Thor's tests):
|
|
106
|
+
* - `connect()` — open the SSE stream. Idempotent (no-op if already connected).
|
|
107
|
+
* - `disconnect()` — close the stream; stop reconnecting.
|
|
108
|
+
* - `onSessionEvent(sessionId, handler)` — register a per-session handler.
|
|
109
|
+
* Returns an unsubscribe function.
|
|
110
|
+
*
|
|
111
|
+
* Internal lifecycle (not part of the contract):
|
|
112
|
+
* - `readLoop()` runs until disconnect; on error, schedules a reconnect.
|
|
113
|
+
* - `parseSseChunk()` splits a raw chunk into individual events.
|
|
114
|
+
* - `dispatch()` routes each event to handlers whose `sessionID` matches.
|
|
115
|
+
*/
|
|
116
|
+
export class EventStream {
|
|
117
|
+
private _baseUrl: string;
|
|
118
|
+
private _directory: string;
|
|
119
|
+
private _authHeader: string;
|
|
120
|
+
private _logger: Logger;
|
|
121
|
+
private _http: HttpClient;
|
|
122
|
+
private _handlers = new Map<string, Set<SessionEventHandler>>();
|
|
123
|
+
/** Global handler invoked for every event regardless of session (v0.7.0). */
|
|
124
|
+
private _globalHandler: SessionEventHandler | null = null;
|
|
125
|
+
private _connected = false;
|
|
126
|
+
private _aborted = false;
|
|
127
|
+
private _abortController: AbortController | null = null;
|
|
128
|
+
private _reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
|
129
|
+
private _reconnectAttempt = 0;
|
|
130
|
+
private _reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
131
|
+
private _connectPromise: Promise<void> | null = null;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Register a global handler invoked for every event (regardless of
|
|
135
|
+
* session). Used to forward events to the dashboard publisher. Returns
|
|
136
|
+
* an unsubscribe function. (v0.7.0-alpha.1 — wired into the v2
|
|
137
|
+
* SDK dashboard-client.ts bridge.)
|
|
138
|
+
*/
|
|
139
|
+
onEvent(handler: SessionEventHandler): () => void {
|
|
140
|
+
this._globalHandler = handler;
|
|
141
|
+
return () => {
|
|
142
|
+
if (this._globalHandler === handler) {
|
|
143
|
+
this._globalHandler = null;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
constructor(opts: {
|
|
149
|
+
baseUrl: string;
|
|
150
|
+
directory: string;
|
|
151
|
+
authHeader: string;
|
|
152
|
+
logger: Logger;
|
|
153
|
+
http: HttpClient;
|
|
154
|
+
}) {
|
|
155
|
+
this._baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
156
|
+
this._directory = opts.directory;
|
|
157
|
+
this._authHeader = opts.authHeader;
|
|
158
|
+
this._logger = opts.logger;
|
|
159
|
+
this._http = opts.http;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// --- Getters ------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
get connected(): boolean {
|
|
165
|
+
return this._connected;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
get directory(): string {
|
|
169
|
+
return this._directory;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// --- Lifecycle ----------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Open the SSE connection. Idempotent: a second call while a connection
|
|
176
|
+
* is in progress is a no-op that returns the in-flight promise.
|
|
177
|
+
*
|
|
178
|
+
* The first read block-waits up to 5s for the initial event so the
|
|
179
|
+
* caller can confirm the connection is alive (spec §2.1).
|
|
180
|
+
*/
|
|
181
|
+
async connect(): Promise<void> {
|
|
182
|
+
if (this._aborted) {
|
|
183
|
+
throw new Error("EventStream: cannot connect after disconnect()");
|
|
184
|
+
}
|
|
185
|
+
if (this._connectPromise !== null) {
|
|
186
|
+
return this._connectPromise;
|
|
187
|
+
}
|
|
188
|
+
this._connectPromise = this.openConnection();
|
|
189
|
+
try {
|
|
190
|
+
await this._connectPromise;
|
|
191
|
+
} finally {
|
|
192
|
+
this._connectPromise = null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Close the stream and stop further reconnects. Idempotent.
|
|
198
|
+
*/
|
|
199
|
+
async disconnect(): Promise<void> {
|
|
200
|
+
this._aborted = true;
|
|
201
|
+
if (this._reconnectTimer !== null) {
|
|
202
|
+
clearTimeout(this._reconnectTimer);
|
|
203
|
+
this._reconnectTimer = null;
|
|
204
|
+
}
|
|
205
|
+
if (this._abortController !== null) {
|
|
206
|
+
try {
|
|
207
|
+
this._abortController.abort();
|
|
208
|
+
} catch {
|
|
209
|
+
// ignore
|
|
210
|
+
}
|
|
211
|
+
this._abortController = null;
|
|
212
|
+
}
|
|
213
|
+
if (this._reader !== null) {
|
|
214
|
+
try {
|
|
215
|
+
await this._reader.cancel();
|
|
216
|
+
} catch {
|
|
217
|
+
// ignore
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
this._reader.releaseLock();
|
|
221
|
+
} catch {
|
|
222
|
+
// ignore
|
|
223
|
+
}
|
|
224
|
+
this._reader = null;
|
|
225
|
+
}
|
|
226
|
+
this._connected = false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Register a handler for events of one session. Returns an unsubscribe
|
|
231
|
+
* function. Multiple handlers per session are allowed.
|
|
232
|
+
*/
|
|
233
|
+
onSessionEvent(sessionId: string, handler: SessionEventHandler): () => void {
|
|
234
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
235
|
+
throw new Error("EventStream.onSessionEvent: sessionId must be non-empty");
|
|
236
|
+
}
|
|
237
|
+
let set = this._handlers.get(sessionId);
|
|
238
|
+
if (!set) {
|
|
239
|
+
set = new Set();
|
|
240
|
+
this._handlers.set(sessionId, set);
|
|
241
|
+
}
|
|
242
|
+
set.add(handler);
|
|
243
|
+
return () => {
|
|
244
|
+
const s = this._handlers.get(sessionId);
|
|
245
|
+
if (!s) return;
|
|
246
|
+
s.delete(handler);
|
|
247
|
+
if (s.size === 0) this._handlers.delete(sessionId);
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// --- Internal: connection lifecycle ------------------------------------
|
|
252
|
+
|
|
253
|
+
private async openConnection(): Promise<void> {
|
|
254
|
+
this._abortController = new AbortController();
|
|
255
|
+
const result: HttpResult<ReadableStream> = await this._http.fetchEventStream(
|
|
256
|
+
this._directory,
|
|
257
|
+
this._abortController.signal,
|
|
258
|
+
);
|
|
259
|
+
if (!result.ok) {
|
|
260
|
+
this._connected = false;
|
|
261
|
+
throw new Error(`EventStream: failed to open SSE: ${result.error}`);
|
|
262
|
+
}
|
|
263
|
+
this._connected = true;
|
|
264
|
+
this._reconnectAttempt = 0;
|
|
265
|
+
this._logger.info(
|
|
266
|
+
`bizar: SSE event stream open (directory=${this._directory})`,
|
|
267
|
+
);
|
|
268
|
+
// Start the read loop in the background.
|
|
269
|
+
void this.readLoop(result.value);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Read the SSE stream chunk by chunk, parse events, and dispatch them.
|
|
274
|
+
* On error, schedule a reconnect (unless we're aborted).
|
|
275
|
+
*/
|
|
276
|
+
private async readLoop(stream: ReadableStream<Uint8Array>): Promise<void> {
|
|
277
|
+
const reader = stream.getReader() as ReadableStreamDefaultReader<Uint8Array>;
|
|
278
|
+
this._reader = reader;
|
|
279
|
+
const decoder = new TextDecoder("utf-8");
|
|
280
|
+
let buffer = "";
|
|
281
|
+
try {
|
|
282
|
+
while (!this._aborted) {
|
|
283
|
+
const { done, value } = await reader.read();
|
|
284
|
+
if (done) {
|
|
285
|
+
// Stream closed cleanly. This is unexpected unless we initiated it.
|
|
286
|
+
if (!this._aborted) {
|
|
287
|
+
this._logger.warn("bizar: SSE stream closed unexpectedly; will reconnect");
|
|
288
|
+
this.scheduleReconnect();
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
293
|
+
buffer += chunk;
|
|
294
|
+
// Process as many complete events as we have.
|
|
295
|
+
let sep: number;
|
|
296
|
+
// SSE events are separated by a blank line (\n\n or \r\n\r\n).
|
|
297
|
+
while ((sep = buffer.indexOf("\n\n")) >= 0 || (sep = buffer.indexOf("\r\n\r\n")) >= 0) {
|
|
298
|
+
const rawEvent = buffer.slice(0, sep);
|
|
299
|
+
buffer = buffer.slice(sep + (buffer[sep] === "\r" ? 4 : 2));
|
|
300
|
+
this.processSseBlock(rawEvent);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch (err: unknown) {
|
|
304
|
+
if (this._aborted) return;
|
|
305
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
306
|
+
this._logger.warn(`bizar: SSE read error: ${msg}; will reconnect`);
|
|
307
|
+
this.scheduleReconnect();
|
|
308
|
+
} finally {
|
|
309
|
+
this._connected = false;
|
|
310
|
+
try {
|
|
311
|
+
this._reader?.releaseLock();
|
|
312
|
+
} catch {
|
|
313
|
+
// ignore
|
|
314
|
+
}
|
|
315
|
+
this._reader = null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private processSseBlock(block: string): void {
|
|
320
|
+
if (block.trim() === "") return;
|
|
321
|
+
// An SSE block has lines like `event: <name>` and `data: <payload>`.
|
|
322
|
+
// Some servers only send `data:`. We treat unknown prefix lines as
|
|
323
|
+
// comments and skip them.
|
|
324
|
+
let eventName: string | null = null;
|
|
325
|
+
const dataLines: string[] = [];
|
|
326
|
+
for (const line of block.split(/\r?\n/)) {
|
|
327
|
+
if (line === "" || line.startsWith(":")) continue;
|
|
328
|
+
const colon = line.indexOf(":");
|
|
329
|
+
if (colon < 0) continue;
|
|
330
|
+
const field = line.slice(0, colon);
|
|
331
|
+
let value = line.slice(colon + 1);
|
|
332
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
333
|
+
if (field === "event") {
|
|
334
|
+
eventName = value;
|
|
335
|
+
} else if (field === "data") {
|
|
336
|
+
dataLines.push(value);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (dataLines.length === 0) return;
|
|
340
|
+
const data = dataLines.join("\n");
|
|
341
|
+
let parsed: unknown;
|
|
342
|
+
try {
|
|
343
|
+
parsed = JSON.parse(data);
|
|
344
|
+
} catch {
|
|
345
|
+
this._logger.debug(`bizar: SSE: dropping non-JSON data (${data.slice(0, 80)}…)`);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
this.dispatchEvent(eventName, parsed);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Map a raw opencode event to a `StreamEvent` and dispatch to handlers.
|
|
353
|
+
*
|
|
354
|
+
* v0.4.3: handles two wire formats.
|
|
355
|
+
* 1) Direct events — `{type, properties: {sessionID, ...}}`.
|
|
356
|
+
* 2) Sync events — `{type: "sync", syncEvent: {type: "x.y.1", data: {...}}}`.
|
|
357
|
+
* We unwrap the sync wrapper and set `obj` to `syncEvent.data` so
|
|
358
|
+
* downstream code can read `obj.sessionID`, `obj.part`, etc.
|
|
359
|
+
*
|
|
360
|
+
* After unwrapping, the event type from sync events has a version
|
|
361
|
+
* suffix (e.g. `session.created.1`). We strip the suffix so
|
|
362
|
+
* downstream code can match on `session.created` (the version is
|
|
363
|
+
* for the wire format, not the plugin's logical event name).
|
|
364
|
+
*/
|
|
365
|
+
private dispatchEvent(eventName: string | null, raw: unknown): void {
|
|
366
|
+
let obj = raw as Record<string, unknown> | null;
|
|
367
|
+
if (obj === null || typeof obj !== "object") return;
|
|
368
|
+
|
|
369
|
+
// Step 1: Detect sync event wrapper, unwrap to the inner data.
|
|
370
|
+
// Sync events have shape: {type: "sync", syncEvent: {type: "x.y.1", data: {...}}}
|
|
371
|
+
// After unwrap, obj points to the inner data so the rest of this
|
|
372
|
+
// method can read fields directly (obj.sessionID, obj.part, etc.).
|
|
373
|
+
let innerType: string | null = null;
|
|
374
|
+
if (obj.type === "sync" && obj.syncEvent) {
|
|
375
|
+
const syncEvent = obj.syncEvent as Record<string, unknown> | null;
|
|
376
|
+
if (syncEvent && typeof syncEvent === "object") {
|
|
377
|
+
if (typeof syncEvent.type === "string") innerType = syncEvent.type;
|
|
378
|
+
const syncData = syncEvent.data;
|
|
379
|
+
if (syncData && typeof syncData === "object") {
|
|
380
|
+
obj = syncData as Record<string, unknown>;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Step 2: Resolve the event type and strip the version suffix.
|
|
386
|
+
// Sync event types look like "session.created.1"; we strip to "session.created".
|
|
387
|
+
const typeFromObj = innerType ?? (typeof obj.type === "string" ? obj.type : null);
|
|
388
|
+
const rawType = typeFromObj ?? eventName ?? "unknown";
|
|
389
|
+
const type = stripVersionSuffix(rawType);
|
|
390
|
+
|
|
391
|
+
const sessionID = extractSessionId(obj);
|
|
392
|
+
const messageID = extractMessageId(obj);
|
|
393
|
+
// After unwrap, `obj.part` exists for `message.part.updated.*` events
|
|
394
|
+
// (sync) and may exist on the top-level payload. We also fall back to
|
|
395
|
+
// `obj.partID` on the part object itself for sync events whose
|
|
396
|
+
// `data` block does not carry `partID` separately.
|
|
397
|
+
const part = obj.part as { type?: string; text?: string; error?: string; state?: { status?: string; error?: string }; id?: string } | undefined;
|
|
398
|
+
const partID = extractPartId(obj, part);
|
|
399
|
+
|
|
400
|
+
// Build the typed event. We use a temporary permissive shape and only
|
|
401
|
+
// assign to a strongly-typed StreamEvent at the end of each branch.
|
|
402
|
+
let event: StreamEvent | null = null;
|
|
403
|
+
if (type === "message.part.updated" && sessionID && part) {
|
|
404
|
+
const partShape: { type: string; text?: string; error?: string; state?: { status?: string; error?: string } } = {
|
|
405
|
+
type: part.type ?? "unknown",
|
|
406
|
+
};
|
|
407
|
+
if (part.text !== undefined) partShape.text = part.text;
|
|
408
|
+
if (part.error !== undefined) partShape.error = part.error;
|
|
409
|
+
if (part.state !== undefined) partShape.state = part.state;
|
|
410
|
+
event = {
|
|
411
|
+
type: "message.part.updated",
|
|
412
|
+
sessionID,
|
|
413
|
+
messageID: messageID ?? "",
|
|
414
|
+
partID,
|
|
415
|
+
part: partShape,
|
|
416
|
+
raw,
|
|
417
|
+
};
|
|
418
|
+
} else if (type === "message.updated" && sessionID) {
|
|
419
|
+
event = { type: "message.updated", sessionID, messageID: messageID ?? "", raw };
|
|
420
|
+
} else if (type === "session.error" && sessionID) {
|
|
421
|
+
// session.error may carry the error string on `properties.error`
|
|
422
|
+
// (direct event) or on `data.error` (sync, but we already
|
|
423
|
+
// unwrapped, so both paths land at `obj.error`).
|
|
424
|
+
const props = obj.properties;
|
|
425
|
+
let errorField: string | undefined =
|
|
426
|
+
typeof obj.error === "string" ? obj.error : undefined;
|
|
427
|
+
if (errorField === undefined && props && typeof props === "object") {
|
|
428
|
+
const p = props as Record<string, unknown>;
|
|
429
|
+
if (typeof p.error === "string") errorField = p.error;
|
|
430
|
+
}
|
|
431
|
+
event = errorField !== undefined
|
|
432
|
+
? { type: "session.error", sessionID, error: errorField, raw }
|
|
433
|
+
: { type: "session.error", sessionID, raw };
|
|
434
|
+
} else if (type === "session.idle" && sessionID) {
|
|
435
|
+
event = { type: "session.idle", sessionID, raw };
|
|
436
|
+
} else if (type === "session.created" && sessionID) {
|
|
437
|
+
event = { type: "session.created", sessionID, raw };
|
|
438
|
+
} else if (type === "session.updated" && sessionID) {
|
|
439
|
+
event = { type: "session.updated", sessionID, raw };
|
|
440
|
+
} else if (type === "session.deleted" && sessionID) {
|
|
441
|
+
event = { type: "session.deleted", sessionID, raw };
|
|
442
|
+
} else {
|
|
443
|
+
// Unknown / untyped event — drop silently. Logged at debug level.
|
|
444
|
+
// We log the *raw* type (with version suffix) for diagnostics.
|
|
445
|
+
this._logger.debug(
|
|
446
|
+
`bizar: SSE: dropping untyped event (rawType=${rawType}${sessionID ? ` sessionID=${sessionID}` : ""})`,
|
|
447
|
+
);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (event === null) {
|
|
452
|
+
this._logger.debug(`bizar: SSE: event without sessionID (rawType=${rawType})`);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
this.dispatchToHandlers(sessionID, event);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private dispatchToHandlers(sessionID: string, event: StreamEvent): void {
|
|
459
|
+
// v0.7.0-alpha.1 — Forward every event to the global handler
|
|
460
|
+
// (typically the dashboard publisher) BEFORE the per-session dispatch.
|
|
461
|
+
// Failures here are isolated so one bad handler doesn't break the
|
|
462
|
+
// dispatch chain for other subscribers.
|
|
463
|
+
if (this._globalHandler) {
|
|
464
|
+
try {
|
|
465
|
+
this._globalHandler(event);
|
|
466
|
+
} catch (err: unknown) {
|
|
467
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
468
|
+
this._logger.warn(
|
|
469
|
+
`bizar: SSE global handler threw for session ${sessionID} (type=${event.type}): ${msg}`,
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const set = this._handlers.get(sessionID);
|
|
475
|
+
if (!set || set.size === 0) {
|
|
476
|
+
this._logger.debug(
|
|
477
|
+
`bizar: SSE event for untracked session ${sessionID} (type=${event.type})`,
|
|
478
|
+
);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
// Snapshot to avoid mutation during iteration.
|
|
482
|
+
for (const handler of [...set]) {
|
|
483
|
+
try {
|
|
484
|
+
handler(event);
|
|
485
|
+
} catch (err: unknown) {
|
|
486
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
487
|
+
this._logger.warn(
|
|
488
|
+
`bizar: SSE handler threw for session ${sessionID} (type=${event.type}): ${msg}`,
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// --- Reconnect ----------------------------------------------------------
|
|
495
|
+
|
|
496
|
+
private scheduleReconnect(): void {
|
|
497
|
+
if (this._aborted) return;
|
|
498
|
+
if (this._reconnectTimer !== null) return;
|
|
499
|
+
// Exponential backoff: 1s, 2s, 4s, 8s, 16s, capped at 30s.
|
|
500
|
+
const base = 1000 * Math.pow(2, this._reconnectAttempt);
|
|
501
|
+
const delay = Math.min(30_000, base);
|
|
502
|
+
this._reconnectAttempt += 1;
|
|
503
|
+
this._logger.warn(`bizar: SSE reconnecting in ${delay}ms (attempt ${this._reconnectAttempt})`);
|
|
504
|
+
this._reconnectTimer = setTimeout(() => {
|
|
505
|
+
this._reconnectTimer = null;
|
|
506
|
+
if (this._aborted) return;
|
|
507
|
+
void this.attemptReconnect();
|
|
508
|
+
}, delay);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private async attemptReconnect(): Promise<void> {
|
|
512
|
+
try {
|
|
513
|
+
this._abortController = new AbortController();
|
|
514
|
+
const result = await this._http.fetchEventStream(
|
|
515
|
+
this._directory,
|
|
516
|
+
this._abortController.signal,
|
|
517
|
+
);
|
|
518
|
+
if (!result.ok) {
|
|
519
|
+
this._logger.warn(`bizar: SSE reconnect failed: ${result.error}`);
|
|
520
|
+
this.scheduleReconnect();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
this._connected = true;
|
|
524
|
+
this._reconnectAttempt = 0;
|
|
525
|
+
this._logger.info("bizar: SSE reconnected");
|
|
526
|
+
void this.readLoop(result.value);
|
|
527
|
+
} catch (err: unknown) {
|
|
528
|
+
if (this._aborted) return;
|
|
529
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
530
|
+
this._logger.warn(`bizar: SSE reconnect threw: ${msg}`);
|
|
531
|
+
this.scheduleReconnect();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// --- Helpers --------------------------------------------------------------
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Extract the `sessionID` from an opencode event payload.
|
|
540
|
+
*
|
|
541
|
+
* v0.4.3: this is called AFTER sync unwrapping, so the `obj` we receive
|
|
542
|
+
* is either:
|
|
543
|
+
* - A direct event: `{properties: {sessionID}}` or `{sessionID}`.
|
|
544
|
+
* - A sync event's `data` block: `{sessionID, ...}` (top-level).
|
|
545
|
+
*
|
|
546
|
+
* We check (in order):
|
|
547
|
+
* 1. `properties.sessionID` / `properties.sessionId` (direct events)
|
|
548
|
+
* 2. Top-level `sessionID` / `sessionId` (direct or post-unwrapped sync)
|
|
549
|
+
*
|
|
550
|
+
* Returns `undefined` if none of those is present.
|
|
551
|
+
*/
|
|
552
|
+
function extractSessionId(obj: Record<string, unknown>): string | undefined {
|
|
553
|
+
const props = obj.properties;
|
|
554
|
+
if (props && typeof props === "object") {
|
|
555
|
+
const p = props as Record<string, unknown>;
|
|
556
|
+
if (typeof p.sessionID === "string" && p.sessionID.length > 0) return p.sessionID;
|
|
557
|
+
if (typeof p.sessionId === "string" && p.sessionId.length > 0) return p.sessionId;
|
|
558
|
+
}
|
|
559
|
+
if (typeof obj.sessionID === "string" && obj.sessionID.length > 0) return obj.sessionID;
|
|
560
|
+
if (typeof obj.sessionId === "string" && obj.sessionId.length > 0) return obj.sessionId;
|
|
561
|
+
return undefined;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function extractMessageId(obj: Record<string, unknown>): string | undefined {
|
|
565
|
+
const props = obj.properties;
|
|
566
|
+
if (props && typeof props === "object") {
|
|
567
|
+
const p = props as Record<string, unknown>;
|
|
568
|
+
if (typeof p.messageID === "string") return p.messageID;
|
|
569
|
+
if (typeof p.messageId === "string") return p.messageId;
|
|
570
|
+
}
|
|
571
|
+
if (typeof obj.messageID === "string") return obj.messageID;
|
|
572
|
+
if (typeof obj.messageId === "string") return obj.messageId;
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Extract the `partID` from an event payload. We check (in order):
|
|
578
|
+
* 1. `properties.partID` (direct events like `message.part.delta`)
|
|
579
|
+
* 2. Top-level `partID` (after sync unwrap, if the data block carries it)
|
|
580
|
+
* 3. `part.id` (sync `message.part.updated.1` — the part object itself
|
|
581
|
+
* carries its own id)
|
|
582
|
+
*/
|
|
583
|
+
function extractPartId(
|
|
584
|
+
obj: Record<string, unknown>,
|
|
585
|
+
part: { id?: string } | undefined,
|
|
586
|
+
): string {
|
|
587
|
+
const props = obj.properties;
|
|
588
|
+
if (props && typeof props === "object") {
|
|
589
|
+
const p = props as Record<string, unknown>;
|
|
590
|
+
if (typeof p.partID === "string" && p.partID.length > 0) return p.partID;
|
|
591
|
+
if (typeof p.partId === "string" && p.partId.length > 0) return p.partId;
|
|
592
|
+
}
|
|
593
|
+
if (typeof obj.partID === "string" && obj.partID.length > 0) return obj.partID;
|
|
594
|
+
if (typeof obj.partId === "string" && obj.partId.length > 0) return obj.partId;
|
|
595
|
+
if (part && typeof part.id === "string" && part.id.length > 0) return part.id;
|
|
596
|
+
return "";
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Strip a trailing version suffix from an event type.
|
|
601
|
+
* e.g. `"session.created.1"` → `"session.created"`, `"foo.bar.baz.12"` → `"foo.bar.baz"`.
|
|
602
|
+
* If the type has no `.N` suffix at the end, it is returned unchanged.
|
|
603
|
+
*/
|
|
604
|
+
function stripVersionSuffix(type: string): string {
|
|
605
|
+
return type.replace(/\.\d+$/, "");
|
|
606
|
+
}
|