Dashboard › opencode › Session 0zQHdddpHIud
0zQHdddpHIudW7XKGRead-only independent security/pentest review of frozen v13. Base ab9408c81cc9916177641ae8e9f08624fc27b5b9. Patch /tmp/opencode/session-recovery-final-v13.patch SHA-256 3e9b20cb5ec05d1065b6bc78bab00b47e8ca9296bc819689a4f3d889eddcb5bf. SEA /home/byk/Code/opencode/packages/opencode/dist/server/opencode-server SHA-256 c3b8c5d78e473ad4104a5b54617900e28921a283029b8484be7bce4bc3cee85a. Do not mutate/run tests/builds/services. Verify hashes, clean apply, inspect all 33 paths. Threat model stale recovery owners and concurrent/replayed assistant creation. Primary closures: quarantine writes for cancellation and projection failure cannot mutate after lease expiry; successor can take over. Step.Recovered transaction rechecks assistant-less ready/unknown/continue claims against captured assistant_seq so concurrent Step.Started rolls back stale recovery; direct producer tests cover acquisition and Step.Started. Verify no fail-open catch path, no stale claim clear, equality compaction works. Reassess complete recovery identity pending/settled, cross-Session, retry/queue/steer reservation, stale owners, cancellation finalizers, external side effects/tools, fairness/DoS, replay, schema/auth/dependency boundaries. Accepted residuals: final synchronous check-to-external-call, process-local interrupt routing, bounded clock skew. Evidence recovery 241 pass, Core 1178 pass, typecheck/migration/smoke. Return substantive exact file:line PASS/CONCERN/MUST-FIX, end exactly MERGE or DO-NOT-MERGE. Empty invalid; no recall.
3e9b20cb5ec05d1065b6bc78bab00b47e8ca9296bc819689a4f3d889eddcb5bf /tmp/opencode/session-recovery-final-v13.patch c3b8c5d78e473ad4104a5b54617900e28921a283029b8484be7bce4bc3cee85a /home/byk/Code/opencode/packages/opencode/dist/server/opencode-server ab9408c81cc9916177641ae8e9f08624fc27b5b9 M .lore.md M AGENTS.md M packages/core/schema.json M packages/core/src/database/migration.gen.ts M packages/core/src/database/migration/20260914170650_session-recovery.ts D packages/core/src/database/migration/20260914214636_session-execution.ts D packages/core/src/database/migration/20260914222526_session-execution-phase.ts M packages/core/src/database/schema.gen.ts M packages/core/src/session.ts M packages/core/src/session/compaction.ts M packages/core/src/session/context-epoch.ts M packages/core/src/session/execution/local.ts M packages/core/src/session/input.ts M packages/core/src/session/message-updater.ts M packages/core/src/session/projector.ts M ...
Checking patch packages/core/schema.json...
error: while searching for:
{
"version": "7",
"dialect": "sqlite",
"id": "f14a9b18-8207-487e-a3d3-227e629ba9ad",
"prevIds": ["169a0f0f-d58f-479f-b024-fa1c7b9a09db"],
"ddl": [
{
"name": "workspace",
error: patch failed: packages/core/schema.json:1
error: packages/core/schema.json: patch does not apply
Checking patch packages/core/src/database/migration.gen.ts...
error: while searching for:
import("./migration/20260622142730_simplify_session_context_epoch"),
import("./migration/20260622170816_reset_v2_session_state"),
import("./migration/20260622202450_simplify_session_input"),
])
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
error: patch failed: packages/core/src/database/migration.gen.ts:40
error: packages/core/src/database/migration.gen.ts: patch does not apply
Checking patch packages/core/src/database/migration/20260914170650_session-recovery.ts... ...
drwxr-xr-x 25 byk byk 4096 Sep 16 20:03 /tmp/opencode dc661f30cb2055ad6248fdc59b14998d834497be .lore.md AGENTS.md packages/core/schema.json packages/core/src/database/migration.gen.ts packages/core/src/database/migration/20260914170650_session-recovery.ts packages/core/src/database/schema.gen.ts packages/core/src/session.ts packages/core/src/session/compaction.ts packages/core/src/session/context-epoch.ts packages/core/src/session/execution/local.ts packages/core/src/session/input.ts packages/core/src/session/message-updater.ts packages/core/src/session/projector.ts packages/core/src/session/recovery.ts packages/core/src/session/run-coordinator.ts packages/core/src/session/runner/index.ts packages/core/src/session/runner/llm.ts packages/core/src/session/runner/publish-llm-event.ts packages/core/src/session/sql.ts packages/core/test/database-migration.test.ts packages/core/test/session-execution-local.test.ts packages/core/test/session-projector.test.ts packages/core/test/session-prompt...
Checking patch packages/core/schema.json... Checking patch packages/core/src/database/migration.gen.ts... Checking patch packages/core/src/database/migration/20260914170650_session-recovery.ts... Checking patch packages/core/src/database/schema.gen.ts... Checking patch packages/core/src/session.ts... Checking patch packages/core/src/session/compaction.ts... Checking patch packages/core/src/session/context-epoch.ts... Checking patch packages/core/src/session/execution/local.ts... Checking patch packages/core/src/session/input.ts... Checking patch packages/core/src/session/message-updater.ts... Checking patch packages/core/src/session/projector.ts... Checking patch packages/core/src/session/recovery.ts... Checking patch packages/core/src/session/run-coordinator.ts... Checking patch packages/core/src/session/runner/index.ts... Checking patch packages/core/src/session/runner/llm.ts... Checking patch packages/core/src/session/runner/publish-llm-event.ts... ...
<path>/tmp/opencode/session-recovery-final-v13.patch</path>
<type>file</type>
<content>
1: diff --git a/packages/core/schema.json b/packages/core/schema.json
2: index d0eeeebd5c..dedf0cc600 100644
3: --- a/packages/core/schema.json
4: +++ b/packages/core/schema.json
5: @@ -1,8 +1,10 @@
6: {
7: "version": "7",
8: "dialect": "sqlite",
9: - "id": "f14a9b18-8207-487e-a3d3-227e629ba9ad",
10: - "prevIds": ["169a0f0f-d58f-479f-b024-fa1c7b9a09db"],
11: + "id": "3a5ec6d4-a60d-47da-a347-3c72396a13c1",
12: + "prevIds": [
13: + "f14a9b18-8207-487e-a3d3-227e629ba9ad"
14: + ],
15: "ddl": [
16: {
17: "name": "workspace",
18: @@ -60,6 +62,10 @@
19: "name": "session_context_epoch",
20: "entityType": "tables"
21: },
22: + {
23: + "name": "session_execution",
24: + "entityType": "tables"
25: + },
26: {
27: "name": "session_input",
28: "entityType": "tables"
29: @@ -68,6 +74,10 @@
30: "name": "session_message",
31: ...
<path>/tmp/opencode/session-recovery-final-v13.patch</path>
<type>file</type>
<content>
1565: export const node = makeGlobalNode({
1566: service: SessionExecution.Service,
1567: layer,
1568: - deps: [SessionStore.node, LocationServiceMap.node],
1569: + deps: [Database.node, SessionStore.node, LocationServiceMap.node],
1570: })
1571:
1572: export * as SessionExecutionLocal from "./local"
1573: diff --git a/packages/core/src/session/input.ts b/packages/core/src/session/input.ts
1574: index 14b613678d..0b38eb2519 100644
1575: --- a/packages/core/src/session/input.ts
1576: +++ b/packages/core/src/session/input.ts
1577: @@ -1,15 +1,16 @@
1578: export * as SessionInput from "./input"
1579:
1580: -import { and, asc, eq, isNull, lte } from "drizzle-orm"
1581: -import { DateTime, Effect, Schema } from "effect"
1582: +import { and, asc, desc, eq, isNull, lte } from "drizzle-orm"
1583: +import { DateTime, Effect, Option, Schema } from "effect"
1584: import { Admitted, Delivery } ...
<path>/tmp/opencode/session-recovery-final-v13.patch</path>
<type>file</type>
<content>
2806: + isNull(SessionRecoveryTable.execution_id),
2807: + ),
2808: + )
2809: + .limit(1),
2810: + ),
2811: + ),
2812: + )
2813: + .returning({ id: SessionTable.id })
2814: + .get()
2815: + .pipe(
2816: + Effect.orDie,
2817: + Effect.flatMap((updated) => (updated ? execution.wake(row.sessionID) : Effect.void)),
2818: + Effect.catchCause((cause) =>
2819: + Effect.logError("Failed to redrive Session input", cause).pipe(
2820: + Effect.annotateLogs({ sessionID: row.sessionID }),
2821: + ),
2822: + ),
2823: + ),
2824: + { discard: true, concurrency: 1 },
2825: + )
2826: +})
2827: +
2828: +const layer = Layer.effectDiscard(
2829: + recover().pipe(
2830: + Effect.catchCause((cause) => Effect.logError("Failed ...
2f1b5ee3b250604baa6f809b664776c5889266390d24b65b4a0ddae8f2374c8f - 3e9b20cb5ec05d1065b6bc78bab00b47e8ca9296bc819689a4f3d889eddcb5bf /tmp/opencode/session-recovery-final-v13.patch 31
3e9b20cb5ec05d1065b6bc78bab00b47e8ca9296bc819689a4f3d889eddcb5bf -
<path>/home/byk/Code/opencode/packages/core/src/session/execution/claim.ts</path>
<type>file</type>
<content>
1: export * as SessionExecutionClaim from "./claim"
2:
3: import { and, eq, gt } from "drizzle-orm"
4: import { Clock, Effect } from "effect"
5: import type { Database } from "../../database/database"
6: import type { SessionSchema } from "../schema"
7: import { SESSION_EXECUTION_LEASE_MS, SessionExecutionTable } from "../sql"
8:
9: type DatabaseService = Database.Interface["db"]
10:
11: export type Claim = {
12: readonly sessionID: SessionSchema.ID
13: readonly executionID: string
14: readonly ownerID: string
15: }
16:
17: export type Values = Partial<
18: Pick<typeof SessionExecutionTable.$inferInsert, "assistant_message_id" | "assistant_seq" | "phase">
19: >
20:
21: export class Lost extends Error {
22: constructor(readonly claim: Claim) {
23: super(`Session execution claim lost: ${claim.executionID}`)
24: }
25: }
26:
27: const where = (claim: Claim, ...
<path>/home/byk/Code/opencode/packages/core/src/session/execution/local.ts</path>
<type>file</type>
<content>
1: import { and, asc, desc, eq, isNull } from "drizzle-orm"
2: import { Cause, Clock, Duration, Effect, Exit, Layer, Schedule } from "effect"
3: import { Database } from "../../database/database"
4: import { LocationServiceMap } from "../../location-service-map"
5: import { makeGlobalNode } from "../../effect/app-node"
6: import { SessionRunCoordinator } from "../run-coordinator"
7: import { SessionRunner } from "../runner"
8: import { SessionMessage } from "../message"
9: import { SessionSchema } from "../schema"
10: import { SessionStore } from "../store"
11: import { SessionExecution } from "../execution"
12: import { SessionExecutionClaim } from "./claim"
13: import {
14: SESSION_EXECUTION_LEASE_MS,
15: SessionExecutionTable,
16: SessionMessageTable,
17: SessionRecoveryTable,
18: } from "../sql"
19:
20: /** Current-process routing for implicit-local Locations. ...
Found 1 matches /home/byk/Code/opencode/packages/core/src/event.ts: Line 123: readonly commit?: (seq: number) => Effect.Effect<void>
<path>/home/byk/Code/opencode/packages/core/src/event.ts</path>
<type>file</type>
<content>
100: },
101: data: event.data,
102: }),
103: )
104: return {
105: events,
106: hasMore: rows.length > input.limit,
107: }
108: })
109:
110: export class SubscriberOverflowError extends Schema.TaggedErrorClass<SubscriberOverflowError>()(
111: "EventV2.SubscriberOverflow",
112: { capacity: Schema.Int },
113: ) {}
114:
115: export const define = Event.define
116: export const versionedType = Event.versionedType
117:
118: export interface PublishOptions {
119: readonly id?: ID
120: readonly metadata?: Record<string, unknown>
121: readonly location?: Location.Ref
122: /** Local operational projection committed atomically with a new durable event. Not replayed or serialized. */
123: readonly commit?: (seq: number) => Effect.Effect<void>
124: }
125:
126: export interface Interface {
127: readonly publish: <D extends Definition>(
128: definition: ...
<path>/home/byk/Code/opencode/packages/core/src/event.ts</path>
<type>file</type>
<content>
236: const list = projectors.get(event.type) ?? []
237: return yield* Effect.uninterruptible(
238: Effect.gen(function* () {
239: const committed = yield* db
240: .transaction(
241: () =>
242: Effect.gen(function* () {
243: const row = yield* db
244: .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id })
245: .from(EventSequenceTable)
246: .where(eq(EventSequenceTable.aggregate_id, aggregateID))
247: .get()
248: .pipe(Effect.orDie)
249: const latest = row?.seq ?? -1
250: const encoded = ...
<path>/home/byk/Code/opencode/packages/core/src/session/recovery-id.ts</path>
<type>file</type>
<content>
1: import { SessionMessage } from "./message"
2:
3: export const message = (executionID: string, assistantMessageID?: SessionMessage.ID) =>
4: assistantMessageID ?? SessionMessage.ID.make(`msg_recovery_${executionID}`)
5:
6: export const continuation = (executionID: string) => SessionMessage.ID.make(`msg_recovery_continue_${executionID}`)
(End of file - total 6 lines)
</content>
Found 63 matches
/home/byk/Code/opencode/packages/core/src/session/recovery.ts:
Line 48: and(isNull(SessionRecoveryTable.execution_id), eq(SessionInputTable.wake_pending, true)),
Line 50: eq(SessionRecoveryTable.wake_pending, true),
Line 51: isNull(SessionRecoveryTable.recovery_error),
Line 74: .where(and(lte(SessionExecutionTable.expires_at, scanTime), isNull(SessionExecutionTable.recovery_error)))
Line 93: isNull(SessionExecutionTable.recovery_error),
Line 106: .set({ recovery_error: `Invalid execution phase: ${String(claim.phase)}` })
Line 125: .set({ recovery_error: "Cancellation outcome unknown after owner loss" })
Line 132: isNull(SessionExecutionTable.recovery_error),
Line 143: yield* SessionExecutionClaim.clear(db, recoveryClaim)
Line 161: yield* SessionExecutionClaim.clear(db, recoveryClaim)
Line 203: yield* ...
Found 100 matches (more matches available)
/home/byk/Code/opencode/packages/core/test/session-runner.test.ts:
Line 322: owner_id: "test",
Line 331: owner_id: "test",
Line 587: const fiber = yield* runner.run({ sessionID, force: true, executionID, ownerID: "test" }).pipe(Effect.forkChild)
Line 752: it.effect("never dispatches a provider after ownership changes", () =>
Line 767: .set({ id: "provider-successor", owner_id: "successor", expires_at: 60_000 })
Line 778: it.effect("never publishes streamed output after ownership changes", () =>
Line 785: response = fragmentFixture("text", "text-fenced", ["stale output"]).completeEvents
Line 793: .set({ id: "publication-successor", owner_id: "successor", expires_at: 60_000 })
Line 805: it.effect("never starts a local tool after ownership changes", () =>
Line 823: .set({ id: "tool-successor", owner_id: "successor", expires_at: 60_000 })
Line 845: ...
<path>/home/byk/Code/opencode/packages/server/src/routes.ts</path>
<type>file</type>
<content>
1: import { Database } from "@opencode-ai/core/database/database"
2: import { LayerNode } from "@opencode-ai/core/effect/layer-node"
3: import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
4: import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
5: import { EventV2 } from "@opencode-ai/core/event"
6: import { Credential } from "@opencode-ai/core/credential"
7: import { PermissionSaved } from "@opencode-ai/core/permission/saved"
8: import { PtyTicket } from "@opencode-ai/core/pty/ticket"
9: import { SessionV2 } from "@opencode-ai/core/session"
10: import { SessionExecution } from "@opencode-ai/core/session/execution"
11: import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
12: import { SessionExecutionLocal } from "@opencode-ai/core/session/execution/local"
13: import { SessionRecovery } from ...
<path>/home/byk/Code/opencode/packages/opencode/src/server/routes/instance/httpapi/server.ts</path>
<type>file</type>
<content>
55: import { LayerNode } from "@opencode-ai/core/effect/layer-node"
56: import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
57: import { EventV2 } from "@opencode-ai/core/event"
58: import { ModelsDev } from "@opencode-ai/core/models-dev"
59: import { Npm } from "@opencode-ai/core/npm"
60: import { PermissionSaved } from "@opencode-ai/core/permission/saved"
61: import { ProjectV2 } from "@opencode-ai/core/project"
62: import { ProjectCopy } from "@opencode-ai/core/project/copy"
63: import { PtyTicket } from "@opencode-ai/core/pty/ticket"
64: import { Ripgrep } from "@opencode-ai/core/ripgrep"
65: import { SessionProjector } from "@opencode-ai/core/session/projector"
66: import { SessionV2 } from "@opencode-ai/core/session"
67: import { SessionExecution } from "@opencode-ai/core/session/execution"
68: import * as SessionExecutionLocal from ...
Found 2 matches /home/byk/Code/opencode/packages/opencode/src/server/routes/instance/httpapi/server.ts: Line 213: const app = LayerNode.group([ Line 300: AppNodeBuilderV1.build(LayerNode.group([SessionV2.node, SessionRecovery.node]), [
<path>/home/byk/Code/opencode/packages/opencode/src/server/routes/instance/httpapi/server.ts</path> <type>file</type> <content> 205: 206: type RouteRequirements = 207: | HttpRouter.HttpRouter 208: | HttpRouter.Request<"Error", unknown> 209: | HttpRouter.Request<"GlobalError", unknown> 210: | HttpRouter.Request<"Requires", unknown> 211: | HttpRouter.Request<"GlobalRequires", never> 212: 213: const app = LayerNode.group([ 214: Npm.node, 215: FSUtil.node, 216: Database.node, 217: Auth.node, 218: Account.node, 219: Config.node, 220: Env.node, 221: Git.node, 222: Ripgrep.node, 223: Storage.node, 224: Snapshot.node, 225: Plugin.node, 226: ModelsDev.node, 227: Provider.node, 228: ProviderAuth.node, 229: Agent.node, 230: Skill.node, 231: Discovery.node, 232: Question.node, 233: Permission.node, 234: PermissionSaved.node, 235: Todo.node, 236: Session.node, 237: SessionProjector.node, 238: SessionStatus.node, 239: ...
<path>/home/byk/Code/opencode/packages/schema/src/session-event.ts</path>
<type>file</type>
<content>
70: messageID: SessionMessage.ID,
71: model: Model.Ref,
72: },
73: })
74: export type ModelSwitched = typeof ModelSwitched.Type
75:
76: export const Moved = Event.define({
77: type: "session.next.moved",
78: ...options,
79: schema: {
80: ...Base,
81: location: Location.Ref,
82: subdirectory: RelativePath.pipe(optional),
83: },
84: })
85: export type Moved = typeof Moved.Type
86:
87: export const Prompted = Event.define({
88: type: "session.next.prompted",
89: ...options,
90: schema: PromptFields,
91: })
92: export type Prompted = typeof Prompted.Type
93:
94: export const PromptAdmitted = Event.define({
95: type: "session.next.prompt.admitted",
96: ...options,
97: schema: {
98: ...PromptFields,
99: requestExecution: Schema.Boolean.pipe(optional),
100: },
101: })
102: export type PromptAdmitted = typeof PromptAdmitted.Type
103: ...
<path>/home/byk/Code/opencode/packages/core/src/session/recovery.ts</path>
<type>file</type>
<content>
60: .get()
61: .pipe(Effect.orDie)
62: return pending !== undefined
63: })
64:
65: export const recover = Effect.fn("SessionRecovery.recover")(function* () {
66: const { db } = yield* Database.Service
67: const events = yield* EventV2.Service
68: const execution = yield* SessionExecution.Service
69: const ownerID = crypto.randomUUID()
70: const scanTime = yield* Clock.currentTimeMillis
71: const claims = yield* db
72: .select()
73: .from(SessionExecutionTable)
74: .where(and(lte(SessionExecutionTable.expires_at, scanTime), isNull(SessionExecutionTable.recovery_error)))
75: .orderBy(asc(SessionExecutionTable.time_created), asc(SessionExecutionTable.id))
76: .limit(BATCH_SIZE)
77: .all()
78: .pipe(Effect.orDie)
79: yield* Effect.forEach(
80: claims,
81: (claim) =>
82: Effect.gen(function* () {
83: const now = ...
<path>/home/byk/Code/opencode/packages/core/src/session/run-coordinator.ts</path>
<type>file</type>
<content>
1: export * as SessionRunCoordinator from "./run-coordinator"
2:
3: import { Deferred, Effect, Exit, Fiber, FiberSet, Scope } from "effect"
4:
5: /** Serializes execution for each key while allowing different keys to run concurrently. */
6: export interface Coordinator<Key, E> {
7: /** Snapshots keys with an execution owned by this coordinator. */
8: readonly active: Effect.Effect<ReadonlySet<Key>>
9: /** Starts execution while idle or joins the active execution. */
10: readonly run: (key: Key) => Effect.Effect<void, E>
11: /** Registers one coalesced follow-up after newly recorded work. */
12: readonly wake: (key: Key) => Effect.Effect<void>
13: /** Stops active execution, runs cleanup, then starts work that arrived while stopping. */
14: readonly interrupt: (key: Key, cleanup?: Effect.Effect<void>) => Effect.Effect<void>
15: }
16:
17: type Entry<E> = {
18: ...
Found 45 matches
/home/byk/Code/opencode/packages/core/test/session-projector.test.ts:
Line 68: it.effect("atomically reconciles one abandoned execution into one continuation", () =>
Line 222: it.effect("records a recovery continuation without forging an assistant identity", () =>
Line 258: it.effect("recovers each abandoned execution once across repeated startup scans", () =>
Line 304: it.effect("never recovers a live execution lease", () =>
Line 342: it.effect("never recovers an execution superseded by a newer assistant", () =>
Line 504: it.effect("recovers assistant-less continuation at its current assistant boundary", () =>
Line 555: it.effect("redrives a durable recovery wake after admission", () =>
Line 606: it.effect("quarantines a malformed recovery row without blocking a valid wake", () =>
Line 660: it.effect("quarantines a full batch of poison recovery rows before redriving valid work", () =>
Line 725: it.effect("fairly ...
<path>/home/byk/Code/opencode/packages/core/test/session-projector.test.ts</path>
<type>file</type>
<content>
330: expires_at: Date.now() + 60_000,
331: })
332: .run()
333:
334: yield* SessionRecovery.recover().pipe(Effect.provideService(SessionExecution.Service, recoveryExecution))
335:
336: expect(yield* db.select().from(SessionExecutionTable).all()).toHaveLength(1)
337: expect(yield* db.select().from(SessionRecoveryTable).all()).toEqual([])
338: expect(yield* db.select().from(SessionInputTable).all()).toEqual([])
339: }),
340: )
341:
342: it.effect("never recovers an execution superseded by a newer assistant", () =>
343: Effect.gen(function* () {
344: const { db } = yield* Database.Service
345: const abandonedID = SessionMessage.ID.make("msg_recovery_abandoned")
346: yield* db
347: .insert(ProjectTable)
348: .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), ...
<path>/home/byk/Code/opencode/packages/core/test/database-migration.test.ts</path>
<type>file</type>
<content>
150: sql`SELECT name FROM sqlite_master WHERE type = 'index' AND name IN ('event_aggregate_seq_idx', 'event_aggregate_type_seq_idx', 'session_input_session_pending_seq_idx', 'session_input_session_pending_delivery_seq_idx', 'session_input_session_admitted_seq_idx', 'session_input_session_promoted_seq_idx', 'session_message_session_idx', 'session_message_session_type_idx', 'session_message_session_seq_idx', 'session_message_session_type_seq_idx', 'session_message_session_time_created_id_idx') ORDER BY name`,
151: ),
152: ).toEqual([
153: { name: "event_aggregate_seq_idx" },
154: { name: "event_aggregate_type_seq_idx" },
155: { name: "session_input_session_admitted_seq_idx" },
156: { name: "session_input_session_pending_delivery_seq_idx" },
157: { name: "session_input_session_promoted_seq_idx" ...
<path>/home/byk/Code/opencode/packages/core/test/session-execution-local.test.ts</path>
<type>file</type>
<content>
100: it.effect("clears the claim after explicit interruption", () =>
101: Effect.gen(function* () {
102: yield* insertSession
103: const started = Deferred.makeUnsafe<void>()
104: const execution = yield* SessionExecutionLocal.make({
105: run: () => Deferred.succeed(started, undefined).pipe(Effect.andThen(Effect.never)),
106: })
107: yield* execution.wake(sessionID)
108: yield* Deferred.await(started)
109: yield* execution.interrupt(sessionID)
110: expect(yield* claims).toEqual([])
111: }),
112: )
113:
114: it.effect("never starts a second owner while the lease is live", () =>
115: Effect.gen(function* () {
116: yield* insertSession
117: const firstStarted = Deferred.makeUnsafe<void>()
118: const firstScope = yield* Scope.make()
119: const first = yield* ...
<path>/home/byk/Code/opencode/packages/core/test/session-projector.test.ts</path>
<type>file</type>
<content>
1790: phase: "ready",
1791: time_created: 1,
1792: expires_at: 0,
1793: })
1794: .run()
1795:
1796: yield* SessionRecovery.recover().pipe(Effect.provideService(SessionExecution.Service, recoveryExecution))
1797:
1798: expect(
1799: yield* db
1800: .select({ error: SessionExecutionTable.recovery_error })
1801: .from(SessionExecutionTable)
1802: .where(eq(SessionExecutionTable.id, "malformed"))
1803: .get(),
1804: ).toEqual({ error: "Invalid execution phase: invalid" })
1805: expect(yield* db.select().from(SessionRecoveryTable).all()).toEqual([
1806: expect.objectContaining({ execution_id: "valid" }),
1807: ])
1808: }),
1809: )
1810:
1811: it.effect("never recovers an expired cancellation claim", () =>
1812: Effect.gen(function* () ...
<path>/home/byk/Code/opencode/packages/core/test/session-projector.test.ts</path>
<type>file</type>
<content>
1235: Effect.provideService(
1236: SessionExecution.Service,
1237: SessionExecution.Service.of({
1238: ...recoveryExecution,
1239: wake: (id) => Effect.sync(() => wakes.push(id)).pipe(Effect.asVoid),
1240: }),
1241: ),
1242: )
1243:
1244: expect(wakes).toEqual([sessionID])
1245: expect(yield* db.select().from(SessionExecutionTable).all()).toEqual([])
1246: expect(yield* db.select().from(SessionRecoveryTable).all()).toHaveLength(1)
1247: expect(yield* db.select().from(SessionInputTable).all()).toHaveLength(1)
1248: }),
1249: )
1250:
1251: it.effect("never recovers a pre-promotion claim when input commits at the recovery boundary", () =>
1252: Effect.gen(function* () {
1253: const { db } = yield* Database.Service
1254: const events = yield* ...
<path>/home/byk/Code/opencode/packages/core/test/session-execution-local.test.ts</path>
<type>file</type>
<content>
1: import { describe, expect } from "bun:test"
2: import { Deferred, Effect, Exit, Fiber, Scope } from "effect"
3: import { TestClock } from "effect/testing"
4: import { eq, sql } from "drizzle-orm"
5: import { Database } from "@opencode-ai/core/database/database"
6: import { Project } from "@opencode-ai/core/project"
7: import { ProjectTable } from "@opencode-ai/core/project/sql"
8: import { AbsolutePath } from "@opencode-ai/core/schema"
9: import { SessionExecutionLocal } from "@opencode-ai/core/session/execution/local"
10: import { SessionSchema } from "@opencode-ai/core/session/schema"
11: import { SessionExecutionTable, SessionTable } from "@opencode-ai/core/session/sql"
12: import { testEffect } from "./lib/effect"
13:
14: const sessionID = SessionSchema.ID.make("ses_execution_local")
15: const it = testEffect(Database.layerFromPath(":memory:"))
16:
17: const ...
Found 35 matches
/home/byk/Code/opencode/packages/core/test/session-runner.test.ts:
Line 43: import { ConfigCompaction } from "@opencode-ai/core/config/compaction"
Line 224: compaction: new ConfigCompaction.Info({
Line 1383: it.effect("rebuilds the baseline directly after completed compaction", () =>
Line 1393: const compactionID = SessionMessage.ID.create()
Line 1396: messageID: compactionID,
Line 1402: messageID: compactionID,
Line 1442: prompt: Prompt.make({ text: "Recent exact request ".repeat(180) }),
Line 1461: expect(userTexts(requests[1])[0]).toContain(`[User]: ${"Recent exact request ".repeat(180)}`)
Line 1464: expect(context.map((message) => message.type)).toEqual(["compaction", "assistant"])
Line 1466: type: "compaction",
Line 1478: prompt: Prompt.make({ text: "Newest exact request ".repeat(180) }),
Line 1487: expect(userTexts(requests[0])[0]).toContain("Recent ...
<path>/home/byk/Code/opencode/packages/core/test/session-runner.test.ts</path>
<type>file</type>
<content>
1480: })
1481: yield* session.resume(sessionID)
1482:
1483: expect(requests).toHaveLength(2)
1484: expect(userTexts(requests[0])[0]).toContain(
1485: "<prior-summary>\n## Objective\n- Preserve the task\n</prior-summary>",
1486: )
1487: expect(userTexts(requests[0])[0]).toContain("Recent exact request")
1488: expect((yield* (yield* SessionStore.Service).context(sessionID))[0]).toMatchObject({
1489: type: "compaction",
1490: summary: "## Objective\n- Preserve the updated task",
1491: })
1492: }),
1493: )
1494:
1495: it.effect("keeps automatic compaction unknown until Compaction.Ended commits", () =>
1496: Effect.gen(function* () {
1497: yield* setup
1498: const session = yield* SessionV2.Service
1499: const { db } = yield* Database.Service
1500: response = ...
Found 29 matches
/home/byk/Code/opencode/packages/core/test/session-prompt.test.ts:
Line 140: it.effect("exposes the execution registry", () =>
Line 147: it.effect("delegates execution continuation through SessionExecution", () =>
Line 159: it.effect("delegates process-local interruption through SessionExecution", () =>
Line 171: it.effect("delegates interruption without requiring a recorded Session", () =>
Line 181: it.effect("durably admits one user message before transcript promotion", () =>
Line 203: it.effect("resolves attachment MIME before admission", () =>
Line 224: it.effect("streams durable Session events after an aggregate sequence", () =>
Line 254: it.effect("resumes through a recorded message without appending another prompt", () =>
Line 275: it.effect("records distinct messages when the ID is omitted", () =>
Line 290: it.effect("returns the original recorded message when the ID is retried", () =>
Line 310: ...
Found 25 matches
/home/byk/Code/opencode/packages/sdk/js/src/v2/gen/types.gen.ts:
Line 24: | EventSessionNextPromptExecutionRequested
Line 32: | EventSessionNextStepInterrupted
Line 33: | EventSessionNextStepRecovered
Line 874: requestExecution?: boolean
Line 1650: | SyncEventSessionNextPromptExecutionRequested
Line 1658: | SyncEventSessionNextStepInterrupted
Line 1659: | SyncEventSessionNextStepRecovered
Line 2785: | SessionNextPromptExecutionRequested
Line 2793: | SessionNextStepInterrupted
Line 2794: | SessionNextStepRecovered
Line 2915: | SessionNextPromptExecutionRequested
Line 2923: | SessionNextStepInterrupted
Line 2924: | SessionNextStepRecovered
Line 3430: requestExecution?: boolean
Line 3435: export type SyncEventSessionNextPromptExecutionRequested = {
Line 3585: export type SyncEventSessionNextStepInterrupted = {
Line 3602: export type SyncEventSessionNextStepRecovered = {
...
Found 12 matches
/home/byk/Code/opencode/packages/sdk/openapi.json:
Line 15336: "$ref": "#/components/schemas/EventSessionNextStepInterrupted"
Line 15339: "$ref": "#/components/schemas/EventSessionNextStepRecovered"
Line 17899: "requestExecution": {
Line 20604: "$ref": "#/components/schemas/SyncEventSessionNextStepInterrupted"
Line 20607: "$ref": "#/components/schemas/SyncEventSessionNextStepRecovered"
Line 25709: "requestExecution": {
Line 26226: "SyncEventSessionNextStepInterrupted": {
Line 26283: "SyncEventSessionNextStepRecovered": {
Line 28599: "requestExecution": {
Line 34811: "requestExecution": {
Line 35156: "EventSessionNextStepInterrupted": {
Line 35192: "EventSessionNextStepRecovered": {
<path>/home/byk/Code/opencode/packages/schema/test/event-manifest.test.ts</path>
<type>file</type>
<content>
1: import { describe, expect, test } from "bun:test"
2: import { FileSystem, Integration, Permission, Project, Reference, Session, Workspace } from "../src"
3: import { EventManifest } from "../src/event-manifest"
4: import { IdeEvent } from "../src/ide-event"
5: import { SessionEvent } from "../src/session-event"
6: import { SessionTodo } from "../src/session-todo"
7: import { SessionV1 } from "../src/session-v1"
8: import { WorkspaceEvent } from "../src/workspace-event"
9:
10: describe("public event manifest", () => {
11: test("owns the complete public event surface", () => {
12: expect(EventManifest.ServerDefinitions.length).toBe(61)
13: expect(EventManifest.Definitions.length).toBe(91)
14: expect(SessionV1.Event.Definitions).toEqual([
15: SessionV1.Event.Created,
16: SessionV1.Event.Updated,
17: SessionV1.Event.Deleted,
18: ...
Found 12 matches
/home/byk/Code/opencode/packages/schema/src/session-event.ts:
Line 104: export const PromptExecutionRequested = Event.define({
Line 112: export type PromptExecutionRequested = typeof PromptExecutionRequested.Type
Line 209: export const Interrupted = Event.define({
Line 218: export type Interrupted = typeof Interrupted.Type
Line 220: export const Recovered = Event.define({
Line 230: export type Recovered = typeof Recovered.Type
Line 490: PromptExecutionRequested,
Line 498: Step.Interrupted,
Line 499: Step.Recovered,
Line 524: PromptExecutionRequested,
Line 532: Step.Interrupted,
Line 533: Step.Recovered,
<path>/home/byk/Code/opencode/packages/schema/src/session-event.ts</path>
<type>file</type>
<content>
470: export namespace RevertEvent {
471: export const Staged = Event.define({
472: type: "session.next.revert.staged",
473: ...options,
474: schema: { ...Base, revert: Revert.State },
475: })
476: export const Cleared = Event.define({ type: "session.next.revert.cleared", ...options, schema: Base })
477: export const Committed = Event.define({
478: type: "session.next.revert.committed",
479: ...options,
480: schema: { ...Base, messageID: SessionMessage.ID },
481: })
482: }
483:
484: export const DurableDefinitions = Event.inventory(
485: AgentSwitched,
486: ModelSwitched,
487: Moved,
488: Prompted,
489: PromptAdmitted,
490: PromptExecutionRequested,
491: ContextUpdated,
492: Synthetic,
493: Shell.Started,
494: Shell.Ended,
495: Step.Started,
496: Step.Ended,
497: Step.Failed,
498: Step.Interrupted,
499: ...
<path>/home/byk/Code/opencode/packages/schema/src/session-event.ts</path>
<type>file</type>
<content>
1: export * as SessionEvent from "./session-event"
2:
3: import { Schema } from "effect"
4: import { optional } from "./schema"
5: import { Event } from "./event"
6: import { ProviderMetadata, ToolContent } from "./llm"
7: import { Delivery } from "./session-delivery"
8: import { Model } from "./model"
9: import { DateTimeUtcFromMillis, NonNegativeInt, RelativePath } from "./schema"
10: import { FileAttachment, Prompt } from "./prompt"
11: import { SessionID } from "./session-id"
12: import { Location } from "./location"
13: import { SessionMessage } from "./session-message"
14: import { Revert } from "./revert"
15:
16: export { FileAttachment }
17:
18: export const Source = Schema.Struct({
19: start: NonNegativeInt,
20: end: NonNegativeInt,
21: text: Schema.String,
22: }).annotate({
23: identifier: "session.next.event.source",
24: })
25: export interface Source extends ...
Found 100 matches (more matches available) /home/byk/Code/opencode/packages/server/src/handlers/event.ts: Line 22: const events = yield* EventV2.Service /home/byk/Code/opencode/packages/core/test/session-runner.test.ts: Line 404: const events = yield* EventV2.Service Line 416: yield* events.replayAll( Line 523: const events = yield* EventV2.Service Line 623: const events = yield* EventV2.Service Line 719: const events = yield* EventV2.Service Line 809: const events = yield* EventV2.Service Line 848: const events = yield* EventV2.Service Line 1038: const events = yield* EventV2.Service Line 1200: const events = yield* EventV2.Service Line 1229: const events = yield* EventV2.Service Line 1261: const events = yield* EventV2.Service Line 1310: const events = yield* EventV2.Service Line 1355: const events = yield* EventV2.Service Line 1387: const events = yield* ...
<path>/home/byk/Code/opencode/packages/core/test/session-projector.test.ts</path>
<type>file</type>
<content>
60: id: _,
61: type,
62: ...data
63: } = encodeMessage(SessionMessage.Assistant.make({ id, type: "assistant", agent: "build", model, content: [], time }))
64: return { id, session_id: sessionID, type, seq, time_created: DateTime.toEpochMillis(time.created), data }
65: }
66:
67: describe("SessionProjector", () => {
68: it.effect("atomically reconciles one abandoned execution into one continuation", () =>
69: Effect.gen(function* () {
70: const { db } = yield* Database.Service
71: const events = yield* EventV2.Service
72: const assistantID = SessionMessage.ID.make("msg_recovery_assistant")
73: const continuationID = continuation("execution")
74: yield* db
75: .insert(ProjectTable)
76: .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
77: .run()
78: yield* ...
Found 1 matches /home/byk/Code/opencode/packages/core/src/session/message-updater.ts: Line 96: const updateOwnedAssistant = (messageID: SessionMessage.ID, recipe: (draft: DraftAssistant) => void) =>
<path>/home/byk/Code/opencode/packages/core/src/session/message-updater.ts</path> <type>file</type> <content> 80: type DraftAssistant = WritableDraft<SessionMessage.Assistant> 81: type DraftTool = WritableDraft<SessionMessage.AssistantTool> 82: type DraftText = WritableDraft<SessionMessage.AssistantText> 83: type DraftReasoning = WritableDraft<SessionMessage.AssistantReasoning> 84: 85: const latestTool = (assistant: DraftAssistant | undefined, callID?: string) => 86: assistant?.content.findLast( 87: (item): item is DraftTool => item.type === "tool" && (callID === undefined || item.id === callID), 88: ) 89: 90: const latestText = (assistant: DraftAssistant | undefined, textID: string) => 91: assistant?.content.findLast((item): item is DraftText => item.type === "text" && item.id === textID) 92: 93: const latestReasoning = (assistant: DraftAssistant | undefined, reasoningID: string) => 94: assistant?.content.findLast((item): item is DraftReasoning ...
Found 1 matches /home/byk/Code/opencode/packages/core/src/session/projector.ts: Line 515: .delete(SessionMessageTable)
<path>/home/byk/Code/opencode/packages/core/src/session/projector.ts</path>
<type>file</type>
<content>
480: yield* events.project(SessionEvent.Compaction.Ended, (event) => run(db, event))
481: yield* events.project(SessionEvent.RevertEvent.Staged, (event) =>
482: db
483: .update(SessionTable)
484: .set({
485: revert: { ...event.data.revert, files: event.data.revert.files ? [...event.data.revert.files] : undefined },
486: time_updated: DateTime.toEpochMillis(event.data.timestamp),
487: })
488: .where(eq(SessionTable.id, event.data.sessionID))
489: .run()
490: .pipe(Effect.orDie, Effect.asVoid),
491: )
492: yield* events.project(SessionEvent.RevertEvent.Cleared, (event) =>
493: db
494: .update(SessionTable)
495: .set({ revert: null, time_updated: DateTime.toEpochMillis(event.data.timestamp) })
496: .where(eq(SessionTable.id, event.data.sessionID))
497: ...
Found 4 matches
/home/byk/Code/opencode/packages/core/src/session.ts:
Line 169: readonly resume: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | SessionRunner.RunError>
Line 170: readonly interrupt: (sessionID: SessionSchema.ID) => Effect.Effect<void>
Line 427: resume: Effect.fn("V2Session.resume")(function* (sessionID) {
Line 431: interrupt: Effect.fn("V2Session.interrupt")((sessionID) =>
<path>/home/byk/Code/opencode/packages/core/src/session.ts</path>
<type>file</type>
<content>
415: model: input.model,
416: })
417: }),
418: compact: Effect.fn("V2Session.compact")(function* (input) {
419: yield* result.get(input.sessionID)
420: return yield* new OperationUnavailableError({ operation: "compact" })
421: }),
422: wait: Effect.fn("V2Session.wait")(function* (sessionID) {
423: yield* result.get(sessionID)
424: return yield* new OperationUnavailableError({ operation: "wait" })
425: }),
426: active: execution.active,
427: resume: Effect.fn("V2Session.resume")(function* (sessionID) {
428: yield* result.get(sessionID)
429: yield* execution.resume(sessionID)
430: }),
431: interrupt: Effect.fn("V2Session.interrupt")((sessionID) =>
432: Effect.uninterruptible(execution.interrupt(sessionID)),
433: ),
434: revert: {
435: stage: ...
Found 1 matches
/home/byk/Code/opencode/packages/core/test/session-runner-recorded.test.ts:
Line 132: it.effect("executes one recorded V2 prompt through the recorded HTTP transport", () =>
Found 5 matches
/home/byk/Code/opencode/packages/core/test/session-runner-tool-events.test.ts:
Line 92: test("local tool success serializes media base64 once and reconstructs from structured content", async () => {
Line 115: test("provider-executed success retains its compatibility result", async () => {
Line 123: test("binary failure emits no success event", async () => {
Line 139: test("old success event data containing result still decodes", () => {
Line 168: test("step finish records settlement without publishing step ended", async () => {
Found 17 matches
/home/byk/Code/opencode/packages/core/test/session-run-coordinator.test.ts:
Line 9: it.effect("joins concurrent resumes for one key", () =>
Line 31: it.effect("joins a wake-started execution without forcing a successor", () =>
Line 57: it.effect("starts execution when woken while idle", () =>
Line 69: it.effect("snapshots only active executions", () =>
Line 102: it.effect("cleans active executions after failure and defect", () =>
Line 122: it.effect("cleans active executions when its scope closes", () =>
Line 141: it.effect("coalesces wakes received during active execution", () =>
Line 173: it.effect("runs again when woken during the follow-up", () =>
Line 209: it.effect("does nothing when interrupted while idle", () =>
Line 218: it.effect("interrupts active execution and clears its pending wake", () =>
Line 247: it.effect("runs a wake registered during interruption cleanup", () =>
Line 285: ...
Found 34 matches
/home/byk/Code/opencode/packages/core/test/session-runner.test.ts:
Line 752: it.effect("never dispatches a provider after ownership changes", () =>
Line 778: it.effect("never publishes streamed output after ownership changes", () =>
Line 805: it.effect("never starts a local tool after ownership changes", () =>
Line 845: it.effect("never fails interrupted tools after ownership changes", () =>
Line 1034: it.effect("interrupts a source Location runner after a Session moves", () =>
Line 1060: it.effect("fails gracefully when a stored context snapshot cannot be decoded", () =>
Line 1225: it.effect("keeps the sampled agent when selection changes during observation", () =>
Line 1257: it.effect("keeps the sampled model when selection changes during model resolution", () =>
Line 1306: it.effect("keeps the baseline and chronological System updates after a model switch", () =>
Line 1351: it.effect("preserves the baseline while ...
Found 7 matches
/home/byk/Code/opencode/packages/core/test/session-projector.test.ts:
Line 160: yield* events.replayAll(
Line 173: const replayedContinuation = yield* db
Line 178: if (!replayedContinuation) return yield* Effect.die("Replayed recovery continuation not found")
Line 199: yield* events.replayAll(
Line 435: it.effect(`never commits assistant-less ${phase} recovery after concurrent assistant supersession`, () =>
Line 2813: it.effect("rejects distinct creator events that reuse one projected message ID", () =>
Line 2834: const id = SessionMessage.ID.make("msg_creator_collision")
<path>/home/byk/Code/opencode/packages/core/test/session-projector.test.ts</path>
<type>file</type>
<content>
2790: "model-switched",
2791: "synthetic",
2792: "shell",
2793: "compaction",
2794: ])
2795: expect(messages.find((message) => message.type === "shell")).toMatchObject({
2796: output: "/project",
2797: time: { completed: DateTime.makeUnsafe(1) },
2798: })
2799: expect(messages.find((message) => message.type === "compaction")).toMatchObject({
2800: summary: "summary",
2801: recent: "recent context",
2802: })
2803: expect(
2804: yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get().pipe(Effect.orDie),
2805: ).toMatchObject({
2806: agent: "build",
2807: model,
2808: time_updated: DateTime.toEpochMillis(created),
2809: })
2810: }),
2811: )
2812:
2813: it.effect("rejects distinct creator events that ...
<path>/home/byk/Code/opencode/packages/core/src/session/input.ts</path>
<type>file</type>
<content>
250: readonly delivery: Delivery
251: readonly timeCreated: DateTime.Utc
252: readonly promotedSeq: number
253: },
254: ) {
255: const validation = yield* validateRecovery(db, input.id)
256: if (validation && (validation.input === undefined || validation.input.promoted_seq === null) && validation.error)
257: return yield* Effect.die(
258: new RecoveryConflict({ executionID: validation.recovery.execution_id, reason: validation.error }),
259: )
260: const updated = yield* db
261: .update(SessionInputTable)
262: .set({ promoted_seq: input.promotedSeq, wake_pending: false })
263: .where(
264: and(
265: eq(SessionInputTable.id, input.id),
266: eq(SessionInputTable.session_id, input.sessionID),
267: isNull(SessionInputTable.promoted_seq),
268: ),
269: )
270: .returning()
271: .get()
272: ...
<path>/home/byk/Code/opencode/packages/core/test/session-projector.test.ts</path>
<type>file</type>
<content>
1440: phase: "continue",
1441: })
1442: yield* db
1443: .insert(SessionExecutionTable)
1444: .values({
1445: id: "conflicting-recovery-identity",
1446: session_id: sessionID,
1447: owner_id: "dead-owner",
1448: phase: "ready",
1449: time_created: 0,
1450: expires_at: 0,
1451: })
1452: .run()
1453:
1454: yield* SessionRecovery.recover().pipe(Effect.provideService(SessionExecution.Service, recoveryExecution))
1455:
1456: expect(yield* db.select().from(SessionExecutionTable).get()).toMatchObject({
1457: id: "conflicting-recovery-identity",
1458: recovery_error: "Recovery projection failed",
1459: })
1460: expect(yield* db.select().from(SessionRecoveryTable).all()).toEqual([
1461: expect.objectContaining({ execution_id: ...
<path>/home/byk/Code/opencode/packages/core/test/session-runner-recorded.test.ts</path>
<type>file</type>
<content>
1: import { HttpRecorder } from "@opencode-ai/http-recorder"
2: import { HttpRecorderInternal } from "@opencode-ai/http-recorder/internal"
3: import * as OpenAIChat from "@opencode-ai/llm/protocols/openai-chat"
4: import { Auth, LLMClient, RequestExecutor } from "@opencode-ai/llm/route"
5: import { Database } from "@opencode-ai/core/database/database"
6: import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
7: import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
8: import { LayerNode } from "@opencode-ai/core/effect/layer-node"
9: import { EventV2 } from "@opencode-ai/core/event"
10: import { EventTable } from "@opencode-ai/core/event/sql"
11: import { PermissionV2 } from "@opencode-ai/core/permission"
12: import { AgentV2 } from "@opencode-ai/core/agent"
13: import { Config } from "@opencode-ai/core/config"
14: import { ...
<path>/home/byk/Code/opencode/packages/core/src/session/runner/llm.ts</path>
<type>file</type>
<content>
180: _tag: "ContinueAfterOverflowCompaction",
181: step,
182: })
183:
184: const loadSystemContext = (agent: AgentV2.Selection) =>
185: Effect.all([systemContext.load(), skillGuidance.load(agent), referenceGuidance.load()], {
186: concurrency: "unbounded",
187: }).pipe(Effect.map(SystemContext.combine))
188:
189: const runTurnAttempt = Effect.fn("SessionRunner.runTurn")(function* (
190: sessionID: SessionSchema.ID,
191: executionID: string,
192: promotion: Promotion | undefined,
193: step: number,
194: ownerID: string,
195: recoverOverflow?: typeof compaction.compactAfterOverflow,
196: ) {
197: const session = yield* getSession(sessionID)
198: if (session.location.directory !== location.directory || session.location.workspaceID !== location.workspaceID)
199: return yield* ...
<path>/home/byk/Code/opencode/packages/server/src/handlers/event.ts</path>
<type>file</type>
<content>
1: import { EventV2 } from "@opencode-ai/core/event"
2: import { OpenCodeEvent } from "@opencode-ai/protocol/groups/event"
3: import { Effect, Schema, Stream } from "effect"
4: import { HttpServerResponse } from "effect/unstable/http"
5: import { HttpApiBuilder } from "effect/unstable/httpapi"
6: import * as Sse from "effect/unstable/encoding/Sse"
7: import { Api } from "../api"
8:
9: const subscriberCapacity = 256
10:
11: function eventData(data: unknown): Sse.Event {
12: return {
13: _tag: "Event",
14: event: "message",
15: id: undefined,
16: data: JSON.stringify(Schema.encodeUnknownSync(OpenCodeEvent)(data)),
17: }
18: }
19:
20: export const EventHandler = HttpApiBuilder.group(Api, "server.event", (handlers) =>
21: Effect.gen(function* () {
22: const events = yield* EventV2.Service
23: return handlers.handleRaw("event.subscribe", () =>
24: ...
3e9b20cb5ec05d1065b6bc78bab00b47e8ca9296bc819689a4f3d889eddcb5bf /tmp/opencode/session-recovery-final-v13.patch c3b8c5d78e473ad4104a5b54617900e28921a283029b8484be7bce4bc3cee85a /home/byk/Code/opencode/packages/opencode/dist/server/opencode-server ab9408c81cc9916177641ae8e9f08624fc27b5b9 33 3e9b20cb5ec05d1065b6bc78bab00b47e8ca9296bc819689a4f3d889eddcb5bf -
A stale runner can quarantine recovery work after losing its lease.
The production caller supplies an ownership guard to promoteExact at packages/core/src/session/runner/llm.ts:235-239. Normal promotion invokes that guard inside the durable event transaction through packages/core/src/session/input.ts:473-484.
However, when projection raises RecoveryConflict, the catch path writes session_recovery.recovery_error directly at packages/core/src/session/input.ts:582-596. Its predicate checks only execution identity, wake state, and existing error. It never checks the active Session execution ID, owner ID, or unexpired lease, and it never invokes the supplied guard.
Therefore, a runner paused across lease expiry or takeover can still mutate durable recovery state and quarantine work owned by its successor. This violates the stale-owner fencing invariant even though most validation errors are monotonic.
Move the quarantine update into an immediate transaction that first validates the exact live claim, then write the error, and only afterward propagate RecoveryConflict. Add a regression that transfers ownership between conflict detection and quarantine and asserts that the stale owner cannot set recovery_error.
Existing coverage at packages/core/test/session-projector.test.ts:1581-1642 calls promoteExact with Effect.void; it does not exercise ownership loss.
ab9408c81cc9916177641ae8e9f08624fc27b5b9 through a base-backed temporary index.3e9b20cb5ec05d1065b6bc78bab00b47e8ca9296bc819689a4f3d889eddcb5bf.packages/core/src/session/recovery.ts:121-138. ...