Skip to content

Report a failed plugin reload instead of exit 0 - #2155

Merged
SawyerHood merged 1 commit into
mainfrom
bb/fix-2029-plugin-reload-orphans
Aug 21, 2026
Merged

Report a failed plugin reload instead of exit 0#2155
SawyerHood merged 1 commit into
mainfrom
bb/fix-2029-plugin-reload-orphans

Conversation

@SawyerHood

@SawyerHood SawyerHood commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

What was wrong

bb plugin reload reported success for a reload that left the plugin unusable. PluginService.reload() returned void, POST /api/v1/plugins/reload answered { ok: true } unconditionally, and the CLI exited 1 only on ok: false or an unknown id. So when a background service ignored its abort signal, the runtime correctly refused to double-start it, marked the plugin degraded, closed the old instance's database handles, and unloaded it (its bb command gone, the orphaned service ticking The database connection is not open every second), and the reload still returned exit 0 / ok: true. The same held when the new sources failed to load and the runtime kept the previous instance (running with reload failed: …). Issue: #2029 (defect 2). Report: https://get-bb.github.io/reports/issues/2029.html

Defect 1 in the issue (the host rebuilding dist/app.* into a path-installed plugin root) is the documented path-install cache policy (isMutableAppBundleStale, covered by existing tests) and is tracked by #1863; this PR does not change it.

What changed

  • apps/server/src/services/plugins/plugin-runtime.ts: loadOne(row) resolves the load problem (string | null) instead of void. Null means the row's current sources are running now, or the plugin stays disabled by the user's switch. Every failure exit returns its reason; a reload that kept the previous instance says so (… (the previous instance is still running)); the two hung-service exits share hungServicesDetail. Statuses and log lines are unchanged.
  • apps/server/src/services/plugins/plugin-service.ts: reload(id?) returns PluginReloadOutcome: { ok: true, plugins } or { ok: false, error, plugins } where error lists each targeted plugin that did not come up and plugins is the inventory after the reload. The builtin source-watcher's reloadPlugin throws the same problem so the dev loop logs reload failed: … instead of reloaded.
  • apps/server/src/routes/plugins.ts: the route answers 422 { ok: false, error, plugins } on failure; success body is unchanged (pluginReloadResponseSchema still matches). SDK callers (web PluginHealthBanner, mobile useReloadPlugins) already surface a thrown BbHttpError with the server's message. No host daemon wire change, so no HOST_DAEMON_PROTOCOL_VERSION bump.
  • apps/cli/src/commands/plugin.ts: bb plugin reload prints the targeted entries (status and detail) before the error and exit 1; --json already exited 1 on ok: false. bb plugin dev already throws on !ok and its loop logs it.
  • Guide (packages/templates/src/templates/bb-guide-plugins.md) and bb-cli skill (apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md): document the exit code.
  • plugin-registration.ts, plugin-activation.ts: loadOne dep type follows.

How you verified

Tests that fail on origin/main and pass here:

  • apps/server/test/services/plugins/plugin-background.test.ts
    • marks the plugin degraded when a service ignores its abort now asserts the outcome. Before: AssertionError: expected undefined to deeply equal { ok: false, …(2) }.
    • reports a failed reload that kept the previous instance (new): broken edit → running + reload failed: boom on load, outcome ok: false with plugin "keeper" reload failed: boom on load (the previous instance is still running); reload-all reports it; a fixed edit reloads ok: true.
  • apps/server/test/services/plugins/plugin-reload-route.test.ts (new, real app harness): success → 200 { ok: true, plugins }; broken sources → 422 { ok: false, error, plugins: [{ status: "running", statusDetail: "reload failed: boom on load" }] }. Before: AssertionError: expected 200 to be 422.

