feat(team): fix TUI crash, add live model observability, autonomous permission preset, and budget-based model handoff - #19
Merged
Conversation
…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.
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.
|
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. |
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.
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue for this PR
Closes #
Type of change
What does this PR do?
Fixes a TUI crash under Team mode and delivers three chantiers of Team autonomy identified by a
/plan-eng-reviewgap analysis against the Team V3 runtime already on this branch:CrashReporterno longer archives EPIPE/broken-pipe errors. A Tauri sidecar losing its parent pipe was triggering a write loop of 1.74M crash reports, andpurgeOld()'s unboundedreaddirSync().filter().sort()reloaded all of them on every startup, pushing the process into a native OOM kill.purgeOld()is now streaming/bounded, andwithStreamingFallback's stitchedReadableStreamswitched fromstart()topull()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).tool/team.tsstreams live per-worker session + model assignment viactx.metadataas each worker starts (mirroringtask.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.external_directory: denyinstead of falling through to the base agent's defaultask, which would hang an unattended run forever on the first out-of-worktree action. In-worktree autonomy is unaffected (Instance.providealready scopes it). Also extractedrestrictedWorkerTools()as the single source deriving both thepermission[]deny list and thetools{}object, which were duplicated verbatim.application-service.tstracks 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: arequestBodyschema usedresolver()in a position hono-openapi's types don't support, and arunRegistry/teamRunRegistrytypo that would throw on pause/resume/cancel over HTTP.test/team/run-registry.test.tsandtest/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 fromMapinsertion order, which tracks module-import order — not stable across platforms. Root-caused thesdk-sync/sdk-driftCI failures; sorted deterministically instead.test/plugin/loader-shared.test.ts: aGlobalBuslistener 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 viaMaxListenersExceededWarningand 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 stuckqueuedindefinitely; 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.BusEventsort fix was verified to produce byte-stableopenapi.jsonoutput across repeated local regenerations.Screenshots / recordings
Not a UI-visual change beyond a sidebar text badge; no recording captured.
Checklist