Skip to content

Import external OpenCode sessions for T3 projects#14

Merged
dbalders merged 2 commits into
tritongptfrom
opencode-external-thread-sync
Jun 20, 2026
Merged

Import external OpenCode sessions for T3 projects#14
dbalders merged 2 commits into
tritongptfrom
opencode-external-thread-sync

Conversation

@dbalders

@dbalders dbalders commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Summary

  • import external OpenCode sessions from the T3-managed OpenCode runtime/database for projects already added to T3
  • keep sessions from other OpenCode stores and sessions from unadded project roots out of the T3 UI
  • preserve child-directory cwd, refresh stopped imported metadata, skip active imported sessions, and avoid aborting a shared resumed OpenCode session during concurrent starts

Verification

  • launched the dev app with T3CODE_DEV_INSTANCE=opencode-external-thread-sync npm run dev:desktop
  • created a T3-env OpenCode run in /Users/davidbalderston/Github/UCSD-Skills-Library; it appeared under the existing T3 project in the UI
  • created a default-store OpenCode run in that repo; it stayed out of T3
  • created a T3-env OpenCode run in this unadded worktree; it stayed out of T3
  • ./node_modules/.bin/vp test run apps/server/src/provider/Layers/OpenCodeAdapter.test.ts apps/server/src/provider/Layers/OpenCodeExternalSessionSync.test.ts apps/server/src/provider/Drivers/OpenCodeDriver.test.ts
  • ./node_modules/.bin/vp run --filter t3 typecheck
  • ./node_modules/.bin/vp check (passes with the existing 13 lint warnings)
  • /Users/davidbalderston/.agents/skills/autoreview/scripts/autoreview --mode local --parallel-tests './node_modules/.bin/vp test run apps/server/src/provider/Layers/OpenCodeAdapter.test.ts apps/server/src/provider/Layers/OpenCodeExternalSessionSync.test.ts apps/server/src/provider/Drivers/OpenCodeDriver.test.ts && ./node_modules/.bin/vp run --filter t3 typecheck && ./node_modules/.bin/vp check' -> clean

Summary by CodeRabbit

  • New Features
    • Added installer-managed OpenCode runtime discovery under the home directory, with automatic PATH and XDG environment defaults and installer-provided config when not explicitly set.
    • Enabled OpenCode session resuming and external session listing.
    • Introduced background synchronization of external OpenCode sessions into local projects, including idempotent imports and metadata refresh/skip logic.
  • Bug Fixes
    • Improved concurrent session start behavior to avoid aborting a session that was resumed by another caller.
  • Tests
    • Migrated test suites to vite-plus/test.

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a047fae-1d19-4fe6-b7f9-36c928577b77

📥 Commits

Reviewing files that changed from the base of the PR and between a189d52 and 1ce6444.

📒 Files selected for processing (4)
  • apps/server/src/provider/Drivers/OpenCodeDriver.ts
  • apps/server/src/provider/Layers/OpenCodeAdapter.ts
  • apps/server/src/provider/Layers/OpenCodeExternalSessionSync.test.ts
  • apps/server/src/provider/Services/OpenCodeAdapter.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/server/src/provider/Services/OpenCodeAdapter.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/server/src/provider/Layers/OpenCodeExternalSessionSync.test.ts
  • apps/server/src/provider/Drivers/OpenCodeDriver.ts
  • apps/server/src/provider/Layers/OpenCodeAdapter.ts

📝 Walkthrough

Walkthrough

This PR adds installer-managed OpenCode runtime discovery to OpenCodeDriver, session-resume support and external-session listing to OpenCodeAdapter, and a new OpenCodeExternalSessionSync service that periodically imports and refreshes external OpenCode sessions into internal threads. The sync service is wired into the server layer composition and startup lifecycle. Several test files are also migrated from vitest to vite-plus/test.

Changes

OpenCode Provider Integration

