Skip to content

feat(team): fix TUI crash, add live model observability, autonomous permission preset, and budget-based model handoff - #19

Merged
Rwanbt merged 6 commits into
devfrom
fix/team-multi-model-selector
Jul 30, 2026
Merged

feat(team): fix TUI crash, add live model observability, autonomous permission preset, and budget-based model handoff#19
Rwanbt merged 6 commits into
devfrom
fix/team-multi-model-selector

Conversation

@Rwanbt

@Rwanbt Rwanbt commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Issue for this PR

Closes #

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Fixes a TUI crash under Team mode and delivers three chantiers of Team autonomy identified by a /plan-eng-review gap analysis against the Team V3 runtime already on this branch:

  • Crash fix: CrashReporter no longer archives EPIPE/broken-pipe errors. A Tauri sidecar losing its parent pipe was triggering a write loop of 1.74M crash reports, and purgeOld()'s unbounded readdirSync().filter().sort() reloaded all of them on every startup, pushing the process into a native OOM kill. purgeOld() is now streaming/bounded, and withStreamingFallback's stitched ReadableStream switched from start() to pull() so it no longer drains the upstream ahead of what the consumer has read (would have compounded memory pressure under Team's nested model fallback chains).
  • Sidebar observability (chantier B): tool/team.ts streams live per-worker session + model assignment via ctx.metadata as each worker starts (mirroring task.ts's existing pattern). The TUI sidebar renders a model badge per active worker and a distinct-model counter, so multiple models actually running is directly visible instead of only inferable from the final report.
  • Autonomous permission preset (chantier A): Team worker sessions get an explicit external_directory: deny instead of falling through to the base agent's default ask, which would hang an unattended run forever on the first out-of-worktree action. In-worktree autonomy is unaffected (Instance.provide already scopes it). Also extracted restrictedWorkerTools() as the single source deriving both the permission[] deny list and the tools{} object, which were duplicated verbatim.
  • Budget-based model handoff (chantier C): application-service.ts tracks cost/tokens per model across finished tasks and excludes any model at ≥95% of the run's budget from subsequent task dispatch; the worker adapter rotates to the next configured model not near its limit. This is a soft, between-tasks proxy (not a true per-provider budget or a mid-generation interruption — both explicitly out of scope), but it means a run can keep making progress on a second model instead of only hard-stopping at 100%.

While getting CI green, also fixed several real pre-existing bugs uncovered along the way (none introduced by this PR, all verified against dev's own history):

  • server/routes/team.ts: a requestBody schema used resolver() in a position hono-openapi's types don't support, and a runRegistry/teamRunRegistry typo that would throw on pause/resume/cancel over HTTP.
  • test/team/run-registry.test.ts and test/plugin/team-cli.test.ts: one called a refactored class statically instead of instantiating it, the other hardcoded a developer-specific absolute path.
  • team/worktree-manager.ts: the same hardcoded-path bug, in production code — would reject every legitimate worktree on any machine but one, including Linux CI.
  • packages/app/src/i18n/team-labels.test.ts: a parity test never exercised 2 of its bundle's 4 accessor functions, silently passing while covering only 8 of 30 declared keys.
  • bus/bus-event.ts: BusEvent.payloads() built its OpenAPI union from Map insertion order, which tracks module-import order — not stable across platforms. Root-caused the sdk-sync/sdk-drift CI failures; sorted deterministically instead.
  • test/plugin/loader-shared.test.ts: a GlobalBus listener was only released on the success path; every call site here exists to exercise an error path, so the listener leaked across ~10 tests, cascading into the full suite (confirmed via MaxListenersExceededWarning and a single test measured at 121s instead of <1s).
  • .github/workflows/storybook.yml: migrated off a stale Blacksmith runner label with no available runner (job stuck queued indefinitely; other Blacksmith-labeled jobs in the same run completed normally, ruling out a wholesale outage).

How did you verify your code works?

  • bun run typecheck (monorepo-wide via turbo) — 15/15 packages, clean.
  • bun test test/team — 815/815 pass.
  • bun test test/bus/bus-event.test.ts, test/plugin/loader-shared.test.ts, test/plugin/install.test.ts, test/model-intelligence/registry.test.ts — all pass in isolation and after fixes.
  • Every fix verified for meaningfulness by temporarily reverting it and confirming the associated test fails, then restoring it.
  • CLI, desktop (Tauri), and Android builds all succeeded from this branch and were manually smoke-tested for the TUI crash fix.
  • The BusEvent sort fix was verified to produce byte-stable openapi.json output across repeated local regenerations.

Screenshots / recordings

Not a UI-visual change beyond a sidebar text badge; no recording captured.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

…ermission preset, and budget-based model handoff

Crash fix:
- CrashReporter no longer archives EPIPE/broken-pipe errors, which were
  causing a write loop that produced 1.74M crash reports and pushed the TUI
  into a native OOM kill.
- purgeOld() now streams and bounds directory enumeration instead of loading
  every report name into memory at once.
- withStreamingFallback's stitched ReadableStream now uses pull() instead of
  start(), so it no longer drains the upstream ahead of what the consumer
  has actually read, which would have compounded memory pressure under
  Team's nested model fallback chains.

Team autonomy (chantiers B/A/C from the plan-eng-review gap analysis):
- Sidebar (B): tool/team.ts streams live per-worker session + model
  assignment via ctx.metadata as each worker starts, mirroring task.ts's
  existing pattern. The TUI sidebar renders a model badge per active worker
  and a distinct-model counter, so multiple models actually running is
  directly visible instead of only inferable from the final report.
- Autonomous permission preset (A): Team worker sessions now get an
  explicit `external_directory: deny` rule. Previously they fell through to
  the base agent's default `ask`, which hangs an unattended run forever on
  the first out-of-worktree action instead of failing it. In-worktree
  autonomy is unaffected (Instance.provide already scopes it). Also
  extracted restrictedWorkerTools() as the single source for the
  permission[]/tools{} deny list, which was duplicated verbatim.
- Budget-based model handoff (C): application-service.ts tracks cost/tokens
  per model across finished tasks and excludes any model at >=95% of the
  run's budget from subsequent task dispatch; opencode-application.ts's
  worker adapter rotates to the next configured model not near its limit.
  This is a soft, between-tasks proxy — not a true per-provider budget or a
  mid-generation interruption, both out of scope for this pass — but it
  means a run can keep making progress on a second model instead of only
  hard-stopping at 100%.

Also fixes a pre-existing implicit-any lint error in server/routes/team.ts
(let control -> let control: TeamRunControl) that was blocking this commit.

Everything here builds on the Team V3 runtime (review, integration,
cherry-pick rollback, TeamStore writes) already delivered on this branch.
MM2-B02-WORKER added 2 commits July 29, 2026 18:50
…hardcoded paths)

- server/routes/team.ts: the PUT /team/config route's requestBody used
  resolver() in a position hono-openapi's types don't support (only
  responses[].content[].schema accepts a Resolver; requestBody.content[].schema
  is the plain OpenAPI SchemaObject type). validator("json", TeamSelection)
  already documents the body via the library's own mechanism, matching every
  other route in this codebase — removed the broken manual field.
- server/routes/team.ts: fixed a typo (`runRegistry` -> `teamRunRegistry`) in
  controlRun() that made pause/resume/cancel over HTTP throw ReferenceError.
- test/team/run-registry.test.ts: TeamRunRegistry was refactored from static
  to instance methods at some point; this test was never updated and called
  the class directly. Instantiate per test instead.
- test/team/team-cli.test.ts: sanity check hardcoded an absolute path into a
  specific developer's local .team-worktrees directory, which no longer
  exists after cleanup. Resolved relative to the test file instead.
- team/worktree-manager.ts: the same class of bug in production code, not
  just a test — the worktree-root fail-closed check hardcoded
  "D:/App/OpenCode/.team-worktrees" instead of deriving it from repoRoot.
  Would reject every legitimate worktree on any machine other than the one
  that wrote it (including Linux CI). Derived from repoRoot instead.
- packages/sdk: regenerated from the server spec after the route fix
  (./script/generate.ts) — was already drifted independently of this PR.

All caught by GitHub Actions CI on PR #19, not by the local pre-push hook,
which trusted a stale turbo cache hit on `opencode:typecheck` instead of
re-running it.
…ed bun version

- team-labels.test.ts: "the declared key list matches the keys the bundle
  actually reads" only invoked selector.missing(), never runStatus()/
  gateVerdict() — two of the bundle's four accessor functions, covering 8 of
  the 30 declared keys. Exercise every declared status/verdict value so the
  read set can actually equal TEAM_LABEL_KEYS.
- openapi.json: my earlier regeneration used the Bun on this machine
  (1.3.14) instead of the version this repo pins (bun@1.3.11 in
  package.json, matched by CI). Confirmed by A/B: 1.3.14 and 1.3.11 produce
  different (but each internally stable across repeated runs) key ordering
  for the same route/event definitions. Regenerated with 1.3.11 downloaded
  directly rather than touching the local bun install, matching what CI's
  sdk-sync check actually compares against.
@github-actions

Copy link
Copy Markdown

This pull request has been automatically closed because it was not updated to meet our contributing guidelines within the 2-hour window.

Feel free to open a new pull request that follows our guidelines.

MM2-B02-WORKER added 3 commits July 30, 2026 00:00
blacksmith-4vcpu-ubuntu-2404 has no available runner — the job sits
queued indefinitely (confirmed stuck >20 minutes on PR #19 while other
Blacksmith-labeled jobs in the same run completed normally, and recent
sdk-sync runs on dev are unaffected, ruling out a wholesale Blacksmith
outage). Every other actively-passing workflow in this repo already runs
on ubuntu-latest; this one was missed in that migration.
registry is a Map, and BusEvent.define() runs as an import-time side
effect across ~30 files — so iteration order tracked module resolution
order, which is not guaranteed stable across platforms/bundlers. That's
exactly what made the generated OpenAPI spec (and downstream SDK)
reproducible on one machine and drift on every other: a Windows-generated
openapi.json never matched what Linux CI regenerated, no matter which Bun
version was used to generate it (verified — the version wasn't the cause).

Confirmed the union member order has no runtime meaning (Zod's
discriminatedUnion dispatches by the "type" literal, not position), and
both consumers of payloads() (event.ts, global.ts) only use it for
OpenAPI schema generation. Sorting alphabetically by type removes the
platform dependency entirely without changing behavior.

Regenerated the SDK with the fix; verified stable across repeated local
runs.
…t just success

errs() registers a GlobalBus "event" listener, then only called .off() after
a successful await Plugin.list() — but every one of this file's ~10 call
sites exists specifically to exercise an error path. A handler left
registered because Plugin.list() threw accumulates across this file's
tests, and since bun test runs a whole suite in one process, it degrades
GlobalBus (confirmed: "MaxListenersExceededWarning: 11 event listeners")
for every test that runs afterward — one test measured at 121s instead of
the usual <1s, with cascading unrelated failures behind it under a full
suite run. Wrapped the await in try/finally so cleanup always runs.

Confirmed fix: the warning is gone and the file's total runtime dropped
from single tests exceeding 60-120s to the whole 27-test file in ~27s.
@github-actions

Copy link
Copy Markdown

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@Rwanbt
Rwanbt merged commit 6f077f2 into dev Jul 30, 2026
17 of 20 checks passed
@Rwanbt
Rwanbt deleted the fix/team-multi-model-selector branch July 30, 2026 07:53
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.

1 participant