Commands (from the committed tree):

  • pnpm exec turbo run typecheck --filter=@bb/server --filter=@bb/cli --filter=@bb/templatesTasks: 6 successful, 6 total
  • pnpm exec turbo run test --filter=@bb/cli --filter=@bb/templatesTasks: 8 successful, 8 total (453 + 41 tests)
  • pnpm exec turbo run test --filter=@bb/server → 1825 passed, 1 failed: test/internal/internal-skill-trees.test.ts (mode: 420 vs 436), the known local umask-0002 failure unrelated to this change; it passes in CI.

Manual repro on my own dev instance with the report's fixture (a path plugin whose lane-watcher service captures bb.storage.database() and ignores abort):

$ bb plugin reload collab-fixture
collab-fixture@0.1.0  degraded  (service lane-watcher did not stop)
  source: path:/home/sawyer/.bb-dev/scratch/2029/bb-plugin-collab
plugin "collab-fixture" reload failed: service lane-watcher did not stop
exit=1
$ bb plugin reload --json collab-fixture   # "ok": false, same error, exit=1
$ curl -X POST $BB_SERVER_URL/api/v1/plugins/reload?id=collab-fixture   # HTTP 422

Before this change the same reload printed the degraded entry and exited 0 with "ok": true (report step 3).

Fixes #2029

AGENT GENERATED: by Claude Opus 5

Independent verification

Checked out bb/fix-2029-plugin-reload-orphans (8d19397, one commit on top of origin/main f6fb434; origin/main has not moved since) in a separate worktree and dev instance.