Layer / File(s) Summary
Adapter contracts and service definition
apps/server/src/provider/Services/OpenCodeAdapter.ts, apps/server/src/provider/Services/OpenCodeExternalSessionSync.ts
Defines OpenCodeExternalSessionModel, OpenCodeExternalSessionSummary, and adds listExternalSessions to OpenCodeAdapterShape. Creates the OpenCodeExternalSessionSync Context.Service with syncOnce/start shape and result-count interface.
Installer-managed OpenCode runtime discovery
apps/server/src/provider/Drivers/OpenCodeDriver.ts, apps/server/src/provider/Drivers/OpenCodeDriver.test.ts
Adds InstallerManagedOpenCodeRuntime interface, constants, and exported helpers (isDefaultOpenCodeBinaryPath, selectInstallerManagedOpenCodeVersionDirectory, resolveInstallerManagedOpenCodeBinaryPath, mergeInstallerManagedOpenCodeEnvironment) plus an Effect-based discovery scanner. create() now discovers the installer runtime to set processEnv (with PATH/XDG_* merging) and resolves effectiveConfig.binaryPath. Tests validate all four helpers.
Session resume and listExternalSessions in OpenCodeAdapter
apps/server/src/provider/Layers/OpenCodeAdapter.ts, apps/server/src/provider/Layers/OpenCodeAdapter.test.ts, apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts
Adds readOpenCodeResumeSessionId, toOpenCodeResumeCursor, and toExternalSessionSummary. startSession reads input.resumeCursor to call session.get vs session.create, tracks createdOpenCodeSession, conditionally aborts only fresh sessions in the race-loser path, and returns resumeCursor in ProviderSession. Adds listExternalSessions (deduplicates directories, connects with reuseLocalServer:true, lists/converts/deduplicates sessions). Concurrency test and registry mock updated.
OpenCodeExternalSessionSync implementation
apps/server/src/provider/Layers/OpenCodeExternalSessionSync.ts, apps/server/src/provider/Layers/OpenCodeExternalSessionSync.test.ts
New sync layer: path normalization, matchOpenCodeSessionToProject, isT3OwnedOpenCodeSession, externalThreadIdForOpenCodeSession. Core importExternalSession dispatches thread.create or thread.meta.update, upserts ProviderSessionDirectory, and dispatches thread.session.set. runSyncOnce iterates projects and enabled instances with per-instance failure handling. start() forks a scheduled repeating worker. Full test suite covering matching, import, idempotency, metadata refresh, and refresh-skipping.
Server layer and startup wiring
apps/server/src/server.ts, apps/server/src/serverRuntimeStartup.ts
Adds OpenCodeExternalSessionSyncLive to ProviderRuntimeLayerLive via Layer.mergeAll. Retrieves OpenCodeExternalSessionSync in makeServerRuntimeStartup and starts it in the orchestration-reactors startup phase.

Test Framework Migration

Layer / File(s) Summary
vitest → vite-plus/test
apps/server/src/provider/builtInDrivers.test.ts, apps/server/src/provider/opencodeRuntime.test.ts, apps/web/src/firstRunOnboarding.test.ts, apps/web/src/providerModels.test.ts, scripts/tritongpt-sync-upstream.test.ts
Replaces vitest import source with vite-plus/test across five test files. No test logic changes.

Sequence Diagrams

sequenceDiagram
  participant Server as Server Startup
  participant SyncService as OpenCodeExternalSessionSync
  participant Adapter as OpenCodeAdapter
  participant SDK as OpenCode SDK
  participant Engine as OrchestrationEngineService

  Server->>SyncService: start() — forked scheduled worker
  loop every syncIntervalMs
    SyncService->>Engine: list active projects
    Engine-->>SyncService: projects[]
    SyncService->>Adapter: listExternalSessions(directories)
    Adapter->>SDK: session.list(dir) per directory (reuseLocalServer: true)
    SDK-->>Adapter: Session[]
    Adapter-->>SyncService: OpenCodeExternalSessionSummary[]
    loop per session
      SyncService->>SyncService: matchOpenCodeSessionToProject
      SyncService->>Engine: thread.create or thread.meta.update
      SyncService->>Engine: thread.session.set (stopped, resume cursor)
    end
    SyncService-->>Server: SyncResult (imported/refreshed/skipped counts)
  end
Loading
sequenceDiagram
  participant Caller as startSession caller
  participant Adapter as OpenCodeAdapter
  participant SDK as OpenCode SDK

  Caller->>Adapter: startSession(resumeCursor?)
  alt resumeCursor present
    Adapter->>SDK: session.get(sessionId)
    SDK-->>Adapter: Session (createdOpenCodeSession=false)
  else no cursor
    Adapter->>SDK: session.create(...)
    SDK-->>Adapter: Session (createdOpenCodeSession=true)
  end
  Adapter-->>Caller: ProviderSession with resumeCursor

  Note over Adapter: Concurrent race-loser path
  alt loser created fresh session
    Adapter->>SDK: session.abort(sessionId)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 Hop, hop! The runtime is found,
In ~/.agents the installer's ground.
Sessions resume with a cursor in paw,
External threads synced without a flaw.
From vitest to vite-plus we bound—
squeak! The bunnies have gained new ground! 🌟

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description includes a clear summary of changes, detailed verification steps, and testing confirmation. However, it lacks the structured template sections (What Changed, Why, UI Changes, Checklist) specified in the repository template. Restructure the description to follow the repository template with explicit sections for 'What Changed', 'Why', and 'Checklist' to ensure consistency with repository standards.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: importing external OpenCode sessions for T3 projects. It is concise, specific, and directly reflects the core functionality added in this changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch opencode-external-thread-sync

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL labels Jun 20, 2026
@dbalders

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
apps/server/src/provider/Layers/OpenCodeExternalSessionSync.test.ts (1)

81-85: ⚡ Quick win

Assert limit propagation in the discovery test harness.

The mock currently records only directories, so a regression that drops limit from listExternalSessions calls would pass unnoticed.

Suggested test-hardening diff
-function makeFakeOpenCodeAdapter(input: {
-  readonly sessions: ReadonlyArray<OpenCodeExternalSessionSummary>;
-  readonly calls: Array<ReadonlyArray<string>>;
-}): OpenCodeAdapterShape {
+function makeFakeOpenCodeAdapter(input: {
+  readonly sessions: ReadonlyArray<OpenCodeExternalSessionSummary>;
+  readonly calls: Array<{ readonly directories: ReadonlyArray<string>; readonly limit?: number }>;
+}): OpenCodeAdapterShape {
   return {
@@
-    listExternalSessions: ({ directories }) =>
+    listExternalSessions: ({ directories, limit }) =>
       Effect.sync(() => {
-        input.calls.push([...directories]);
+        input.calls.push({ directories: [...directories], limit });
         return input.sessions;
       }),
@@
-function makeHarnessLayer(sessions: ReadonlyArray<OpenCodeExternalSessionSummary>) {
-  const calls: Array<ReadonlyArray<string>> = [];
+function makeHarnessLayer(sessions: ReadonlyArray<OpenCodeExternalSessionSummary>) {
+  const calls: Array<{ readonly directories: ReadonlyArray<string>; readonly limit?: number }> = [];
@@
-        expect(harness.calls).toEqual([[KNOWN_PROJECT_ROOT]]);
+        expect(harness.calls).toEqual([
+          { directories: [KNOWN_PROJECT_ROOT], limit: 50 },
+        ]);

Also applies to: 110-112, 268-268

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/provider/Layers/OpenCodeExternalSessionSync.test.ts` around
lines 81 - 85, The mock function for listExternalSessions currently only records
the directories parameter in input.calls but omits the limit parameter. Update
the listExternalSessions mock function at all three locations (lines around
81-85, 110-112, and 268) to also capture and record the limit parameter
alongside directories in the input.calls array to ensure that any regression
dropping the limit parameter would be detected by the test harness.
apps/server/src/provider/Drivers/OpenCodeDriver.ts (1)

213-221: 💤 Low value

Inconsistent sort order between findInstallerManagedOpenCodeRuntime and selectInstallerManagedOpenCodeVersionDirectory.

The sorting logic here sorts in descending order (right.version, left.version), while selectInstallerManagedOpenCodeVersionDirectory (lines 126-128) sorts in ascending order and picks the last element. Both achieve "select newest" but via different approaches.

This inconsistency could cause confusion for future maintainers. Consider standardizing on one approach.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/provider/Drivers/OpenCodeDriver.ts` around lines 213 - 221,
The sorting logic in the current method sorts in descending order to get the
newest version, while selectInstallerManagedOpenCodeVersionDirectory sorts in
ascending order and picks the last element. Both achieve the same result but
with inconsistent approaches. Standardize by making both methods use the same
sorting strategy and element selection. Choose either to sort descending and
pick the first element (like the current implementation) or sort ascending and
pick the last element (like selectInstallerManagedOpenCodeVersionDirectory).
Update whichever function is easier to modify to match the other's approach,
ensuring consistent logic across the OpenCodeDriver class.
apps/server/src/provider/Layers/OpenCodeAdapter.ts (2)

1128-1147: ⚡ Quick win

Missing distinct error handling for resumed sessions that no longer exist.

When resuming a session via session.get, if the session was deleted or doesn't exist, the SDK may return { data: undefined } or throw. The current error message "OpenCode session.get returned no session payload" is generic.

Consider adding a more specific check or error message to help users understand that the resumed session may have been deleted, allowing them to retry without a resume cursor.

💡 Suggested improvement
               if (!openCodeSession.data) {
                 return yield* new OpenCodeRuntimeError({
                   operation: resumeSessionId !== undefined ? "session.get" : "session.create",
                   detail:
                     resumeSessionId !== undefined
-                      ? "OpenCode session.get returned no session payload."
+                      ? `OpenCode session '${resumeSessionId}' not found or was deleted.`
                       : "OpenCode session.create returned no session payload.",
                 });
               }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/provider/Layers/OpenCodeAdapter.ts` around lines 1128 - 1147,
The error handling after the openCodeSession retrieval does not distinguish
between a failed session.get for a resumed session versus a failed
session.create. When resumeSessionId is defined and openCodeSession.data is
falsy, add a more specific error message that indicates the resumed session may
have been deleted or no longer exists, suggesting the user retry without the
resume cursor. Consider adding a separate conditional check for the
resumeSessionId !== undefined case before the generic error return in the
openCodeSession.data validation block to provide clearer feedback to users
attempting to resume sessions that no longer exist.

1450-1497: 💤 Low value

The session limit is applied per-directory, potentially returning more results than expected.

The sessionLimit is passed to each client.session.list call per directory. If there are N directories, the total results could be up to N * sessionLimit before deduplication. This is likely intentional since sessions are deduplicated by sessionId, but the behavior differs from what a caller might expect if they pass limit: 10 expecting at most 10 results.

Consider documenting this behavior in the interface or applying a final cap after deduplication if strict limiting is desired.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/provider/Layers/OpenCodeAdapter.ts` around lines 1450 - 1497,
The sessionLimit in the listExternalSessions function is applied per-directory
when calling client.session.list inside the for loop, which means with N
directories you could return up to N*sessionLimit results before deduplication.
Either document this per-directory behavior in the interface definition for
listExternalSessions to set caller expectations, or apply a final cap by
limiting the results returned from the deduplication map (e.g., slice or limit
the array returned from [...summaries.values()]) to ensure the total returned
results never exceed sessionLimit.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/server/src/provider/Layers/OpenCodeAdapter.test.ts`:
- Line 362: The test assertion for closeCalls in the concurrent startSession
test expects a single close call, but because each concurrent startSession
creates its own sessionScope with a finalizer registered via
connectToOpenCodeServer, when the race loser's scope is cleaned up at line 1177,
it triggers an additional close call. Update the assertion at line 362 to expect
two close calls ["http://127.0.0.1:9999", "http://127.0.0.1:9999"] to match the
actual behavior from both sessionScope finalizers executing, or alternatively
suppress the loser's scope cleanup if the intended behavior is to only have one
close call.

---

Nitpick comments:
In `@apps/server/src/provider/Drivers/OpenCodeDriver.ts`:
- Around line 213-221: The sorting logic in the current method sorts in
descending order to get the newest version, while
selectInstallerManagedOpenCodeVersionDirectory sorts in ascending order and
picks the last element. Both achieve the same result but with inconsistent
approaches. Standardize by making both methods use the same sorting strategy and
element selection. Choose either to sort descending and pick the first element
(like the current implementation) or sort ascending and pick the last element
(like selectInstallerManagedOpenCodeVersionDirectory). Update whichever function
is easier to modify to match the other's approach, ensuring consistent logic
across the OpenCodeDriver class.

In `@apps/server/src/provider/Layers/OpenCodeAdapter.ts`:
- Around line 1128-1147: The error handling after the openCodeSession retrieval
does not distinguish between a failed session.get for a resumed session versus a
failed session.create. When resumeSessionId is defined and openCodeSession.data
is falsy, add a more specific error message that indicates the resumed session
may have been deleted or no longer exists, suggesting the user retry without the
resume cursor. Consider adding a separate conditional check for the
resumeSessionId !== undefined case before the generic error return in the
openCodeSession.data validation block to provide clearer feedback to users
attempting to resume sessions that no longer exist.
- Around line 1450-1497: The sessionLimit in the listExternalSessions function
is applied per-directory when calling client.session.list inside the for loop,
which means with N directories you could return up to N*sessionLimit results
before deduplication. Either document this per-directory behavior in the
interface definition for listExternalSessions to set caller expectations, or
apply a final cap by limiting the results returned from the deduplication map
(e.g., slice or limit the array returned from [...summaries.values()]) to ensure
the total returned results never exceed sessionLimit.

In `@apps/server/src/provider/Layers/OpenCodeExternalSessionSync.test.ts`:
- Around line 81-85: The mock function for listExternalSessions currently only
records the directories parameter in input.calls but omits the limit parameter.
Update the listExternalSessions mock function at all three locations (lines
around 81-85, 110-112, and 268) to also capture and record the limit parameter
alongside directories in the input.calls array to ensure that any regression
dropping the limit parameter would be detected by the test harness.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5efd253b-7ea1-43be-aa96-9f555929adec

📥 Commits

Reviewing files that changed from the base of the PR and between ca1d3cc and a189d52.

📒 Files selected for processing (16)
  • apps/server/src/provider/Drivers/OpenCodeDriver.test.ts
  • apps/server/src/provider/Drivers/OpenCodeDriver.ts
  • apps/server/src/provider/Layers/OpenCodeAdapter.test.ts
  • apps/server/src/provider/Layers/OpenCodeAdapter.ts
  • apps/server/src/provider/Layers/OpenCodeExternalSessionSync.test.ts
  • apps/server/src/provider/Layers/OpenCodeExternalSessionSync.ts
  • apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts
  • apps/server/src/provider/Services/OpenCodeAdapter.ts
  • apps/server/src/provider/Services/OpenCodeExternalSessionSync.ts
  • apps/server/src/provider/builtInDrivers.test.ts
  • apps/server/src/provider/opencodeRuntime.test.ts
  • apps/server/src/server.ts
  • apps/server/src/serverRuntimeStartup.ts
  • apps/web/src/firstRunOnboarding.test.ts
  • apps/web/src/providerModels.test.ts
  • scripts/tritongpt-sync-upstream.test.ts

Comment thread apps/server/src/provider/Layers/OpenCodeAdapter.test.ts
@dbalders

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@dbalders
dbalders marked this pull request as ready for review June 20, 2026 06:36
@dbalders
dbalders merged commit c8cc140 into tritongpt Jun 20, 2026
8 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant