Dashboard › opencode › Session 1RFB228Ckffw
1RFB228Ckffwzy8jbRead-only correctness review frozen v17. Base ab9408c81cc9916177641ae8e9f08624fc27b5b9. Patch /tmp/opencode/session-recovery-final-v17.patch SHA-256 0eb85245c71fb55fa2a8e169a1cd1de5579059d77aaca30b6ac254482e255a2e. SEA /home/byk/Code/opencode/packages/opencode/dist/server/opencode-server SHA-256 308e9e562a3473b66bac430166db9d6cdd29f5f071d5695c1c35f28b99f26bf4. Verify immutable inputs, clean apply, 34 paths, inspect all. No mutation/tests/builds/services, no recall. Primary v16 security blocker closure: when cancellation arrives after Step.Failed committed while local tool settlement is active, reconciliation still uses cancellation-only fence; publisher exposes durable failed state; runner transitions cancelling->cancelled directly without duplicate Step.Interrupted; cleanup clears exact claim after fibers/finalizers; regression requires one Step.Failed, zero Step.Interrupted, no claim/recovery. Publication defects still retain cancelling. Reassess all ownership/replay/supersession/fairness/schema/generated/auth/dependency invariants and accepted residuals. Evidence exact source: typecheck; 229 recovery pass; full Core 1186/0; migration upgrades 2 pass and script --check pass; Schema/sdk typechecks pass (other package checks from preceding production-equivalent tree); exact artifact smoke health/index/recovery/idempotence pass. Return PASS/CONCERN/MUST-FIX with file:line and end exactly MERGE or DO-NOT-MERGE. Empty invalid.
<path>/home/byk/Code/opencode</path> <type>directory</type> <entries> .dockerignore .editorconfig .git/ .gitattributes .github/ .gitignore .gitleaksignore .husky/ .lore.md .node-cache/ .opencode/ .oxlintrc.json .prettierignore .turbo/ .vscode/ .zed/ AGENTS.md artifacts/ bun.lock bunfig.toml CONTEXT.md CONTRIBUTING.md flake.lock flake.nix github/ infra/ install LICENSE nix/ node_modules/ package.json packages/ patches/ perf.data perf.data.old perf/ pnpm-lock.yaml pnpm-workspace.yaml README.ar.md README.bn.md README.br.md README.bs.md README.da.md README.de.md README.es.md README.fr.md README.gr.md README.it.md README.ja.md README.ko.md README.md README.no.md README.pl.md README.ru.md README.th.md README.tr.md README.uk.md README.vi.md README.zh.md README.zht.md screenshot-uk.png script/ sdks/ SECURITY.md specs/ sst-env.d.ts sst.config.ts STATS.md tmp/ tsconfig.json turbo.json (71 entries) </entries>
0eb85245c71fb55fa2a8e169a1cd1de5579059d77aaca30b6ac254482e255a2e /tmp/opencode/session-recovery-final-v17.patch 308e9e562a3473b66bac430166db9d6cdd29f5f071d5695c1c35f28b99f26bf4 /home/byk/Code/opencode/packages/opencode/dist/server/opencode-server
M .lore.md M AGENTS.md M packages/client/src/generated/types.ts 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 packages/core/src/session/recovery.ts M packages/core/src/session/run-coordinator.ts M packages/core/src/session/runner/index.ts M packages/core/src/session/runner/llm.ts M packages/core/src/session/runner/publish-llm-event.ts M packages/core/src/session/sql.ts M ...
72 0 packages/client/src/generated/types.ts 567 48 packages/core/schema.json 1 0 packages/core/src/database/migration.gen.ts 62 0 packages/core/src/database/migration/20260914170650_session-recovery.ts 52 1 packages/core/src/database/schema.gen.ts 6 4 packages/core/src/session.ts 31 16 packages/core/src/session/compaction.ts 54 34 packages/core/src/session/context-epoch.ts 165 19 packages/core/src/session/execution/local.ts 391 31 packages/core/src/session/input.ts 69 0 packages/core/src/session/message-updater.ts 88 1 packages/core/src/session/projector.ts 504 0 packages/core/src/session/recovery.ts 30 5 packages/core/src/session/run-coordinator.ts 4 0 packages/core/src/session/runner/index.ts 300 104 packages/core/src/session/runner/llm.ts 108 46 packages/core/src/session/runner/publish-llm-event.ts 88 1 packages/core/src/session/sql.ts 66 1 packages/core/test/database-migration.test.ts 352 0 packages/core/test/session-execution-local.test.ts 2771 3 packages/core/test/session-project...
/home/byk/Code/opencode/packages/codemode/AGENTS.md /home/byk/Code/opencode/packages/session-ui/AGENTS.md /home/byk/Code/opencode/packages/schema/AGENTS.md /home/byk/Code/opencode/packages/opencode/AGENTS.md /home/byk/Code/opencode/packages/opencode/test/AGENTS.md /home/byk/Code/opencode/packages/stats/AGENTS.md /home/byk/Code/opencode/packages/opencode/test/server/AGENTS.md /home/byk/Code/opencode/packages/effect-drizzle-sqlite/AGENTS.md /home/byk/Code/opencode/packages/llm/AGENTS.md /home/byk/Code/opencode/packages/ui/AGENTS.md /home/byk/Code/opencode/packages/core/src/tool/AGENTS.md /home/byk/Code/opencode/packages/opencode/src/session/llm/AGENTS.md /home/byk/Code/opencode/packages/opencode/src/server/routes/instance/httpapi/AGENTS.md /home/byk/Code/opencode/packages/app/AGENTS.md /home/byk/Code/opencode/packages/app/e2e/AGENTS.md /home/byk/Code/opencode/packages/app/e2e/performance/AGENTS.md /home/byk/Code/opencode/packages/desktop/AGENTS.md
<path>/home/byk/Code/opencode/packages/schema/AGENTS.md</path> <type>file</type> <content> 1: # Schema Package Guide 2: 3: `@opencode-ai/schema` owns browser-safe wire and storage contracts shared by protocol, server, core, and generated SDKs. Keep runtime behavior, service layers, side effects, and host-local implementation details in the domain package that owns them. 4: 5: ## Package Boundary 6: 7: - Preserve the dependency direction: `@opencode-ai/schema <- @opencode-ai/protocol <- @opencode-ai/server`. 8: - Schema values should be serializable contract definitions, not service implementations or runtime registries. 9: - A domain may keep a minimal public wire contract here when SDK generation needs it, but do not move the broader runtime model into Schema just because an event is public. `plugin.added` is the current example: Schema may own the minimum browser-safe event payload, while plugin runtime behavior stays outside Schema. ...
<path>/home/byk/Code/opencode/packages/opencode/AGENTS.md</path>
<type>file</type>
<content>
1: # opencode database guide
2:
3: ## Database
4:
5: - **Schema**: Drizzle schema lives in `packages/core/src/**/*.sql.ts`.
6: - **Migrations**: database migrations live in `packages/core` and are applied by core.
7:
8: ## Development server
9:
10: - Running `bun dev` from `packages/opencode` starts the live interactive TUI. Do not run it as a blocking foreground command when you need to inspect the result.
11: - Start it in `tmux` instead: `tmux new-session -d -s opencode-dev 'bun dev'`.
12: - Capture the current TUI output with: `tmux capture-pane -pt opencode-dev`.
13: - Stop the session explicitly when done: `tmux kill-session -t opencode-dev`.
14:
15: # Module shape
16:
17: Do not use `export namespace Foo { ... }` for module organization. It is not
18: standard ESM, it prevents tree-shaking, and it breaks Node's native TypeScript
19: runner. ...
<path>/home/byk/Code/opencode/packages/opencode/src/server/routes/instance/httpapi/AGENTS.md</path>
<type>file</type>
<content>
1: # HttpApi Route Patterns
2:
3: Use `HttpApiBuilder.group(...)` for normal HTTP endpoints, including streaming HTTP responses such as server-sent events. Handlers should yield stable services once while building the handler layer, then close over those services in endpoint implementations.
4:
5: ```ts
6: export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", (handlers) =>
7: Effect.gen(function* () {
8: const session = yield* Session.Service
9:
10: return handlers.handle("list", () => session.list())
11: }),
12: )
13: ```
14:
15: For SSE endpoints, stay in `HttpApiBuilder.group(...)` and return `HttpServerResponse.stream(...)` from the handler. Annotate the endpoint success schema with `HttpApiSchema.asText({ contentType: "text/event-stream" })` so OpenAPI documents the stream content type. ...
<path>/tmp/opencode/session-recovery-final-v17.patch</path>
<type>file</type>
<content>
1: diff --git a/packages/client/src/generated/types.ts b/packages/client/src/generated/types.ts
2: index 3b3188c874..5130fa9496 100644
3: --- a/packages/client/src/generated/types.ts
4: +++ b/packages/client/src/generated/types.ts
5: @@ -775,8 +775,17 @@ export type SessionsHistoryOutput = {
6: }>
7: }
8: readonly delivery: "steer" | "queue"
9: + readonly requestExecution?: boolean
10: }
11: }
12: + | {
13: + readonly id: string
14: + readonly metadata?: { readonly [x: string]: JsonValue }
15: + readonly type: "session.next.prompt.execution_requested"
16: + readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
17: + readonly location?: { readonly directory: string; readonly workspaceID?: string }
18: + readonly data: { readonly timestamp: number; ...
<path>/tmp/opencode/session-recovery-final-v17.patch</path>
<type>file</type>
<content>
1563: + .where(and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.type, "assistant")))
1564: + .orderBy(desc(SessionMessageTable.seq))
1565: + .limit(1)
1566: + .get()
1567: + .pipe(Effect.orDie)
1568: + return yield* db
1569: + .insert(SessionExecutionTable)
1570: + .values({
1571: + id: executionID,
1572: + session_id: sessionID,
1573: + owner_id: ownerID,
1574: + phase: "ready",
1575: + assistant_seq: latest?.seq ?? -1,
1576: + time_created: now,
1577: + expires_at: now + leaseDuration,
1578: + })
1579: + .onConflictDoNothing()
1580: + .returning({ id: SessionExecutionTable.id })
1581: + ...
<path>/tmp/opencode/session-recovery-final-v17.patch</path>
<type>file</type>
<content>
2811: + { discard: true, concurrency: 1 },
2812: + )
2813: +
2814: + const retriedRecovery = yield* db
2815: + .select({
2816: + sessionID: SessionRecoveryTable.session_id,
2817: + executionID: SessionRecoveryTable.execution_id,
2818: + continuationMessageID: SessionRecoveryTable.continuation_message_id,
2819: + wakeAttempts: SessionRecoveryTable.wake_attempts,
2820: + })
2821: + .from(SessionRecoveryTable)
2822: + .where(
2823: + and(
2824: + eq(SessionRecoveryTable.wake_pending, true),
2825: + isNull(SessionRecoveryTable.recovery_error),
2826: + gt(SessionRecoveryTable.wake_attempts, 0),
2827: + lte(SessionRecoveryTable.wake_attempts, MAX_WAKE_ATTEMPTS),
2828: + ),
2829: + )
2830: + .orderBy(
2831: + desc(sql`${SessionRecoveryTable.wake_attempts} = ${MAX_WAKE_ATTEMPTS}`),
2832: + ...
<path>/tmp/opencode/session-recovery-final-v17.patch</path>
<type>file</type>
<content>
3956: if (tool.inputEnded) return yield* Effect.die(`Tool input delta after end: ${event.id}`)
3957: yield* toolInput.append(event.id, event.text)
3958: - yield* events.publish(SessionEvent.Tool.Input.Delta, {
3959: + yield* publishEvent(SessionEvent.Tool.Input.Delta, {
3960: sessionID: input.sessionID,
3961: timestamp: yield* timestamp,
3962: assistantMessageID: tool.assistantMessageID,
3963: @@ -317,10 +372,8 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
3964: if (tool.name !== event.name)
3965: return yield* Effect.die(`Tool call name changed for ${event.id}: ${tool.name} -> ${event.name}`)
3966: if (tool.called) return yield* Effect.die(`Duplicate tool call: ${event.id}`)
3967: - tool.called = true
3968: - tool.providerExecuted = ...
<path>/tmp/opencode/session-recovery-final-v17.patch</path>
<type>file</type>
<content>
5237: + wake_pending: true,
5238: + }),
5239: + ])
5240: + }),
5241: + )
5242: +
5243: + it.effect("quarantines a malformed recovery row without blocking a valid wake", () =>
5244: + Effect.gen(function* () {
5245: + const { db } = yield* Database.Service
5246: + const events = yield* EventV2.Service
5247: + const valid = SessionV2.ID.make("ses_recovery_row_valid")
5248: + yield* db
5249: + .insert(ProjectTable)
5250: + .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
5251: + .run()
5252: + yield* db
5253: + .insert(SessionTable)
5254: + .values(
5255: + [sessionID, valid].map((id) => ({
5256: + id,
5257: + project_id: Project.ID.global,
5258: + slug: id,
5259: + directory: "/project",
5260: + title: ...
<path>/tmp/opencode/session-recovery-final-v17.patch</path>
<type>file</type>
<content>
6660: + .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
6661: + .run()
6662: + yield* db
6663: + .insert(SessionTable)
6664: + .values({
6665: + id: sessionID,
6666: + project_id: Project.ID.global,
6667: + slug: "test",
6668: + directory: "/project",
6669: + title: "test",
6670: + version: "test",
6671: + })
6672: + .run()
6673: + yield* db
6674: + .insert(SessionExecutionTable)
6675: + .values({
6676: + id: "expired-cancellation",
6677: + session_id: sessionID,
6678: + owner_id: "cancelled-owner",
6679: + phase: "cancelling",
6680: + time_created: 0,
6681: + expires_at: 0,
6682: + })
6683: + .run()
6684: + const wakes: string[] = []
6685: +
6686: + yield* ...
<path>/tmp/opencode/session-recovery-final-v17.patch</path>
<type>file</type>
<content>
8055: + run: (claim, force, recoveryInputID) =>
8056: + sessionRunner.run({ ...claim, force, recoveryInputID }).pipe(
8057: + Effect.ensuring(Effect.suspend(() => runnerFinalizer)),
8058: + ),
8059: })
8060: }),
8061: -).pipe(Layer.provide(runnerLayer))
8062: +).pipe(Layer.provide([runnerLayer, LayerNode.compile(Database.node)]))
8063: const it = testEffect(
8064: AppNodeBuilder.build(
8065: LayerNode.group([
8066: @@ -309,6 +311,34 @@ const insertSession = (id: SessionV2.ID) =>
8067: .pipe(Effect.orDie)
8068: })
8069:
8070: +const insertExecution = (id: SessionV2.ID) =>
8071: + Effect.gen(function* () {
8072: + const { db } = yield* Database.Service
8073: + yield* db
8074: + .insert(SessionExecutionTable)
8075: + .values({
8076: + id: executionID,
8077: + session_id: id,
8078: + owner_id: "test",
8079: + ...
<path>/tmp/opencode/session-recovery-final-v17.patch</path>
<type>file</type>
<content>
9314: + .select({ type: EventTable.type })
9315: + .from(EventTable)
9316: + .where(eq(EventTable.type, "session.next.step.interrupted.1"))
9317: + .all(),
9318: + ).toHaveLength(1)
9319: + expect(yield* db.select().from(SessionExecutionTable).get()).toMatchObject({ session_id: sessionID })
9320: + expect(
9321: + yield* db
9322: + .insert(SessionExecutionTable)
9323: + .values({
9324: + id: "replacement-during-finalizer",
9325: + session_id: sessionID,
9326: + owner_id: "replacement",
9327: + phase: "ready",
9328: + time_created: 0,
9329: + expires_at: 60_000,
9330: + })
9331: + .onConflictDoNothing()
9332: + .returning({ id: SessionExecutionTable.id })
9333: + .get()
9334: + .pipe(Effect.orDie),
9335: + ...
<path>/tmp/opencode/session-recovery-final-v17.patch</path>
<type>file</type>
<content>
10889: diff --git a/packages/core/src/session/execution/claim.ts b/packages/core/src/session/execution/claim.ts
10890: new file mode 100644
10891: index 0000000000..5c9cd38f23
10892: --- /dev/null
10893: +++ b/packages/core/src/session/execution/claim.ts
10894: @@ -0,0 +1,112 @@
10895: +export * as SessionExecutionClaim from "./claim"
10896: +
10897: +import { and, eq, gt, notInArray } from "drizzle-orm"
10898: +import { Clock, Effect } from "effect"
10899: +import type { Database } from "../../database/database"
10900: +import type { SessionSchema } from "../schema"
10901: +import { SESSION_EXECUTION_LEASE_MS, SessionExecutionTable } from "../sql"
10902: +
10903: +type DatabaseService = Database.Interface["db"]
10904: +
10905: +export type Claim = {
10906: + readonly sessionID: SessionSchema.ID
10907: + readonly executionID: string
10908: + readonly ownerID: string
10909: +}
10910: +
10911: ...
paths=34 base_preimages_match=True OK 3b3188c874..5130fa9496 packages/client/src/generated/types.ts OK d0eeeebd5c..81e5eca808 packages/core/schema.json OK e6ea4eaa14..77948f1529 packages/core/src/database/migration.gen.ts OK 0000000000..20eb1a4373 packages/core/src/database/migration/20260914170650_session-recovery.ts OK ed60fde6c5..94881e7038 packages/core/src/database/schema.gen.ts OK 2dabfb2d6f..1fbd6285d0 packages/core/src/session.ts OK f714633295..92a934c44f packages/core/src/session/compaction.ts OK 65b17a86a4..880d42bcf9 packages/core/src/session/context-epoch.ts OK d874c40832..0bb4f02b2c packages/core/src/session/execution/local.ts OK 14b613678d..61f6a9a465 packages/core/src/session/input.ts OK 46118a89fe..e2d7e6b38e packages/core/src/session/message-updater.ts OK 792067017d..2742cabc24 packages/core/src/session/projector.ts OK 0000000000..e289b821c7 packages/core/src/session/recovery.ts OK 2f89aff9e3..ab4b833374 packages/core/src/session/run-coordinator.ts OK ...
<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, notInArray } 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 = ...
<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. ...
<path>/home/byk/Code/opencode/packages/core/src/session/runner/llm.ts</path>
<type>file</type>
<content>
100: const events = yield* EventV2.Service
101: const llm = yield* LLMClient.Service
102: const agents = yield* AgentV2.Service
103: const tools = yield* ToolRegistry.Service
104: const models = yield* SessionRunnerModel.Service
105: const store = yield* SessionStore.Service
106: const location = yield* Location.Service
107: const systemContext = yield* SystemContextRegistry.Service
108: const skillGuidance = yield* SkillGuidance.Service
109: const referenceGuidance = yield* ReferenceGuidance.Service
110: const config = yield* Config.Service
111: const snapshots = yield* Snapshot.Service
112: const db = (yield* Database.Service).db
113: const compaction = SessionCompaction.make({
114: events,
115: llm,
116: config: yield* config.entries(),
117: })
118: const getSession = ...
postimages_match=True
<path>/home/byk/Code/opencode/packages/core/src/session/runner/publish-llm-event.ts</path>
<type>file</type>
<content>
1: import { ToolOutput, type LLMEvent, type ProviderMetadata, type ToolResultValue, type Usage } from "@opencode-ai/llm"
2: import { DateTime, Effect } from "effect"
3: import { EventV2 } from "../../event"
4: import { ModelV2 } from "../../model"
5: import { SessionEvent } from "../event"
6: import { SessionMessage } from "../message"
7: import { SessionSchema } from "../schema"
8:
9: type Input = {
10: readonly sessionID: SessionSchema.ID
11: readonly agent: string
12: readonly model: ModelV2.Ref
13: readonly snapshot?: string
14: readonly startCommit: (assistantMessageID: SessionMessage.ID, seq: number) => Effect.Effect<void>
15: readonly eventCommit: () => Effect.Effect<void>
16: }
17:
18: const safe = (value: number | undefined) => Math.max(0, Number.isFinite(value) ? (value ?? ...
No files found
/home/byk/Code/opencode/packages/core/src/event.ts
Found 23 matches
/home/byk/Code/opencode/packages/core/src/event.ts:
Line 122: /** Local operational projection committed atomically with a new durable event. Not replayed or serialized. */
Line 123: readonly commit?: (seq: number) => Effect.Effect<void>
Line 205: function commitDurableEvent(
Line 214: commit?: (seq: number) => Effect.Effect<void>,
Line 239: const committed = yield* db
Line 316: const committed = {
Line 321: yield* projector(committed)
Line 323: if (commit) yield* commit(seq)
Line 354: if (committed) {
Line 356: pubsub.durable.get(committed.aggregateID) ?? [],
Line 361: return committed
Line 369: function publishEvent<D extends Definition>(definition: D, event: Payload<D>, commit?: PublishOptions["commit"]) {
Line 371: if (!definition?.durable ...
<path>/home/byk/Code/opencode/packages/core/src/event.ts</path>
<type>file</type>
<content>
190: return created
191: })
192:
193: yield* Effect.addFinalizer(() =>
194: Effect.gen(function* () {
195: yield* PubSub.shutdown(pubsub.all)
196: yield* Effect.forEach(
197: pubsub.durable.values(),
198: (pubsubs) => Effect.forEach(pubsubs, PubSub.shutdown, { discard: true }),
199: { discard: true },
200: )
201: yield* Effect.forEach(pubsub.typed.values(), PubSub.shutdown, { discard: true })
202: }),
203: )
204:
205: function commitDurableEvent(
206: definition: Definition,
207: event: Payload,
208: input?: {
209: readonly seq: number
210: readonly aggregateID: string
211: readonly ownerID?: string
212: readonly strictOwner?: boolean
213: },
214: commit?: (seq: number) => ...
<path>/home/byk/Code/opencode/packages/core/test/session-runner.test.ts</path>
<type>file</type>
<content>
3880: {
3881: type: "tool",
3882: id: "call-before-interrupt",
3883: state: {
3884: status: "error",
3885: error: { type: "unknown", message: "Tool execution outcome unknown after interruption" },
3886: },
3887: },
3888: ],
3889: },
3890: ])
3891:
3892: yield* replaySessionProjection(sessionID)
3893:
3894: expect(yield* session.context(sessionID)).toMatchObject([
3895: { type: "user", text: "Interrupt blocked tool" },
3896: { type: "assistant", content: [{ type: "tool", id: "call-before-interrupt", state: { status: "error" } }] },
3897: ])
3898: requests.length = 0
3899: responseStream = undefined
3900: response = []
3901: yield* session.resume(sessionID)
3902: ...
<path>/home/byk/Code/opencode/packages/core/src/session/sql.ts</path>
<type>file</type>
<content>
145: ],
146: )
147:
148: export const SessionInputTable = sqliteTable(
149: "session_input",
150: {
151: id: text().$type<SessionMessage.ID>().primaryKey(),
152: session_id: text()
153: .$type<SessionSchema.ID>()
154: .notNull()
155: .references(() => SessionTable.id, { onDelete: "cascade" }),
156: prompt: text({ mode: "json" }).notNull().$type<Prompt>(),
157: delivery: text().$type<SessionInput.Delivery>().notNull(),
158: admitted_seq: integer().notNull(),
159: promoted_seq: integer(),
160: wake_pending: integer({ mode: "boolean" }).notNull().default(false),
161: time_created: integer()
162: .notNull()
163: .$default(() => Date.now()),
164: },
165: (table) => [
166: index("session_input_session_pending_delivery_seq_idx").on(
167: table.session_id,
168: table.promoted_seq,
169: ...
<path>/home/byk/Code/opencode/packages/core/src/session/recovery.ts</path>
<type>file</type>
<content>
1: export * as SessionRecovery from "./recovery"
2:
3: import { and, asc, desc, eq, exists, gt, isNull, lte, or, sql } from "drizzle-orm"
4: import { Clock, DateTime, Effect, Layer, Option, Schedule, Schema } from "effect"
5: import { Database } from "../database/database"
6: import { makeGlobalNode } from "../effect/app-node"
7: import { EventV2 } from "../event"
8: import { SessionEvent } from "./event"
9: import { SessionExecution } from "./execution"
10: import { SessionProjector } from "./projector"
11: import { SessionExecutionClaim } from "./execution/claim"
12: import { SessionInput } from "./input"
13: import { continuation } from "./recovery-id"
14: import { SessionSchema } from "./schema"
15: import {
16: MAX_WAKE_ATTEMPTS,
17: SESSION_EXECUTION_LEASE_MS,
18: SessionExecutionTable,
19: SessionInputTable,
20: SessionRecoveryTable,
21: SessionMessageTable,
22: ...
<path>/home/byk/Code/opencode/packages/core/src/session/input.ts</path>
<type>file</type>
<content>
45:
46: export class RecoveryConflict extends Schema.TaggedErrorClass<RecoveryConflict>()("SessionInput.RecoveryConflict", {
47: executionID: Schema.String,
48: reason: Schema.String,
49: }) {}
50:
51: export const admit = Effect.fn("SessionInput.admit")(function* (
52: db: DatabaseService,
53: events: EventV2.Interface,
54: input: {
55: readonly id: SessionMessage.ID
56: readonly sessionID: SessionSchema.ID
57: readonly prompt: Prompt
58: readonly delivery: Delivery
59: readonly requestExecution: boolean
60: },
61: ) {
62: const existing = yield* find(db, input.id)
63: if (existing !== undefined) {
64: if (yield* isRecoveryInput(db, input.id)) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
65: if (!equivalent(existing, input)) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
66: const shouldWake = ...
<path>/home/byk/Code/opencode/packages/core/src/session/input.ts</path>
<type>file</type>
<content>
1: export * as SessionInput from "./input"
2:
3: import { and, asc, desc, eq, isNull, lte } from "drizzle-orm"
4: import { DateTime, Effect, Option, Schema } from "effect"
5: import { Admitted, Delivery } from "@opencode-ai/schema/session-input"
6: import type { Database } from "../database/database"
7: import { EventV2 } from "../event"
8: import { SessionEvent } from "./event"
9: import { SessionMessage } from "./message"
10: import { Prompt } from "./prompt"
11: import { continuation, message } from "./recovery-id"
12: import { SessionSchema } from "./schema"
13: import { SessionInputTable, SessionMessageTable, SessionRecoveryTable, SessionTable } from "./sql"
14:
15: type DatabaseService = Database.Interface["db"]
16:
17: export { Admitted, Delivery }
18:
19: const decodePrompt = Schema.decodeUnknownSync(Prompt)
20: const encodePrompt = Schema.encodeSync(Prompt)
21: const ...
<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: ...
0eb85245c71fb55fa2a8e169a1cd1de5579059d77aaca30b6ac254482e255a2e /tmp/opencode/session-recovery-final-v17.patch 308e9e562a3473b66bac430166db9d6cdd29f5f071d5695c1c35f28b99f26bf4 /home/byk/Code/opencode/packages/opencode/dist/server/opencode-server commit reverse_check=0
Durable Step.Failed can still be followed by duplicate Step.Interrupted.
failAssistant sets assistantFailed only after events.publish(...) returns (packages/core/src/session/runner/publish-llm-event.ts:222-240). Event publication commits the durable row inside the uninterruptible transaction, then performs interruptible notification before returning (packages/core/src/event.ts:237-363, packages/core/src/event.ts:379-390). Cancellation can interrupt during that notification window. The durable Step.Failed exists, but assistantFailed remains false. The cancellation path then publishes Step.Interrupted because it trusts that flag (packages/core/src/session/runner/llm.ts:429-443).
The regression polls for the committed event and then interrupts, but it does not hold publication between commit and return, so it does not deterministically exercise this window (packages/core/test/session-runner.test.ts:3965-3976). Add a gated listener or equivalent commit/return boundary regression. Terminal-state selection must use state established at the durable commit boundary.
ab9408c81cc9916177641ae8e9f08624fc27b5b9 exists as a commit.0eb85245c71fb55fa2a8e169a1cd1de5579059d77aaca30b6ac254482e255a2e.308e9e562a3473b66bac430166db9d6cdd29f5f071d5695c1c35f28b99f26bf4.packages/core/src/session/execution/claim.ts:27-109).packages/core/src/session/run-coordinator.ts:99-125, packages/core/src/session/execution/local.ts:128-162). ...