Commands:

  • pnpm install --frozen-lockfile --prefer-offline && pnpm exec turbo run build (18/18 tasks).
  • Fail-before: git checkout origin/main -- apps/cli/src/commands/plugin.ts apps/server/src/routes/plugins.ts apps/server/src/services/plugins/{plugin-activation,plugin-registration,plugin-runtime,plugin-service}.ts, then pnpm exec vitest run test/services/plugins/plugin-background.test.ts test/services/plugins/plugin-reload-route.test.ts from apps/server: 3 failed / 15 passed.
    • marks the plugin degraded when a service ignores its abort: AssertionError: expected undefined to deeply equal { ok: false, …(2) }
    • reports a failed reload that kept the previous instance: TypeError: Cannot read properties of undefined (reading 'ok') at expect(healthy.ok).toBe(true)
    • answers ok:false with the load problem when the new sources did not load (#2029): AssertionError: expected 200 to be 422
  • Pass-after: git checkout HEAD -- <same files>, same vitest run: 18/18 passed.
  • pnpm exec turbo run typecheck --filter=@bb/server --filter=@bb/cli --filter=@bb/templates: Tasks: 6 successful, 6 total.
  • pnpm exec turbo run test --filter=@bb/server --filter=@bb/cli --filter=@bb/templates: cli 453/453, templates 41/41, server 1825 passed / 1 failed (test/internal/internal-skill-trees.test.ts mode 420 vs 436, the known local umask-0002 failure; unrelated, green in CI).
  • CI on the PR: all checks pass (Checks, Package Smoke x2, Tests app-1/2/3, integration, packages, server).

Repro on the fixed branch (own dev instance, report fixture: path plugin whose lane-watcher service captures bb.storage.database() and ignores abort):

$ bb plugin install <fixture> --yes      -> collab-fixture@0.1.0 running; bb collab -> "collab ok", exit=0
$ bb plugin reload collab-fixture
collab-fixture@0.1.0  degraded  (service lane-watcher did not stop)
  source: path:.../bb-plugin-collab
plugin "collab-fixture" reload failed: service lane-watcher did not stop
exit=1
$ bb plugin reload --json collab-fixture -> {"ok":false,"error":"plugin \"collab-fixture\" reload failed: service lane-watcher did not stop"}, exit=1
$ curl -X POST $BB_SERVER_URL/api/v1/plugins/reload?id=collab-fixture -> http=422
$ bb plugin reload                        -> exit=1, prints the degraded entry then the error
$ bb plugin reload automations            -> running, exit=0; raw POST -> http=200

On main the same reload exits 0 with ok: true (report step 3). The orphaned service still logs The database connection is not open every tick, which is the documented degraded contract and out of scope here.

Review notes: root cause (reload outcome never propagated) is fixed in the server, which owns the policy; no host-daemon wire change, so no protocol bump needed. CLI callPlugins already passes 422 bodies through, the dev loop catches the thrown reload, and the SDK surfaces the 422 as BbHttpError with the server message (web banner toasts, mobile mutation toasts). Guide and bb-cli skill document the exit code. Minor, non-blocking: pluginReloadResponseSchema in server-contract still types only the success body, so SDK callers see the failure via BbHttpError.body untyped; web PluginHealthBanner does not invalidate the plugin list on error and relies on the plugins-changed broadcast for the refreshed status. Defect 1 (rebuild into a path plugin root) is intentionally left to #1863.

AGENT GENERATED: by Claude Opus 5

Rebase

Rebased onto origin/main 85eec4da6 (was 33 commits behind; still one commit, now c7cd438b3). git rebase origin/main applied with no conflicts. Main's changes in the touched files are orthogonal to this fix: plugin-runtime.ts gained provider installRank and per-command settings reads (#2148), plugin-service.ts gained agent-tool presentation for grammar v3 (#2164), and the guide/skill docs gained unrelated lines. None of that touches loadOne's exits or reload, so the string | null load problem and the PluginReloadOutcome plumbing map onto the new code unchanged.

Re-proved on the new base. With the six non-test source files reverted to origin/main, plugin-background.test.ts + plugin-reload-route.test.ts: 3 failed / 15 passed (expected undefined to deeply equal { ok: false, …(2) }, Cannot read properties of undefined (reading 'ok'), expected 200 to be 422); restored: 18/18 passed. From the committed tree: turbo typecheck --filter=@bb/server --filter=@bb/cli --filter=@bb/templates --filter=@bb/host-daemon --filter=@bb/sdkTasks: 8 successful, 8 total; turbo test --filter=@bb/cli --filter=@bb/templatesTasks: 8 successful, 8 total (453 + 41 tests); turbo test --filter=@bb/server → 1898 passed, 2 failed: the known local umask internal-skill-trees failure and a 5s timeout in plugin-update.test.ts (waits one full interval…) on a box at load average 128 with swap full; that file passes 27/27 when run alone.

AGENT GENERATED: by Claude Opus 5

Independent verification (post-rebase)

Re-verified the rebased head c7cd438b3 (one commit; merge base with origin/main is 75d6fc4d4, and the only newer main commit, 85eec4da6, is a TestFlight CI change) in a fresh worktree and own dev instance. git merge --no-commit origin/main is clean. Main's intervening changes to plugin-runtime.ts (#2148 provider installRank, per-command settings reads) and plugin-service.ts (#2164 grammar-v3 agent-tool presentation) do not add any exit to loadOne; every exit in the rebased loadOne returns its problem (string | null) and the final path returns null. No provider-bridge-protocol, provider plugin, or agent-runtime file is touched, so no parity run was needed and no HOST_DAEMON_PROTOCOL_VERSION bump is required.

Commands:

  • pnpm install --frozen-lockfile --prefer-offline && pnpm exec turbo run build (18/18 tasks).
  • Fail-before: git checkout origin/main -- apps/cli/src/commands/plugin.ts apps/server/src/routes/plugins.ts apps/server/src/services/plugins/{plugin-activation,plugin-registration,plugin-runtime,plugin-service}.ts, then pnpm exec vitest run test/services/plugins/plugin-background.test.ts test/services/plugins/plugin-reload-route.test.ts from apps/server: 3 failed / 15 passed.
    • marks the plugin degraded when a service ignores its abort: AssertionError: expected undefined to deeply equal { ok: false, …(2) }
    • reports a failed reload that kept the previous instance: TypeError: Cannot read properties of undefined (reading 'ok')
    • answers ok:false with the load problem when the new sources did not load (#2029): AssertionError: expected 200 to be 422
  • Pass-after: git checkout HEAD -- <same files>, same run: 18/18 passed.
  • From the committed tree (git status --porcelain empty): pnpm exec turbo run typecheck --filter=@bb/server --filter=@bb/cli --filter=@bb/templates --filter=@bb/sdk --filter=@bb/host-daemonTasks: 8 successful, 8 total.
  • pnpm exec turbo run test --filter=@bb/server --filter=@bb/cli --filter=@bb/templates: cli 453/453, templates 41/41, server 1899 passed / 1 failed (test/internal/internal-skill-trees.test.ts mode 420 vs 436, the known local umask-0002 failure; unrelated, green in CI).
  • CI on c7cd438b3: all checks pass (Checks, Package Smoke ubuntu + macos, Tests app-1/2/3, integration, packages, server, version check); iOS simulator and Node compat smoke are conditional skips.

Repro on the rebased branch (own dev instance, report fixture: path plugin whose lane-watcher captures bb.storage.database() and ignores abort):

$ bb plugin install <fixture> --yes   -> collab-fixture@0.1.0 running; bb collab -> "collab ok", exit=0
$ bb plugin reload collab-fixture
plugin "collab-fixture" reload failed: service lane-watcher did not stop
exit=1
$ bb plugin reload --json collab-fixture -> {"ok":false,"error":"plugin \"collab-fixture\" reload failed: service lane-watcher did not stop", ...}, exit=1
$ curl -X POST $BB_SERVER_URL/api/v1/plugins/reload?id=collab-fixture -> http=422, same body
$ bb collab                            -> error: unknown command 'collab' (degraded, as designed)
$ bb plugin reload                     -> exit=1 (reports the degraded plugin)
$ bb plugin reload automations         -> running, exit=0; raw POST -> http=200

On main the same reload exits 0 with ok: true (report step 3). Residual, unchanged from the first verification: the orphaned service still logs The database connection is not open every tick (documented degraded contract); pluginReloadResponseSchema types only the success body, so SDK callers see the 422 as a BbHttpError with the server message; defect 1 (rebuild into a path plugin root) is left to #1863.

AGENT GENERATED: by Claude Opus 5

@SawyerHood
SawyerHood marked this pull request as ready for review August 21, 2026 05:59
`bb plugin reload` answered ok:true and exited 0 even when the reload
left the plugin unusable: a previous service that ignored its abort put
the plugin in degraded with nothing loaded (its CLI command gone, its
closed database handles still ticking under the orphaned service), or
the new sources failed to load and the previous instance was kept.
`PluginService.reload` returned void, so neither POST /plugins/reload
nor the CLI could tell.

`loadOne` now resolves the load problem (null once the row's current
sources are running or the plugin stays disabled). `reload` collects the
problems per targeted plugin and returns a PluginReloadOutcome; the route
answers 422 { ok: false, error, plugins } on failure, so the CLI prints
the entries and the reason and exits 1, the dev loop logs "reload
failed", and the app/mobile SDK callers surface the error. The builtin
source watcher's reload throws the same problem. No daemon wire change.

The committed-artifact rebuild in the same issue (#2029 defect 1) is the
documented path-install cache policy and is tracked by #1863.

Co-Authored-By: Claude <noreply@anthropic.com>
@SawyerHood
SawyerHood force-pushed the bb/fix-2029-plugin-reload-orphans branch from 8d19397 to c7cd438 Compare August 21, 2026 16:48
@SawyerHood
SawyerHood merged commit 22d3622 into main Aug 21, 2026
13 checks passed
@SawyerHood
SawyerHood deleted the bb/fix-2029-plugin-reload-orphans branch August 21, 2026 21:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plugin reload: host rebuilds plugin artifacts into the deployed root, and orphaned services survive with closed handles while reload reports exit 0

1 participant