66 KiB
Mixed Team Per-Member Runtime Lanes Plan
Status: proposed implementation plan after code-level confidence pass
Chosen path: preserve the existing primary team bootstrap lane, add secondary per-member runtime lanes only where the primary lane cannot truthfully own a member, starting with OpenCode
Score: 🎯 9 🛡️ 9 🧠 8
Expected size: roughly 1700-2900 lines in claude_team, plus only small additive follow-up work in agent_teams_orchestrator if a provider-specific side-lane seam needs extra metadata
Executive Decision
The first version of mixed teams should not try to turn every provider/model difference into its own lane.
That would be too abstract for the current code and would create avoidable risk.
Instead, V1 should do this:
- keep the current primary team bootstrap lane as the owner for the lead and for teammates that the existing primary team bootstrap can already launch safely
- introduce secondary member lanes only for runtime families that cannot truthfully live on the primary lane
- start with
OpenCodeas the first secondary-lane provider - ship the first mixed rollout for
Anthropic/Codex/Geminileads with oneOpenCodeteammate - explicitly defer
OpenCode-led mixed teams until canonical team ownership through the adapter path is hardened - keep secondary lanes single-member in V1
- allow at most one
OpenCodesecondary lane/member per team in V1 until provider-local OpenCode runtime stores become lane-scoped - keep team-level backend/fast UI in V1
- make member-level backend/fast an additive internal contract first, not a renderer editing surface
- explicitly exclude the current one-shot scheduler from this phase because it is not a team-runtime lifecycle system
This is more reliable than the earlier generic framing and matches the real code much better.
What this feature is really about
This is not a generic rewrite of all mixed-provider behavior.
The current system already has a working primary-lane notion of mixed teammate provider/model/effort for non-OpenCode teammates.
The real missing capability is:
- providers that require a different runtime owner than the primary team bootstrap owner
So V1 should be implemented as a side-lane extension, not as a universal per-provider decomposition rewrite.
Why The Earlier Draft Needed Tightening
The earlier draft had three low-confidence assumptions that were too loose:
- It treated all mixed-provider differences as if they needed lane separation.
- It implied schedule parity through the same coordinator, but the current scheduler does not launch persistent teams.
- It talked about replacing
team -> runmaps wholesale, while a safer path is to keep the primary run maps and add side-lane tracking additively.
After checking the code, those assumptions should be corrected.
What The Current Code Actually Proves
1. The primary team bootstrap path already supports member-level provider/model/effort
TeamProvisioningService already materializes member-level runtime intent through:
materializeEffectiveTeamMemberSpecs(...)resolveAndValidateLaunchIdentity(...)- deterministic team bootstrap inputs built from
effectiveMemberSpecs
That means the primary lane is already more capable than a purely lead-only launch contract.
This is important because it means we should preserve and wrap that path, not bypass it.
2. The runtime adapter registry is currently provider-specific and effectively OpenCode-specific
The runtime adapter layer in:
src/main/services/team/runtime/TeamRuntimeAdapter.tssrc/main/services/team/runtime/OpenCodeTeamRuntimeAdapter.ts
is not a generic multi-provider lane system today.
In practice, this means:
Anthropic,Codex, andGeministill live on the primary team bootstrap pathOpenCodeis the provider that currently forces an alternate launch owner
That is the real reason vector-room-7 broke.
3. Current live tracking is fundamentally primary-team-scoped
TeamProvisioningService still maintains:
provisioningRunByTeamaliveRunByTeamruntimeAdapterRunByTeam
These encode a strong team -> run assumption.
Replacing them all in one shot would be high risk.
The safer path is:
- keep them as the primary lane truth
- add secondary lane tracking next to them
4. team.meta.json is already lead-level launch truth
TeamMetaStore already persists:
providerIdproviderBackendIdmodeleffortfastModelaunchIdentity
This makes team.meta.json the natural lead-level desired and resolved runtime source.
5. members.meta.json is still missing the runtime fields needed for mixed-lane truth
TeamMembersMetaStore currently persists TeamMember[], which already carries:
providerIdmodeleffort
but not:
providerBackendIdfastMode
So the current member metadata is not rich enough to reconstruct mixed-lane runtime identity deterministically.
6. The current scheduler is not a team-runtime lifecycle engine
ScheduledTaskExecutor is a one-shot CLI runner:
- it spawns
claude -p - it uses
ScheduleLaunchConfig - it validates fast mode and provider env
- it does not create or reconcile a persistent team with member lanes
Therefore the current scheduler must be treated as out of scope for mixed-team lane rollout.
The earlier draft was too optimistic there.
7. Backup already includes the right metadata files
TeamBackupService already backs up:
config.jsonteam.meta.jsonmembers.meta.json
This is good news: additive mixed-lane persistence does not require a separate backup architecture.
What it does require is:
- additive field compatibility
- restore/reconcile tests proving those new fields survive backup/restore intact
8. The current OpenCode adapter flattens failures across its own expectedMembers
OpenCodeTeamRuntimeAdapter maps blocked launch and readiness failures across every member in its input.expectedMembers.
This means the coordinator must treat adapter input subsets as a hard invariant:
- if the coordinator passes unrelated members into a side-lane adapter input, the adapter will correctly flatten that failure across all of them
- therefore adapter-local
expectedMembersmust contain only lane-owned members
This is not just an optimization. It is required for truthful failure attribution.
9. The primary lane already supports member-level provider/model/effort
This is an important positive fact from the code:
- teammate spawn prompts include provider/model/effort overrides
buildAgentToolArgsSuffix(...)passesprovider,model, andeffortinto persistent teammate spawn requests- relaunch reconstruction already restores member
providerId/model/effortfrommembers.meta.json
So Anthropic / Codex / Gemini variation inside the primary lane is not hypothetical. The current primary bootstrap already has a real member-level provider/model/effort seam.
That is why V1 should preserve those members on the primary lane instead of over-normalizing them into new side lanes.
10. The primary lane does not yet honor member-level providerBackendId/fastMode
The current teammate spawn seam for the primary lane does not pass member-level:
providerBackendIdfastMode
through the Agent-team spawn contract.
That means the plan must be explicit:
- primary-lane teammates in V1 continue to inherit backend/fast behavior from the lead-side launch/runtime owner
- member-level backend/fast additions are additive persistence contracts first
- side-lane owners may consume those fields earlier than the primary lane
Without this clarification, the plan would over-promise primary-lane capability that the code does not currently have.
11. restoreTeam(...) is not backup restoration
There is another place where terminology can easily mislead implementation:
restoreTeam(...)inTeamDataServicesimply undeletesconfig.deletedAt- startup backup restoration is handled separately by
TeamBackupService.restoreIfNeeded()
So any mixed-lane "restore" design must be explicit about which path it means:
- undelete
- startup recovery from persisted files
- backup restoration after source disappearance
12. members.meta.json already contains a root providerBackendId, but it is not member truth
TeamMembersMetaStore already persists a file-level:
providerBackendId
at the root of members.meta.json.
That is useful as compatibility metadata, but it is not member-scoped runtime truth.
So V1 must be explicit:
- keep the existing root field for backward compatibility
- do not reinterpret that root field as if it were per-member backend truth
- add member-level backend/fast fields separately
13. config.json is intentionally normalized to lead-only before launch
normalizeTeamConfigForLaunch(...) removes teammates from config.json before the primary CLI launch, and assertConfigLeadOnlyForLaunch(...) enforces that invariant.
This is a critical architectural fact:
config.jsonis not a reliable full-roster artifact during launch- mixed-lane design must not rely on
config.jsonas canonical teammate truth - full desired roster must live in
members.meta.json, not in the launch-timeconfig.json
14. Live addMember and restartMember currently go through the lead-side Agent tool seam
buildAddMemberSpawnMessage(...) and buildRestartMemberSpawnMessage(...) tell the lead to spawn teammates through the Agent tool with:
providermodeleffort
and no member-level:
providerBackendIdfastMode- lane ownership metadata
That means the current add/restart seam is only safe for primary-lane teammates.
It cannot be reused unchanged for OpenCode or any future secondary-lane member.
15. Launch roster fallbacks are degraded for mixed-lane truth
resolveLaunchExpectedMembers(...) falls back in this order:
members.meta.json- inboxes merged with
config.jsonprovider/model/effort overrides config.jsonfallback
Only the first path can become authoritative mixed-lane truth.
The inbox/config fallbacks recover at most:
- name
- role
- workflow
- isolation
- provider/model/effort
They do not recover:
- member-level backend
- member-level fast mode
- lane attribution
- resolved member launch identity
So mixed-lane relaunch/startup recovery must treat those fallbacks as degraded and non-authoritative.
16. Primary launch env and provider args are resolved once at the root request
The current create/launch flow calls buildProvisioningEnv(request.providerId, request.providerBackendId) once for the primary request and then carries:
shellEnvproviderArgs- auth/runtime warnings
into the primary spawn.
That means secondary lanes must not inherit primary launch env blindly.
Each secondary lane needs its own provider-aware env/auth/provider-args resolution.
17. TeamMemberResolver and TeamMemberSnapshot are not mixed-lane-aware today
Current team-view membership and runtime labels are still resolved mainly from:
config.membersmembers.meta.json- inbox names
and TeamMemberSnapshot currently carries:
providerIdmodeleffort
but not:
providerBackendIdselectedFastModeresolvedFastModelaneIdlaneKind
So renderer truth will diverge from runtime truth unless these contracts are upgraded explicitly.
18. launch-state.json normalization will drop new mixed-lane fields unless upgraded
normalizePersistedLaunchSnapshot(...) and normalizePersistedMemberState(...) rebuild the persisted launch-state shape field-by-field.
That means additive mixed-lane fields are not automatically safe just because they were written once.
Any mixed-lane schema rollout must therefore include:
- shared type updates
- launch-state write-path updates
- launch-state normalize/read-path updates
Otherwise read-normalize-write cycles will silently erase the new fields.
19. expectedMembers currently means the full expected teammate roster across many callers
Today the codebase uses expectedMembers as a durable "all expected teammates" field in:
- launch-state summaries
- bootstrap recovery
- provisioning progress/presentation
- Team list/detail launch badges
This is not a private primary-lane-only field.
So mixed-lane rollout must not silently repurpose existing expectedMembers to mean "primary bootstrap subset".
If V1 needs both concepts, it must add them explicitly, for example:
expectedMembersas full mixed-team desired rosterprimaryExpectedMembersorbootstrapExpectedMembersas the primary-lane subset
instead of redefining the old field in place.
20. choosePreferredLaunchSnapshot(...) currently prefers by timestamp only
TeamConfigReader and other readers currently pick between bootstrap and persisted launch snapshots using only updatedAt.
That is safe enough today because the snapshots are structurally similar.
It becomes unsafe for mixed teams if:
- bootstrap snapshot is structurally poorer and only knows about the primary lane
- persisted launch-state is structurally richer and includes lane-aware mixed truth
In that world, a newer bootstrap snapshot could silently clobber richer mixed truth.
So the mixed plan must include an explicit precedence rule:
- richer mixed-lane persisted launch-state must not be overridden by a structurally poorer bootstrap snapshot just because it is newer
21. Team summary readers currently have a much smaller launch-state size budget than the main store
Today:
TeamLaunchStateStorereads up to256 KBTeamConfigReadersummary path reads only up to32 KBteam-fs-workersummary path also reads only up to32 KB
So a richer mixed-lane launch-state.json can become perfectly valid for the main runtime path while silently disappearing from Team list / dashboard summary readers.
The plan therefore needs an explicit size-budget rule for mixed-lane persistence.
22. Team summary logic is duplicated between main-thread and fs-worker paths
Mixed launch-state summary behavior is not centralized today:
TeamConfigReaderhas its own launch-state summary path and bootstrap snapshot precedence logicteam-fs-workerhas a separate launch-state summary implementation
They are already not perfectly symmetric, and mixed-lane schema/precedence changes would widen that drift unless the plan explicitly treats them as a parity surface.
23. Post-launch config.json runtime projection is currently lossy
applyEffectiveLaunchStateToConfig(...) projects runtime state back into config.members, but only for:
providerIdmodeleffort
It does not project:
- member backend
- fast mode
- lane ownership
- resolved launch identity
So post-launch config.json is not just non-canonical in principle - it is concretely a lossy projection today.
Mixed rollout must treat it only as a compatibility-facing artifact, never as authoritative runtime reconstruction input.
24. Live roster mutation on running teams is still lead-message-driven and not mixed-safe
Today, when a team is alive:
addMemberpersists metadata, then tells the lead to spawn the teammatereplaceMembersdiffs the roster, sends lead spawn messages for added members, then sends a summary messageremoveMembermarks metadata and sends a generic lead message
That means live roster mutation is fundamentally coupled to:
- lead-owned teammate spawning
- lead-side interpretation of roster changes
- no side-lane ownership contract
So V1 should not treat only "lane migration" as unsafe.
The safer rule is broader: live roster mutation of a running mixed team is out of scope.
25. OpenCode bridge launch is still single-model and team-scoped
OpenCodeLaunchTeamCommandBody currently carries:
- one
selectedModel - one
teamName - one
runId
for the whole OpenCode launch command.
That is a hard seam, not just an implementation detail.
So even before considering runtime stores, the current state-changing bridge contract is not yet a natural fit for:
- multiple independent
OpenCodeside lanes inside one mixed team - different
OpenCodeside-lane members choosing different raw models concurrently
26. OpenCode provider-local runtime stores are team-scoped, not lane-scoped
The current provider-local runtime store namespace lives under:
<team>/.opencode-runtime/manifest.json<team>/.opencode-runtime/launch-state.json<team>/.opencode-runtime/opencode-launch-transaction.json<team>/.opencode-runtime/opencode-permissions.json
And those stores currently encode:
- one
activeRunIdper team manifest - one
activeCapabilitySnapshotIdper team manifest - one active
OpenCodelaunch transaction per team
This means the provider-local OpenCode state layer is still team-scoped single-run state, not a lane namespace that can safely host multiple concurrent OpenCode side lanes for the same logical team.
So V1 must be explicit:
- keep global mixed-team
launch-state.jsonseparate from provider-local.opencode-runtime/launch-state.json - support at most one
OpenCodesecondary lane/member per team - defer multi-
OpenCodemixed teams until the provider-local runtime store namespace is lane-scoped
The Real Problem We Need To Solve
vector-room-7 failed because runtime ownership answered the wrong question.
Today the system effectively asks:
- "does this team contain OpenCode anywhere?"
But it should ask:
- "which runtime owner is allowed to launch each member?"
That distinction matters because:
- the lead and same-family teammates may still be valid on the primary lane
- a specific member such as an
OpenCodeteammate may require a side lane - a side-lane failure must stay attributed to that member or lane only
Goals
- Support mixed teams inside one logical team without splitting into fake subteams.
- Keep one lead and one canonical team identity.
- Make runtime ownership member-scoped where needed.
- Preserve the current primary team bootstrap path for the cases it already handles well.
- Add secondary lanes only where truth requires it.
- Make relaunch and restore deterministic.
- Keep renderer truth honest for partial success and partial failure.
- Roll out additively and safely.
Non-Goals
- No hidden split-team illusion under the hood.
- No big-bang rewrite of every provider into lane adapters.
- No immediate per-member backend/fast editing UI.
- No claim that the current one-shot schedule system becomes mixed-team aware in this phase.
- No optimistic multi-member secondary lane grouping in V1.
V1 Scope Decision
✅ In scope
- primary lane + secondary side-lane coordinator
OpenCodeas the first secondary-lane provider- mixed teams where the lead remains on the existing primary team bootstrap path
- exactly one
OpenCodesecondary teammate per mixed team - member-level lane attribution and persistence
- mixed create/launch/relaunch/restore/stop/reconcile for persistent teams
- truthful renderer diagnostics
❌ Out of scope for V1
- schedule parity through the current
ScheduledTaskExecutor - per-member backend/fast editing controls in the dialogs
- multi-member secondary lane grouping
- more than one
OpenCodeteammate inside the same mixed team - converting
CodexorAnthropicinto side-lane providers unless a future runtime seam truly requires it OpenCode-led mixed teams where the canonical team owner would need to move onto the adapter path
Chosen Runtime Ownership Model
Two Kinds Of Lanes
1. Primary lane
The primary lane is the existing deterministic team bootstrap lane owned by the main team process.
It remains responsible for:
- the lead
- canonical team prompt/session semantics
- teammates that the current primary bootstrap owner can still launch truthfully
This is not a new idea. It is the current working system, formalized as a lane.
2. Secondary lanes
A secondary lane is a side runtime owner for a member that cannot be truthfully launched by the primary lane.
In V1:
- secondary lanes are single-member only
- the first supported owner is
OpenCode - a secondary lane never becomes the canonical team owner
This keeps the model simple and reliable.
Compatibility Rule
The compatibility question in V1 is not:
- "same provider?"
- "same model?"
- "same effort?"
The real question is:
- "is this member allowed to remain owned by the current primary team bootstrap lane?"
V1 compatibility policy
A member stays on the primary lane when:
- their provider/runtime family is supported by the primary team bootstrap owner
- no runtime family declares that the member must be isolated into a secondary lane
A member moves to a secondary lane when:
- their runtime family explicitly requires a separate runtime owner
Initial concrete policy
In V1:
anthropic-> primary-lane eligiblecodex-> primary-lane eligiblegemini-> primary-lane eligibleopencode-> secondary-lane required
This is intentionally specific because that is what the code currently supports.
Lead policy in V1
For the first shipping phase:
- mixed teams are supported when the lead is
anthropic,codex, orgemini - pure
opencodeteams continue to use the existing runtime adapter path opencode-led mixed teams are deferred
This keeps canonical team ownership on the already-proven primary bootstrap path for V1 mixed support.
Why We Are Not Using “Any Difference = New Lane”
That approach looks elegant on paper but is the wrong tradeoff here.
It would force us to:
- duplicate or replace working bootstrap behavior for
Codex/Anthropic/Gemini - redesign the renderer editor prematurely
- massively widen relaunch and restore risk
The better architecture is:
- generic coordinator
- specific V1 ownership policy
- a separate later phase for adapter-owned mixed primary lanes if we need them
That gives us scalability later without pretending the current code is more symmetric than it really is.
Source Of Truth Matrix
The most important plan improvement is to separate desired truth from resolved truth.
| Artifact | Responsibility in V1 | Source kind |
|---|---|---|
team.meta.json |
Lead-level desired runtime request and resolved lead launch identity | desired + resolved |
members.meta.json |
Member desired runtime contract overrides | desired |
launch-state.json |
Per-member resolved launch outcome, lane assignment, resolved launch identity | resolved |
| in-memory run maps | Live lane ownership and stop/reconcile handles | live runtime only |
Rules
team.meta.jsonis the source of truth for the lead requestmembers.meta.jsonis the source of truth for member overrideslaunch-state.jsonis the source of truth for the most recent resolved lane/member execution- live maps must never become the only source of member lane ownership
config.jsonis not canonical teammate-roster truth during launch because it is intentionally normalized to lead-only- post-launch
config.jsonruntime projection is lossy and compatibility-facing only - provider-local
OpenCoderuntime stores under.opencode-runtime/*are a separate provider-owned namespace, not the canonical mixed-team snapshot - if a compact launch summary projection is introduced for Team list / dashboard, it must be derived from the same canonical snapshot precedence rules as the main runtime path
- the root
members.meta.json.providerBackendIdremains compatibility metadata, not member-scoped runtime truth - team-view/runtime labels must eventually be derived from lane-aware snapshot truth, not only from
config + meta + inboxes - existing
launch-state.expectedMemberssemantics must remain explicit and backward-safe; do not silently redefine it to mean primary-only subset
Contracts To Add Or Tighten
1. TeamProvisioningMemberInput
Extend src/shared/types/team.ts:
export interface TeamProvisioningMemberInput {
name: string
role?: string
workflow?: string
isolation?: 'worktree'
providerId?: TeamProviderId
providerBackendId?: TeamProviderBackendId
model?: string
effort?: EffortLevel
fastMode?: TeamFastMode
}
Reason:
- member desired runtime contract must be able to express full future lane identity
- even if the renderer does not expose backend/fast editors in V1, the contract must exist additively
V1 semantic rule
In the shipping V1 scope:
- member
providerId/model/effortare meaningful for both primary-lane and secondary-lane members - member
providerBackendId/fastModeare meaningful for secondary-lane planning and future compatibility - primary-lane teammates still inherit effective backend/fast behavior from the lead/runtime owner
2. TeamMember
Extend TeamMember additively with:
providerBackendId?: TeamProviderBackendIdfastMode?: TeamFastMode
Guardrail
config.json writers must not start depending on those fields.
They may remain omitted in canonical CLI-facing config writes until the CLI actually needs them.
This lets members.meta.json evolve without silently changing config.json semantics.
3. TeamMemberSnapshot
Extend renderer-facing member truth with:
providerBackendId?: TeamProviderBackendIdselectedFastMode?: TeamFastModeresolvedFastMode?: booleanlaneId?: stringlaneKind?: 'primary' | 'secondary'
This is needed so Team Detail can show truthful member runtime identity without flattening everything into team-level labels.
Guardrail
This is not optional polish.
The mixed feature must stay dark until TeamMemberSnapshot and the resolver path can consume lane-aware truth. Otherwise runtime behavior will be correct while Team Detail still lies.
4. PersistedTeamLaunchMemberState
Additive fields required:
providerId?: TeamProviderIdproviderBackendId?: TeamProviderBackendIdlaunchIdentity?: ProviderModelLaunchIdentitylaneId?: stringlaneKind?: 'primary' | 'secondary'laneOwnerProviderId?: TeamProviderId
This is the minimum needed for deterministic relaunch and accurate diagnostics.
Guardrail
Because normalizePersistedLaunchSnapshot(...) rebuilds the persisted structure explicitly, this schema work is incomplete until the normalizer also roundtrips these additive fields safely.
5. TeamAgentRuntimeEntry
Extend live runtime snapshot entries with:
providerId?: TeamProviderIdproviderBackendId?: TeamProviderBackendIdlaneId?: stringlaneKind?: 'primary' | 'secondary'
This is a live presentation contract only.
It does not replace persisted truth.
6. MemberDraft
Do not expose member-level backend/fast in the UI in V1.
But for future-proofing, the internal draft type may be extended additively later if needed.
In V1:
- renderer editing remains
providerId/model/effort - backend/fast remain inherited internal fields
That is the safer scope boundary.
Domain Model
Desired member contract
type DesiredTeamMemberRuntimeContract = {
memberName: string
providerId: TeamProviderId
providerBackendId: TeamProviderBackendId | null
model: string | null
effort: EffortLevel | null
fastMode: TeamFastMode | null
cwd: string
}
This is reconstructed from:
team.meta.jsonlead defaultsmembers.meta.jsonmember overrides
Resolved member runtime identity
type ResolvedTeamMemberRuntimeIdentity = {
memberName: string
role: 'lead' | 'member'
desired: DesiredTeamMemberRuntimeContract
launchIdentity: ProviderModelLaunchIdentity
laneRequirement: 'primary-eligible' | 'secondary-required'
}
This is computed after provider-aware resolution.
Lane plan
type TeamRuntimeLanePlan = {
laneId: string
laneKind: 'primary' | 'secondary'
ownerProviderId: TeamProviderId
memberNames: string[]
launchMode: 'existing-primary-bootstrap' | 'runtime-adapter'
}
Important V1 simplification
Secondary lanes are single-member only, so:
laneKind === 'secondary'always impliesmemberNames.length === 1- in the shipping V1 scope, only one secondary
OpenCodelane may exist for a team - if a mixed draft includes more than one
OpenCodeteammate, the planner must reject it explicitly instead of silently grouping or serializing them
This is deliberate and should be written into the code as a policy, not left as an implicit side effect.
Runtime Ownership Architecture
New coordinator
Introduce a feature-owned coordinator:
src/features/team-runtime-lanes/main/composition/createTeamRuntimeLaneCoordinator.ts
Suggested facade:
TeamRuntimeLaneCoordinator
Responsibilities
- read desired lead/member runtime contracts
- resolve member runtime identity
- partition members into primary vs secondary lanes
- launch the primary lane through the existing
TeamProvisioningServicepath - launch each secondary lane through an adapter owner
- merge lane results back into one aggregate team snapshot
- stop and reconcile all lanes
Hard rules
- adapters never get unrelated members
- a secondary-lane failure cannot overwrite primary-lane member states
- in the shipping V1 mixed scope, the lead remains owned by the existing primary bootstrap lane
Why the coordinator must preserve the primary path
The current primary path already owns:
- deterministic bootstrap
- lead inbox/session continuity
- much of the team lifecycle
So the coordinator should wrap it, not replace it.
That means the architecture is:
- existing primary provisioning path remains the canonical team owner
- secondary lanes are additive sidecars
This is safer than trying to make every provider look like a fully independent team owner.
In-Memory Tracking Model
The earlier draft said to replace team -> run maps.
That is too broad for V1.
Safer V1 tracking decision
Keep:
provisioningRunByTeamaliveRunByTeam
as the primary-lane run markers.
Add:
type SecondaryLaneRunRecord = {
laneId: string
providerId: TeamProviderId
memberName: string
runId: string
cwd?: string
}
and:
private readonly secondaryLaneRunsByTeam = new Map<string, Map<string, SecondaryLaneRunRecord>>()
Optionally also add:
private readonly secondaryLaneProgressByRunId = new Map<string, TeamProvisioningProgress>()
Why this is safer
- most of the codebase already assumes one primary team run
- mixed lanes only need additive side-lane tracking
- stop/reconcile/restore can aggregate over the new side map without destabilizing every existing caller
Create And Launch Flow
Critical bootstrap invariant
The current primary bootstrap flow explicitly embeds the teammate roster into:
- deterministic bootstrap spec
members - primary run
expectedMembers - primary-lane member spawn tracking
So a mixed-lane implementation must not treat those structures as the full mixed-team roster.
🚨 V1 rule:
- secondary-lane members must be excluded from the primary bootstrap spec
- secondary-lane members must be excluded from the primary in-memory
expectedMembers - aggregate mixed-team progress must track the full desired roster separately
Otherwise the primary lane will wait forever for members that are intentionally being launched elsewhere.
Step 1 - persist desired contracts
Before runtime launch:
- write
team.meta.jsonwith lead request and lead launch identity seed - write
members.meta.jsonwith full roster and member desired overrides
Rule
Canonical team artifacts must always describe the full roster.
No provider-specific adapter is allowed to "own" who belongs in the roster.
Additional rule
That full roster must be persisted in:
members.meta.json- mixed-lane-aware
launch-state.json
and not inferred from launch-time config.json, because the current launch flow intentionally strips teammates out of config.json before spawning the lead CLI.
Step 2 - resolve member runtime identity
Resolve:
- lead desired contract
- each member desired contract
- resolved runtime identity for each member
This uses the same provider-aware resolution services already used for team launch, but now produces member-scoped output.
Step 3 - partition lanes
Partition members into:
- primary-lane members
- secondary-lane members
V1 partition policy
- lead is always primary
- primary-eligible members stay primary
OpenCodemembers become one-member secondary lanes
Bootstrap subset rule
After partitioning:
primaryBootstrapMembers = lead + primary-eligible teammatessecondaryLaneMembers = secondary-required teammates
Only primaryBootstrapMembers may feed:
buildDeterministicCreateBootstrapSpec(...)buildDeterministicLaunchBootstrapSpec(...)- primary-lane
expectedMembers - primary-lane
memberSpawnStatuses
Step 4 - launch primary lane first
Use the existing primary team bootstrap flow for:
- lead
- all primary-lane teammates
This preserves current session and deterministic bootstrap assumptions.
Step 5 - launch secondary lanes after primary readiness
Once the primary lane reaches the point where it can truthfully own the team context:
- launch secondary lanes
- one side lane per member in V1
Concurrency
Allow bounded parallelism only if provider-specific side-lane launches are proven safe.
Default to conservative small concurrency.
Join semantics
Secondary lanes must behave like post-bootstrap teammate attachments, not hidden members inside the original primary bootstrap transaction.
That means:
- the primary lane becomes ready against its own subset
- secondary lanes attach afterward
- mixed-team aggregate progress remains launching until both:
- the primary lane is ready enough
- every planned secondary lane has either joined or failed
Explicit V1 mixed-team shipping matrix
Supported in V1
Anthropic lead + one OpenCode teammateCodex lead + one OpenCode teammateGemini lead + one OpenCode teammate- all existing pure non-OpenCode teams
- all existing pure OpenCode teams
Deferred after V1
- more than one
OpenCodeteammate inside the same mixed team OpenCode lead + Codex teammateOpenCode lead + Anthropic teammateOpenCode lead + Gemini teammate
Those cases require canonical team ownership on the adapter path and should be treated as a separate risk class.
Step 6 - merge lane outcomes
Merge:
- primary lane member evidence
- secondary lane member evidence
into one launch-state.json snapshot.
Guardrail
Aggregate team state must be derived from member states, never the other way around.
Aggregate roster truth
The mixed-team aggregate snapshot should distinguish:
- full desired roster for the whole launch
- primary bootstrap expected members only
Without that distinction, UI and restore logic will confuse:
- "primary bootstrap is still waiting" with
- "a side lane has not attached yet"
Backward-safe field rule
Because current readers and UI already treat expectedMembers as the full expected teammate roster, the mixed schema should add a new explicit field for the primary subset rather than silently changing the meaning of expectedMembers.
Relaunch And Restore
This remains one of the highest-risk parts.
Relaunch source rules
When relaunching:
- desired contract comes from
members.meta.json+ lead-levelteam.meta.json - previous resolved member/lane truth comes from
launch-state.json - current global provider settings are fallback only when stored team/member truth is absent
Why this matters
The current relaunch flow:
- stops the team
- replaces members
- launches again
So if we do not persist member lane truth explicitly, relaunch will drift back toward team-level assumptions.
Restore rules
On restore or startup reconciliation:
- restore primary lane from primary persisted state
- restore or reconcile secondary lanes from per-member lane attribution in
launch-state.json - if a secondary lane cannot be restored, only that member is degraded or failed
Snapshot precedence rule
If both bootstrap-derived state and persisted launch-state exist:
- do not choose only by
updatedAt - prefer the snapshot that preserves mixed-lane structural truth
- a newer but structurally poorer bootstrap snapshot must not overwrite richer lane-aware launch-state
Explicit non-goal
Do not reinterpret a previously mixed team as a pure team just because side-lane state is missing.
Stop And Cleanup
Ordering decision
For stop/relaunch:
- stop secondary lanes first
- stop primary lane second
Why this order
- prevents side-lane members from being left attached to a dead team owner
- reduces orphaned-runtime risk
Cleanup obligations
stopTeam(...) must:
- stop all secondary lanes
- stop the primary lane
- clear additive side-lane tracking maps
- persist a reconciled launch snapshot
No stop path may claim success while a side-lane member is still live.
Live Edit And Add-Member Boundary
This is another high-risk area that the earlier draft was too soft about.
Today:
Edit TeamusesreplaceMembers(...)- runtime-affecting edits on a live team rely on
restartMember(...) Add memberis a post-bootstrap attachment flow
Those semantics are not equivalent for mixed lanes.
V1 policy
Allowed
- fresh mixed create
- mixed relaunch
- mixed stop/start
Explicitly blocked
- live
addMemberon a running mixed team in V1 - live
replaceMemberson a running mixed team in V1 - live
removeMemberon a running mixed team in V1 - live editing an existing member from primary-lane ownership to secondary-lane ownership
- live editing an existing member from secondary-lane ownership back to primary-lane ownership
- treating a lane migration as a simple restart
- using the current lead Agent tool restart flow as if it were a safe secondary-lane restart/migration path
Why this is the right boundary
addMember(...) is already conceptually a post-bootstrap attachment flow.
restartMember(...) is not. It assumes the same runtime ownership model still applies.
And for secondary lanes there is one more hard constraint:
- the current add/restart seam is lead-owned Agent tool spawning with only provider/model/effort overrides
- that seam is insufficient for side-lane runtime owners like
OpenCode
So the safer V1 boundary is:
- ship fresh launch / relaunch / stop / startup recovery for mixed teams
- defer live roster mutation of running mixed teams
- defer live side-lane add-member and live lane migration to a later phase
Renderer Truth
V1 renderer scope
Do not redesign the team editor UI.
Do:
- show lead and members with truthful runtime identity
- show that some members are on secondary lanes when useful
- keep partial-failure attribution member-scoped
Shipping rule
Mixed-team execution must not be enabled for real users until TeamMemberSnapshot and TeamMemberResolver can surface lane-aware truth.
Without that, runtime behavior and Team Detail diverge immediately.
Required visible truth
- lead row must always exist
- member rows must not inherit another member's failure
- provider/runtime labels must be per member, not flattened from one team-wide owner
Current lead bug tie-in
The vector-room-7 missing lead issue proves one more rule:
- lead visibility must not depend on provider-specific config writing quirks
The canonical roster must remain team-owned, not adapter-owned.
Scheduler Boundary
This part needed the strongest correction.
Current scheduler reality
ScheduledTaskExecutor is a one-shot CLI job runner.
It is not a persistent team-runtime lifecycle owner.
Therefore this feature should not claim:
- mixed-team lane execution through the current schedule system
- schedule parity with the persistent mixed-team coordinator
V1 schedule policy
- existing
ScheduleLaunchConfigremains lead-level only - current schedules remain independent one-shot prompt runs
- mixed-team lane coordination is explicitly out of scope for this feature
Terminology clarification
This repository currently has multiple different "restore" concepts:
restoreTeam(...)inTeamDataServiceis undelete ofconfig.deletedAtTeamBackupService.restoreIfNeeded()is startup backup restoration- startup reconciliation also reads persisted launch state
This feature plan uses:
- relaunch for stop + launch again with persisted team metadata
- startup recovery for recovering mixed-lane runtime truth from persisted files after app/runtime interruption
Keeping those concepts separate will reduce implementation mistakes.
Future extension
If later the product adds a true "scheduled team relaunch / scheduled team run" feature, that feature must call the same TeamRuntimeLaneCoordinator.
But that is a different system than the current scheduler.
Diagnostics And Observability
The coordinator must emit:
- member desired contract summary
- lane assignment summary
- per-lane prepare result
- per-member launch attribution
- aggregate launch snapshot revision
Example diagnostics
member alice stays on primary lane (codex primary bootstrap)member tom routed to secondary lane opencode:tommember bob routed to secondary lane opencode:bob
This is essential. Mixed teams will be too hard to debug without explicit lane attribution.
Migration And Compatibility
Read compatibility
Old teams remain valid:
- missing member
providerBackendId-> inherit lead/default provider backend - missing member
fastMode->inherit - missing member lane attribution in
launch-state.json-> assume primary-lane if the member is primary-eligible
Important limit
That last fallback must be used only for clearly primary-eligible providers.
Do not silently infer OpenCode members back onto the primary lane.
Mixed-Lane Degraded Fallback Policy
This needed a stricter statement after the code pass.
Pure primary-lane teams
For existing non-mixed teams, the current degraded launch-roster fallback behavior can stay:
members.meta.json- inboxes +
config.json config.json
because the primary lane already understands member provider/model/effort in that model.
Persisted mixed-lane teams
For teams that already have mixed-lane evidence, the coordinator must treat these as authoritative inputs:
members.meta.jsonlaunch-state.json
If those files are missing or inconsistent, the product must not silently rebuild a mixed-lane plan from inboxes/config fallback only.
Required degraded behavior
If mixed-lane evidence is missing:
- recover only the canonical team summary and visible roster if possible
- mark runtime/lane reconstruction as degraded
- block or require a fresh relaunch/bootstrap for mixed-lane execution
This is safer than pretending inbox/config fallback can recover member backend/fast/lane truth when the current code proves it cannot.
Write policy
All new mixed launches must persist:
- member desired overrides in
members.meta.json - member resolved lane attribution in
launch-state.json
Do not remove old team-level fields during rollout.
Existing relaunch compatibility we can reuse
The current relaunch path already reconstructs member providerId/model/effort from members.meta.json when relaunching a team.
So mixed-lane persistence work should focus mainly on adding:
- lane attribution
- member-level backend/fast metadata
- resolved launch identity where needed
and should not re-solve the already-working member provider/model/effort relaunch behavior from scratch.
Backup And Restore Compatibility
Because backup already captures team.meta.json and members.meta.json, this feature should follow a simple compatibility rule:
- only additive field changes in those files during V1
- no rename or shape break of the existing root files
Required tests
- backup a team with mixed-lane additive member metadata
- restore it
- verify
team.meta.json,members.meta.json, andlaunch-state.jsonstill reconstruct the same desired + resolved member truth
Dangerous And Thin Places
1. Treating lane compatibility as model/effort math instead of runtime ownership policy
This would produce an elegant but wrong planner.
Guardrail:
- first ask which runtime owner is allowed
- only then talk about model/effort/backend specifics
2. Letting side-lane adapters write canonical roster artifacts
That caused the missing-lead bug class already.
Guardrail:
- canonical roster is always written by team-owned code
3. Replacing the primary run maps too early
That would destabilize a lot of lifecycle code unrelated to mixed teams.
Guardrail:
- keep primary maps
- add side-lane maps
4. Claiming schedule parity too early
This would create a misleading plan and likely bugs.
Guardrail:
- keep current scheduler explicitly out of scope
5. Allowing multi-member secondary lanes in V1
That creates grouping bugs before ownership is stable.
Guardrail:
- V1 side lanes are single-member only
6. Mixing primary bootstrap expectations with aggregate mixed-team progress
This would create stuck launches and misleading "Members joining" states.
Guardrail:
- primary in-memory
expectedMembersis subset-only - persisted launch-state
expectedMembersstays the full expected roster unless a new explicit field is introduced for the bootstrap subset - aggregate desired roster is tracked separately
7. Treating live lane migration as a simple restart
This would corrupt ownership and attribution.
Guardrail:
- block lane-migration edits in V1
- implement migration later as a separate coordinator-backed increment
8. Assuming primary-lane teammates already support member-level backend/fast
This would create hidden runtime drift between persisted desired state and actual spawn behavior.
Guardrail:
- document that primary-lane teammates inherit backend/fast from the lead/runtime owner in V1
- only secondary-lane planning may consume member-level backend/fast earlier
9. Passing non-owned members into a side-lane adapter input
This would cause provider-local flattening of failures back onto unrelated members.
Guardrail:
- adapter inputs must be lane-local subsets only
- add tests where one side-lane member fails and no other member inherits that failure
10. Trusting config.json as canonical mixed-team roster truth
The launch path intentionally normalizes config.json to lead-only.
Guardrail:
config.jsonmay remain CLI-facing and lead-centric- mixed full-roster truth must come from
members.meta.jsonand mixed-lane-awarelaunch-state.json
11. Reusing the current lead Agent tool add-member/restart seam for secondary lanes
That seam only carries provider/model/effort and assumes the lead owns the teammate spawn.
Guardrail:
- coordinator-managed secondary-lane attachment must bypass the generic lead Agent tool spawn-message path
- V1 must not claim secondary-lane restart/migration support through the current restart helper
12. Treating inbox/config fallback as authoritative mixed-lane recovery
That would silently erase member backend/fast/lane truth on relaunch/startup recovery.
Guardrail:
- inbox/config fallback may recover names and basic provider/model/effort only
- persisted mixed-lane teams require
members.meta.json+launch-state.jsonfor authoritative recovery - if those are missing, degrade explicitly instead of inventing a mixed plan
13. Reusing primary-lane env/provider args for side lanes
Primary create/launch resolves env/auth/provider args once for the root request.
Guardrail:
- every secondary lane resolves its own env/auth/provider args
- do not inherit root request provider args into side-lane launches
14. Treating renderer parity as optional polish
Today TeamMemberResolver is built from config + meta + inboxes, not lane-aware launch-state truth.
Guardrail:
- lane-aware
TeamMemberSnapshotand resolver parity must land before mixed support is enabled - do not treat this as a post-shipping cosmetic cleanup
15. Adding mixed-lane fields without upgrading launch-state normalization
normalizePersistedLaunchSnapshot(...) reconstructs known fields and would otherwise drop new member-level lane/runtime data.
Guardrail:
- any mixed-lane schema change must ship with normalizer support
- schema upgrade is not complete until read-normalize-write roundtrips preserve the new fields
16. Quietly redefining expectedMembers
That field is already used widely for launch badges, summaries, provisioning presentation, and recovery.
Guardrail:
- do not change
expectedMembersfrom "full expected roster" to "primary subset" - add a new explicit field for bootstrap/primary expected members instead
17. Choosing bootstrap vs persisted launch snapshot by timestamp only
Mixed teams introduce a structurally richer launch-state than bootstrap-only recovery can express.
Guardrail:
- timestamp-only precedence is not enough once mixed-lane fields exist
- snapshot selection must prefer structurally richer mixed-lane truth over a newer but poorer bootstrap snapshot
18. Letting mixed-lane launch-state outgrow Team list / dashboard summary readers
Main runtime storage and summary readers currently have different size budgets.
Guardrail:
- mixed-lane rollout must define an explicit launch-state size budget for summary-safe operation
- preferred V1 answer is a compact summary projection written from canonical snapshot truth, not a blind increase of all reader limits
- if that compact projection does not exist, rollout must stay blocked until reader limits, performance, and summary parity are explicitly re-validated
19. Updating summary logic only in one of the two reader paths
Team summaries are read both in-process and through team-fs-worker.
The current worker path does not use choosePreferredLaunchSnapshot(...) and does not read bootstrap launch snapshots. It only reads raw persisted launch-state.json.
Guardrail:
- schema, precedence, and summary derivation changes must land in both paths together
- preferred V1 answer is a shared summary derivation contract or compact summary artifact consumed by both paths
- if parity cannot be guaranteed, mixed launch-state summary should stay dark rather than diverge between worker and fallback modes
20. Treating post-launch config.json runtime projection as authoritative mixed truth
The existing config projection writes only provider/model/effort and is intentionally lossy.
Guardrail:
- never reconstruct mixed-lane runtime truth from projected
config.json - keep config projection compatibility-focused and minimal unless the CLI itself requires more
21. Allowing live roster mutation on running mixed teams in V1
Current live add/remove/replace flows all route through lead-side messaging and primary-lane assumptions.
Guardrail:
- V1 mixed support should block live roster mutation on running mixed teams
- require stop/edit/relaunch until a coordinator-owned live mutation model exists
22. Treating provider-local OpenCode runtime stores as if they were already lane-scoped
The current .opencode-runtime namespace is team-scoped and single-active-run-scoped.
Guardrail:
- keep provider-local
OpenCodelaunch-state/manifest/transaction files separate from global mixed-teamlaunch-state.json - do not multiplex multiple
OpenCodeside lanes into the same provider-local store namespace in V1
23. Assuming multiple OpenCode side lanes are possible without changing the bridge contract
The current OpenCode launch command takes one selectedModel for the whole provider-owned run.
Guardrail:
- V1 mixed support must reject drafts with more than one
OpenCodeteammate - future multi-
OpenCodesupport requires both lane-scoped runtime-store namespacing and a bridge/API seam that can represent lane-local model ownership truthfully
24. Reusing the legacy "expected teammates vs confirmed artifacts" summary fallback for mixed teams
Current Team list / dashboard fallback summary logic infers partial launch failure by comparing:
- expected teammate names from config/meta
- confirmed artifacts such as config members and inbox files
That heuristic is not lane-aware.
For mixed teams, a side-lane member can be legitimately absent from primary-lane artifacts during bootstrap or attachment without meaning "launch failed".
Guardrail:
- once a team is mixed-aware, summary fallback must not reuse the legacy artifact-count heuristic as if it were canonical launch truth
- mixed teams should prefer canonical lane-aware snapshot/projection truth
- if that truth is missing, summary should degrade to unknown/pending rather than inventing a partial failure
25. Reusing the current primary-run persistLaunchStateSnapshot(...) as the canonical mixed snapshot writer
Today persistLaunchStateSnapshot(run) writes launch-state.json from one ProvisioningRun and uses:
run.expectedMembersrun.memberSpawnStatusesrun.provisioningComplete
That is safe for a single primary run, but unsafe for mixed teams because:
- primary
run.expectedMembersis only the bootstrap subset - a clean-success primary run currently clears persisted launch-state entirely
- aggregate mixed truth may still include pending or failed side lanes
Guardrail:
- mixed teams need an aggregate snapshot writer owned by the coordinator, not direct reuse of the current primary-run persistence helper as canonical truth
- primary-lane snapshot persistence may remain lane-local input, but it must not overwrite or clear the global mixed-team launch snapshot on its own
26. Letting createPersistedLaunchSnapshot(...) auto-fail not-yet-attached side-lane members
Current createPersistedLaunchSnapshot(...) upgrades members from starting to failed_to_start whenever:
launchPhase !== 'active'- the member never reached spawned/alive/confirmed signals
That is a good repair for single-lane crashed launches, but dangerous for mixed teams if:
- the primary lane flips out of
active - a planned side lane has not attached yet
- the aggregate launch is not actually terminal
Guardrail:
- aggregate mixed
launchPhasemust stayactiveuntil every planned side lane is terminal or explicitly removed from the plan - primary-lane completion must not by itself trigger terminalization of still-planned side-lane members
- if V1 cannot enforce that invariant, mixed aggregate snapshot creation must use a separate terminalization policy instead of reusing the default helper unchanged
27. Reusing bootstrap-specific pending copy for mixed side-lane attachment
Current Team list / Team detail copy for partial_pending talks in bootstrap terms such as:
- teammate still joining
- runtime pending bootstrap
For mixed teams, partial_pending may instead mean:
- primary lane is already ready
- one side lane has not attached yet
- one side lane is reconciling provider-local runtime truth
Guardrail:
- mixed renderer states must not over-interpret aggregate
partial_pendingas bootstrap-only - once a team is mixed-aware, pending copy should become lane-aware or neutral, for example "launch still reconciling" rather than "pending bootstrap"
Rollout Plan
Phase 0 - Additive contracts and persistence parity
Commit boundary: feat(team-runtime): add additive mixed-lane contracts
🎯 10 🛡️ 10 🧠 4
Roughly 420-750 lines
Scope
- extend shared member runtime contracts with
providerBackendIdandfastMode - extend
TeamMemberSnapshotand persisted launch member contracts with lane-aware additive fields - extend
members.meta.jsonread/write shape - extend launch-state member shape additively
- upgrade launch-state normalizer/read path so the new fields survive roundtrip
- add explicit mixed-safe roster fields instead of redefining
expectedMembers - define a mixed-safe launch-state size budget for summary readers
- choose a summary-safe read strategy for Team list / dashboard, preferably a compact summary projection derived from canonical snapshot precedence
- define aggregate mixed-team snapshot ownership so primary-lane persistence helpers cannot overwrite or clear canonical mixed truth
- no behavior change yet
Tests
- backward-compatible meta read/write
- backward-compatible launch-state normalization
- launch-state roundtrip preserves new additive mixed-lane fields
- old readers still see
expectedMembersas full expected roster - summary readers continue to read mixed launch-state within the agreed size budget
- summary strategy tests prove worker and in-process readers agree even when bootstrap snapshot and persisted launch-state disagree on recency
- aggregate snapshot tests prove primary-lane clean-success does not clear pending mixed side-lane truth
- member contract serialization tests
- backup/restore compatibility for additive member runtime metadata
- explicit semantic tests proving primary-lane teammates still inherit backend/fast in V1
TeamMemberSnapshotcompatibility tests for sparse old teams
Phase 1 - Coordinator shell plus resolver parity
Commit boundary: feat(team-runtime): introduce mixed-lane coordinator shell
🎯 9 🛡️ 10 🧠 6
Roughly 520-900 lines
Scope
- add
TeamRuntimeLaneCoordinator - keep current primary launch path untouched behaviorally
- add side-lane tracking maps without enabling mixed behavior yet
- make
TeamMemberResolverand related team-view snapshot assembly lane-aware for additive fields - centralize Team summary derivation so
TeamConfigReaderandteam-fs-workerconsume the same mixed-summary contract instead of reimplementing snapshot choice independently - introduce coordinator-owned aggregate launch-state assembly so lane-local progress feeds one canonical mixed snapshot
- primary-only teams remain identical
Tests
- single-provider create/launch parity
- no regression in pure
OpenCodegated path - no regression in stop/reconcile for primary-only teams
- no regression in existing mixed primary-lane member
provider/model/effortbehavior - team view remains stable for old teams with no mixed-lane fields
- lead synthesis still works when lane-aware fields are missing
- worker and in-process team summary paths derive the same launch summary from the same mixed snapshot
- worker and in-process paths agree when bootstrap snapshot is newer than persisted launch-state and when persisted launch-state is structurally richer than bootstrap snapshot
- mixed-aware teams do not fall back to the legacy artifact-count partial-failure heuristic when canonical lane-aware snapshot truth is missing
- lane-local primary snapshot updates do not clear or terminalize the aggregate mixed snapshot prematurely
Phase 2 - Enable OpenCode single-member secondary lanes
Commit boundary: feat(team-runtime): enable opencode secondary member lanes
🎯 9 🛡️ 9 🧠 8
Roughly 650-1100 lines
Scope
- remove team-wide
OpenCodecapture logic - allow one
OpenCodemember to become a single-member secondary lane - reject mixed drafts with more than one
OpenCodeteammate with a clear unsupported-in-v1 message - exclude secondary-lane members from primary bootstrap spec and primary
expectedMembers - launch primary lane plus side lanes
- merge lane results into member-scoped launch-state
- keep aggregate mixed
launchPhaseactive until all planned lanes are terminal, even if the primary lane finishes earlier
Tests
Codexlead +OpenCodeteammateAnthropiclead +OpenCodeteammateGeminilead +OpenCodeteammate- multiple
OpenCodeteammates in one mixed team are rejected explicitly in V1 - primary lane does not wedge waiting for side-lane members
- one failing
OpenCodeside lane does not flatten failure onto other members - mixed launch keeps
config.jsonlead-only whilemembers.meta.jsonretains the full desired roster - every
OpenCodeside lane resolves its own env/auth/provider args instead of reusing the lead launch env - global mixed-team
launch-state.jsonand provider-local.opencode-runtime/launch-state.jsonremain separate and do not overwrite each other - primary-lane clean-success does not clear aggregate mixed launch-state while a side lane is still pending
Phase 3 - Lifecycle parity for mixed persistent teams
Commit boundary: feat(team-runtime): add mixed-lane lifecycle parity
🎯 8 🛡️ 9 🧠 7
Roughly 450-850 lines
Scope
- mixed relaunch
- mixed restore/startup recovery
- mixed stop
- mixed reconcile
- explicit blocking for unsupported live lane-migration edits
- explicit degraded-state handling when persisted mixed-lane truth is missing and only inbox/config fallback is available
- explicit blocking for live side-lane add-member in V1
- mixed-safe snapshot precedence when bootstrap and persisted launch-state disagree
- mixed-safe summary precedence in both Team summary reader paths
- explicit blocking for live roster mutation on running mixed teams
Tests
- create -> launch -> relaunch
- create -> launch -> stop
- startup recovery with stale secondary lane
- partial side-lane failure stays member-scoped
- live lane-migration edit is rejected with a clear message
- relaunch preserves primary-lane member
provider/model/effortand does not invent member-level backend/fast behavior - persisted mixed-lane recovery degrades explicitly when
members.meta.jsonor lane-awarelaunch-state.jsonis missing - live side-lane add-member attempt is rejected with a clear unsupported-in-v1 message
- richer persisted mixed launch-state wins over a newer bootstrap-only snapshot when both exist
- Team list / dashboard summary stays stable in both worker and in-process modes for the same mixed team
- live
addMember/replaceMembers/removeMemberon a running mixed team are rejected with clear stop-edit-relaunch guidance
Phase 4 - Renderer truth and diagnostics polish
Commit boundary: feat(team-runtime): surface mixed-lane member truth in ui
🎯 8 🛡️ 8 🧠 5
Roughly 180-320 lines
Scope
- member lane labels
- member-scoped failure copy
- mixed-aware pending/reconciling copy that does not pretend every pending state is "bootstrap pending"
- cleaner diagnostics and affordances on top of already-landed resolver parity
Tests
- Team Detail shows lead reliably
- member rows show provider/runtime truth
- side-lane failure does not flatten team-wide
- Team list/detail pending copy stays truthful when the primary lane is ready but a side lane is still attaching or reconciling
Future Phase - Live mixed-team roster mutation
Not part of the first mixed-team rollout.
Only start this after primary mixed launch/relaunch/recovery is stable.
Scope
- add or remove members on a running mixed team
- replace member roster on a running mixed team
- restart a secondary-lane member through a coordinator-owned path
- explicit migration workflow for primary <-> secondary lane changes
Why this is deferred
The current live add/remove/replace/restart seams are lead-Agent-tool-based or lead-message-based and do not carry enough runtime metadata for side-lane ownership. Shipping them in V1 would be materially riskier than fresh mixed launch support.
Future Phase - OpenCode lane-scoped runtime namespace and multi-OpenCode mixed teams
Not part of the first mixed-team rollout.
Only start this after the single-OpenCode-teammate V1 is stable.
Scope
- introduce lane-scoped provider-local runtime-store namespace, for example
.opencode-runtime/<laneId>/... - replace team-scoped single active-run assumptions in manifest/transaction/permission stores
- extend bridge/API contracts so
OpenCodeside-lane launches can represent lane-local model ownership truthfully - only after that allow more than one
OpenCodeteammate inside the same mixed team
Future Phase - Scheduled team runtime coordinator
Not part of this feature.
Only start this when the product adds a real scheduled team-runtime flow distinct from the current one-shot scheduler.
Test Plan
Main-process tests
- member desired-contract inheritance from lead defaults
- lane partition policy: primary-eligible vs secondary-required
- launch-state merge preserves per-member attribution
- side-lane run tracking does not break primary run tracking
Integration tests
- mixed create writes full canonical roster
- mixed launch persists member lane attribution
- mixed relaunch uses persisted member truth, not current global defaults
- side-lane stop/reconcile only affects side-lane members
- mixed launch/relaunch never treats lead-only
config.jsonas the canonical full roster source - degraded inbox/config fallback for a mixed team does not silently reconstruct
OpenCodeback onto the primary lane - team-view snapshots reflect lane-aware persisted truth instead of flattening back to config/meta-only labels
- Team list / dashboard summaries still read mixed launch-state within the supported size budget
- mixed drafts with more than one
OpenCodeteammate are rejected explicitly in V1 - global mixed-team
launch-state.jsonand provider-local.opencode-runtime/launch-state.jsonremain disambiguated - mixed Team list / dashboard summaries do not infer partial failure from missing side-lane artifacts while the team is still launching
Renderer tests
- lead row remains visible even when old data is sparse
- mixed team rows show member-level runtime truth
- partial failure UI remains member-scoped
Live Signoff
Required live signoff before rollout:
Codexlead +OpenCodeteammateAnthropiclead +OpenCodeteammateGeminilead +OpenCodeteammate- mixed team relaunch
- mixed team stop and restart
- startup recovery with an interrupted side lane
- Team Detail truth matches persisted lane/member runtime identity for a mixed team
- mixed draft with two
OpenCodeteammates is rejected with a clear unsupported-in-v1 message
Final Recommendation
Implement mixed teams as:
- one canonical primary team bootstrap lane
- additive single-member secondary lanes where runtime ownership truly requires it
- in V1, at most one
OpenCodesecondary teammate per mixed team - member-scoped persistence before behavior changes
- side-lane tracking added beside, not instead of, current primary run maps
- fresh mixed launch/relaunch/recovery first, with live side-lane attachment deferred
This is the most reliable and scalable path because it:
- matches the real code
- fixes the
vector-room-7class of bugs directly - keeps working bootstrap behavior intact
- gives us a clean extension point for future side-lane providers without over-engineering V1