Skip to content

feat(sdk): add scoped direct Session handles - #45988

Closed
kitlangton wants to merge 3 commits into
v2from
direct-sdk
Closed

kitlangton wants to merge 3 commits into
v2from
direct-sdk

Conversation

@kitlangton

@kitlangton kitlangton commented Aug 28, 2026 •

Copy link
Copy Markdown
Contributor

Why

Applications that keep one conversation per thread need to choose that conversation's live tools and plugins without building an embedded HTTP gateway. Location-based instance sharing also cannot give two conversations at the same directory independently configured capabilities.

The existing embedded SDK remains available. This adds a direct Effect entrypoint whose handles retain their instances through preparation, execution, child work, observation, and shutdown.

What Changes

@opencode-ai/sdk/direct/effect exports Session.create and Shared.layer. Provide shared infrastructure once and acquire private, ready Session handles inside that scope. Handles expose id, prompt, resume, interrupt, wait, and live events.subscribe.

Operation Behavior
Create two Sessions at the same directory Independent instance-local tools/config/plugins; one configured database, Bus/projector substrate, and coordinator
Create with an existing Session ID Adopt saved facts and history; build the new runtime at the saved location, ignoring retried durable facts
Construct a handle Wait for plugin and initial MCP-tool readiness; do not implicitly resume
Subscribe, then prompt/resume immediately Subscription is already acquired; callbacks consume live Session-ID-filtered events outside publication transactions
Prompt with resume: false Commit durable admission without scheduling model execution
Bind an already-live Session Fail with Session.AlreadyBoundError, never replace its live instance
Close the caller's scope or Shared layer Settle bound execution and child work before disposing capabilities; release private instances before shared backing; preserve shutdown recovery claims; retained methods fail with Session.ClosedError

Direct construction defaults to discovery: false; opt in to filesystem discovery explicitly. Runtime plugins/options are not serialized into Session metadata. Explicit relative database paths are resolved against the application's working directory.

flowchart LR
    Shared[Shared.layer] --> Backing[Shared Database and Bus/projectors]
    Shared --> Execution[One SessionExecution coordinator]
    A[Private Session A] --> Execution
    B[Private Session B at the same directory] --> Execution
    Execution -->|Session A binding| RunnerA[A's runner and capabilities]
    Execution -->|Session B binding| RunnerB[B's runner and capabilities]
    RunnerA --> Backing
    RunnerB --> Backing
Loading

Bound Execution

Core's SessionInstance boundary separates instance selection from the existing Session algorithms. The server adapter still uses its original LocationServiceMap; direct bindings retain already-built contexts and resolve children through their bound ancestor. Both adapters use the same durable prompt work and shared coordinator, not separate model loops.

Instance.compose requires explicit shared infrastructure and freshens only local services. Shared cleanup directly groups the existing Shell and ToolOutput cleanup nodes; no new graph-discard transform is needed. Captured Bus views isolate existing instance-local ephemeral inventory notifications through plugin context trimming while preserving shared credentials, placement/path events, and Session-event semantics. Durable publication, projector transactions, event schemas, and replay are unchanged.

Shutdown marks the entire bound execution chain before signaling any fibers. Bound runtime admission is closed before interruption, so background child completion cannot consume a recoverable notification ID with a teardown cancellation. Foreground cleanup cannot reclassify a shutdown child as user-cancelled.

Each private graph is a child of the Shared layer's acquisition scope and also closes with its caller's scope. This keeps private cleanup ahead of backing disposal even when the caller's scope outlives an Effect.provide(Shared.layer(...)) block.

Observation fibers attach to the caller with Fiber.runIn and detach automatically on completion. Their own Scope still owns the eager subscription and callback resources, avoiding accumulated caller finalizers when observations are repeatedly stopped.

Scope

This PR owns the map-free direct Effect slice and the Core binding/composition needed to support it. The server keeps its existing location cache, and the shipped embedded/Workerd SDK entrypoints remain unchanged. Movable placement, custom server assignment, replay-cursor observation, exactly-once outbound delivery, and a direct Promise facade are follow-ups.

Direct resume() drains the selected Session; it does not run the server's automatic restart/background-Job recovery sweep. Automatic background Job restoration after reopening handles remains a follow-up, with the durable recovery markers preserved by shutdown.

Verification

# packages/core
bun typecheck
bun run test
bun run test test/instance-compose.test.ts test/instance-compose-types.test.ts test/instance-plugins.test.ts test/instance-vanilla.test.ts test/bus-capture.test.ts test/bus-observe.test.ts test/instance-bus-capture.test.ts test/session-run-coordinator.test.ts test/session-execution.test.ts test/effect/layer-node test/shell.test.ts test/tool-output.test.ts

# packages/server
bun typecheck
bun run ../core/script/test.ts test/session-message-update.test.ts test/session-import.test.ts test/fetch.test.ts test/generate.test.ts test/handler-policy.test.ts

# packages/sdk; tests use isolated HOME/XDG/OPENCODE_CONFIG_DIR paths
bun typecheck
OPENCODE_DISABLE_MODELS_FETCH=true bun run ../core/script/test.ts
OPENCODE_DISABLE_MODELS_FETCH=true bun run verify:package

# packages/util
bun typecheck

git diff --check
  • Full Core before the simplification pass: 3817 passed, 39 skipped, 0 failed across 228 files on the refreshed v2 baseline. After simplification, 111 focused tests passed, 0 failed across 14 files, including composition, cleanup, events, execution, and existing graph tests.
  • Full SDK after simplification with isolated home/config: 44 passed, 0 failed, including 17 direct production-stack integration tests using scripted LLM responses with real preparation, tools, admission, projections, runner, and coordination. The new observer-finalizer regression failed before the fix and passed afterward.
  • Focused HTTP regressions: 18 passed, 0 failed.
  • All 33 pre-push typecheck tasks passed without bypassing hooks.
  • New Bus/instance audience tests: 11 tests passed in five repeated runs, including real plugin contexts, explicit global credential delivery, eager observation, and same-aggregate callback publication.
  • Type regressions cover missing shared infrastructure/callback dependencies, open or fallible ready-constructor replacements, incorrect replacement outputs, and the low-level composition's retained requirements/errors.
  • Import-boundary bundles verify the direct entrypoint includes Core but no Client or Server runtime modules.
  • Packed consumer validation passed at 1383222327, before the simplification pass. The new direct entrypoint constructs two co-located handles, commits real admission, and receives its already-acquired Session observation from packed packages. Existing imports, the Workerd dry-run bundle and boot/health check passed. SDK packed contents and emitted direct declarations were inspected.

The initial SDK run against ambient home configuration timed out in seven existing embedded fixtures after loading configured MCP servers. The full isolated rerun passed without changing those fixtures. Formatting passes for the new source/tests and changed runtime surfaces; the existing session-prompt.test.ts has a baseline formatting warning, with only a no-op shutdown fixture field added here.

During simplification verification, a parallel SDK run timed out in the existing five-second contract-generation test. The full SDK suite passed when rerun alone, without changing its timeout or fixture.

@Noisemaker111

Copy link
Copy Markdown
Contributor

This is great

@github-actions

Copy link
Copy Markdown
Contributor

Automated PR Cleanup

Thank you for contributing to opencode.

Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions.

This PR was closed because it matched the following cleanup criteria:

  • The PR was created more than 1 month ago
  • The PR had fewer than 2 positive reactions
  • Positive reactions are counted as thumbs-up, heart, celebration, or rocket reactions on the PR

PRs created within the last month are not affected by this cleanup.

If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate.

Thanks again for taking the time to contribute.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants