Skip to content

Session execution: V1 and V2 stacks can execute the same session concurrently (no cross-stack guard) #37615

Description

@Alter-Igor

Summary

The legacy V1 session stack and the V2 Session Core share one per-session durable event log (aggregate = sessionID), but their process-local execution registries are mutually unaware. Nothing prevents V1 and V2 from executing the same session concurrently in one process, semantically interleaving durable events on that session's aggregate sequence.

Evidence

  • V1 execution is serialized by SessionRunState (packages/opencode/src/session/run-state.ts) — a per-instance runners map with BusyError. It has zero references to SessionV2 / SessionExecution / SessionRunCoordinator.
  • V2 execution is serialized by SessionRunCoordinator (packages/core/src/session/run-coordinator.ts) — a per-key fiber serializer. It has zero references to SessionPrompt / SessionRunState / V1.
  • Both stacks publish durable events into the same per-session aggregate via the single commit path in packages/core/src/event.ts (commitDurableEvent), so an immediate transaction protects seq integrity but not semantic interleaving (duplicate promotions, double tool settlement, mixed context-epoch baselines).
  • The event_sequence.owner_id claim checks (event.ts) apply only to replay, not live publish — they don't guard this.
  • Related: V1 prompts never produce session_input rows (no V2 inbox entries for V1-driven sessions), and specs/v2/session.md (~line 35) still describes a "V1-to-V2 shadow bridge" republishing V1 prompts as V2 Prompted events that does not exist in code.

Why it's latent today (and why that changes soon)

No first-party client drives V2 yet (TUI / app / slack / acp / run CLI all use the V1 /session/* handlers; V2 is consumed by sdk-next). But V2 is expanding fast (compact in #30986, resume-after-restart in #36105, embedded live-tail fix in #34017), so the first realistic concurrent-stack scenario — e.g. a TUI session (V1) while an embedded host wakes the same session (V2) — is getting closer.

Proposal sketch

A shared process-local per-session execution arbiter in core, consulted by both SessionRunState.ensureRunning and SessionRunCoordinator.run/wake/resume, failing with BusyError on cross-stack conflict. Deliberately process-local and non-durable, consistent with the "Session drains are process-local" design rule — this closes the same-process hole without prejudging clustered placement.

Question for maintainers

Is this the right shape, or is it subsumed by the remote workspace lifecycle seam in #37437? Happy to PR the process-local subset with tests if you agree it's worth closing ahead of (or as part of) that work.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions