@@ -40,11 +40,7 @@ import {
4040
4141const PROVIDER = "opencode" as const ;
4242const OPEN_CODE_STALL_WARNING_MS = 15_000 ;
43-
44- interface OpenCodeTurnSnapshot {
45- readonly id : TurnId ;
46- readonly items : Array < unknown > ;
47- }
43+ const EXTERNAL_OPENCODE_ABORT_GRACE_MS = 2_000 ;
4844
4945interface OpenCodeSessionContext {
5046 session : ProviderSession ;
@@ -58,7 +54,6 @@ interface OpenCodeSessionContext {
5854 readonly partById : Map < string , Part > ;
5955 readonly emittedTextByPartId : Map < string , string > ;
6056 readonly completedAssistantPartIds : Set < string > ;
61- readonly turns : Array < OpenCodeTurnSnapshot > ;
6257 activeTurnId : TurnId | undefined ;
6358 activeAgent : string | undefined ;
6459 activeVariant : string | undefined ;
@@ -177,29 +172,22 @@ function mapPermissionDecision(reply: "once" | "always" | "reject"): string {
177172 }
178173}
179174
180- function resolveTurnSnapshot (
181- context : OpenCodeSessionContext ,
182- turnId : TurnId ,
183- ) : OpenCodeTurnSnapshot {
184- const existing = context . turns . find ( ( turn ) => turn . id === turnId ) ;
185- if ( existing ) {
186- return existing ;
187- }
188-
189- const created : OpenCodeTurnSnapshot = { id : turnId , items : [ ] } ;
190- context . turns . push ( created ) ;
191- return created ;
192- }
193-
194- function appendTurnItem (
195- context : OpenCodeSessionContext ,
196- turnId : TurnId | undefined ,
197- item : unknown ,
198- ) : void {
199- if ( ! turnId ) {
200- return ;
201- }
202- resolveTurnSnapshot ( context , turnId ) . items . push ( item ) ;
175+ /**
176+ * Drop per-turn streaming bookkeeping after a turn ends. Long sessions otherwise
177+ * accumulate full assistant/reasoning text and tool-call state in `partById` /
178+ * `emittedTextByPartId` for every turn ever processed, which can grow into the
179+ * multi-GB range for sustained use.
180+ *
181+ * We intentionally drop *all* parts: events that arrive after the turn ends for
182+ * a previously-seen part are no-ops in the existing handlers (they guard on
183+ * `partById.get(...)` returning `undefined`), and assistant text for a closed
184+ * turn has already been emitted as `item.completed`.
185+ */
186+ function pruneCompletedTurnState ( context : OpenCodeSessionContext ) : void {
187+ context . partById . clear ( ) ;
188+ context . emittedTextByPartId . clear ( ) ;
189+ context . completedAssistantPartIds . clear ( ) ;
190+ context . messageRoleById . clear ( ) ;
203191}
204192
205193function ensureSessionContext (
@@ -509,12 +497,20 @@ async function stopOpenCodeContext(context: OpenCodeSessionContext): Promise<voi
509497 context . stallWarningTimer = undefined ;
510498 }
511499 context . eventsAbortController . abort ( ) ;
512- try {
513- await context . client . session
514- . abort ( { sessionID : context . openCodeSessionId } )
515- . catch ( ( ) => undefined ) ;
516- } catch { }
500+ if ( context . server . external ) {
501+ // External servers outlive us: try to abort the remote session politely,
502+ // but do not block teardown if the RPC is wedged.
503+ await Promise . race ( [
504+ context . client . session . abort ( { sessionID : context . openCodeSessionId } ) . catch ( ( ) => undefined ) ,
505+ new Promise < void > ( ( resolve ) => setTimeout ( resolve , EXTERNAL_OPENCODE_ABORT_GRACE_MS ) ) ,
506+ ] ) ;
507+ context . server . close ( ) ;
508+ return ;
509+ }
510+ // Local server: skip the abort RPC entirely — we own the process and are
511+ // about to terminate it, so the HTTP call would just race a dropped socket.
517512 context . server . close ( ) ;
513+ pruneCompletedTurnState ( context ) ;
518514}
519515
520516export function makeOpenCodeAdapterLive ( _options ?: OpenCodeAdapterLiveOptions ) {
@@ -531,6 +527,8 @@ export function makeOpenCodeAdapterLive(_options?: OpenCodeAdapterLiveOptions) {
531527 stream : "native" ,
532528 } )
533529 : undefined ) ;
530+ const managedNativeEventLogger =
531+ _options ?. nativeEventLogger === undefined ? nativeEventLogger : undefined ;
534532 const runtimeEvents = yield * Queue . unbounded < ProviderRuntimeEvent > ( ) ;
535533 const sessions = new Map < ThreadId , OpenCodeSessionContext > ( ) ;
536534
@@ -593,7 +591,9 @@ export function makeOpenCodeAdapterLive(_options?: OpenCodeAdapterLiveOptions) {
593591 context . stopped = true ;
594592 clearTurnStallWarning ( context ) ;
595593 sessions . delete ( context . session . threadId ) ;
594+ context . eventsAbortController . abort ( ) ;
596595 context . server . close ( ) ;
596+ pruneCompletedTurnState ( context ) ;
597597 void logWarning ( "opencode.session.unexpected-exit" , {
598598 message,
599599 ...turnObservabilitySnapshot ( context ) ,
@@ -861,7 +861,6 @@ export function makeOpenCodeAdapterLive(_options?: OpenCodeAdapterLiveOptions) {
861861 : "item.updated" ,
862862 payload,
863863 } ;
864- appendTurnItem ( context , turnId , part ) ;
865864 await emitPromise ( runtimeEvent ) ;
866865 }
867866 break ;
@@ -1022,6 +1021,7 @@ export function makeOpenCodeAdapterLive(_options?: OpenCodeAdapterLiveOptions) {
10221021 clearTurnStallWarning ( context ) ;
10231022 context . activeTurnId = undefined ;
10241023 context . activeTurnStartedAt = undefined ;
1024+ pruneCompletedTurnState ( context ) ;
10251025 updateProviderSession (
10261026 context ,
10271027 { status : "ready" } ,
@@ -1044,6 +1044,7 @@ export function makeOpenCodeAdapterLive(_options?: OpenCodeAdapterLiveOptions) {
10441044 clearTurnStallWarning ( context ) ;
10451045 context . activeTurnId = undefined ;
10461046 context . activeTurnStartedAt = undefined ;
1047+ pruneCompletedTurnState ( context ) ;
10471048 updateProviderSession (
10481049 context ,
10491050 {
@@ -1238,7 +1239,6 @@ export function makeOpenCodeAdapterLive(_options?: OpenCodeAdapterLiveOptions) {
12381239 emittedTextByPartId : new Map ( ) ,
12391240 messageRoleById : new Map ( ) ,
12401241 completedAssistantPartIds : new Set ( ) ,
1241- turns : [ ] ,
12421242 activeTurnId : undefined ,
12431243 activeAgent : undefined ,
12441244 activeVariant : undefined ,
@@ -1397,6 +1397,7 @@ export function makeOpenCodeAdapterLive(_options?: OpenCodeAdapterLiveOptions) {
13971397 context . activeAgent = undefined ;
13981398 context . activeVariant = undefined ;
13991399 context . activeTurnStartedAt = undefined ;
1400+ pruneCompletedTurnState ( context ) ;
14001401 updateProviderSession (
14011402 context ,
14021403 {
@@ -1671,6 +1672,18 @@ export function makeOpenCodeAdapterLive(_options?: OpenCodeAdapterLiveOptions) {
16711672 } ) ,
16721673 } ) ;
16731674
1675+ yield * Effect . addFinalizer ( ( ) =>
1676+ stopAll ( ) . pipe (
1677+ Effect . catchCause ( ( cause ) =>
1678+ Effect . logWarning ( "opencode.session.stop-all-finalizer-failed" , {
1679+ cause : Cause . pretty ( cause ) ,
1680+ } ) ,
1681+ ) ,
1682+ Effect . tap ( ( ) => Queue . shutdown ( runtimeEvents ) ) ,
1683+ Effect . tap ( ( ) => managedNativeEventLogger ?. close ( ) ?? Effect . void ) ,
1684+ ) ,
1685+ ) ;
1686+
16741687 return {
16751688 provider : PROVIDER ,
16761689 capabilities : {
0 commit comments