Dashboard › opencode › Session 0YQ7KI0CShVN
0YQ7KI0CShVNGLEl5Perform an independent, adversarial, strictly READ-ONLY security/integrity review of the frozen V2 interrupted-session recovery candidate in /home/byk/Code/opencode.
Immutable evidence:
ab9408c81cc9916177641ae8e9f08624fc27b5b9/tmp/opencode/session-recovery-final.patchb670599f8dbb4d4e14a74691a286622077c87e4d5c96a9acd3e2843c2c01072c/home/byk/Code/opencode/packages/opencode/dist/server/opencode-server8e567b2cce03c95263df6fd06d80de6e143858d521e70712f5dc97bbfeb3355bReview all changed files for authorization, cross-Session confusion, event forgery/replay, lease theft/revival, stale-owner side effects, duplicate execution, unbounded/poison denial of service, migration integrity, malformed durable state, advisory wake loss, cancellation races, and public schema/client drift. Core constraints:
continue.b670599f8dbb4d4e14a74691a286622077c87e4d5c96a9acd3e2843c2c01072c /tmp/opencode/session-recovery-final.patch 8e567b2cce03c95263df6fd06d80de6e143858d521e70712f5dc97bbfeb3355b /home/byk/Code/opencode/packages/opencode/dist/server/opencode-server commit tree d3323b963b9ef2de41a7ff9884299c74b93886ba parent 2e9407768d1401a25b69febb36657638a779053a author Burak Yigit Kaya <byk@sentry.io> 1789377773 +0000 committer Burak Yigit Kaya <byk@sentry.io> 1789377773 +0000 gpgsig -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgzS4Rh7Rq5E/xya3qyMl1mZhhQT vwxhC5O7kPj3XBlMAAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQL/V55k/T4w/ys8SGVUksyBN9XRyAWcR7Moc+vbsSfnrdJr3JzEp7BCK5VWs5q+W66 n4VtFbVF9lOvyD/Wt3vwg= -----END SSH SIGNATURE----- feat(app): adapt vertical tab density
<path>/tmp/opencode/session-recovery-final.patch</path>
<type>file</type>
<content>
1: diff --git a/packages/core/schema.json b/packages/core/schema.json
2: index d0eeeebd5c..dad07b6ce6 100644
3: --- a/packages/core/schema.json
4: +++ b/packages/core/schema.json
5: @@ -1,8 +1,8 @@
6: {
7: "version": "7",
8: "dialect": "sqlite",
9: - "id": "f14a9b18-8207-487e-a3d3-227e629ba9ad",
10: - "prevIds": ["169a0f0f-d58f-479f-b024-fa1c7b9a09db"],
11: + "id": "797e6838-8a41-4793-932a-d200aa8d51fb",
12: + "prevIds": ["0445bfe8-162e-4719-a49b-23f1210dcacd"],
13: "ddl": [
14: {
15: "name": "workspace",
16: @@ -60,6 +60,10 @@
17: "name": "session_context_epoch",
18: "entityType": "tables"
19: },
20: + {
21: + "name": "session_execution",
22: + "entityType": "tables"
23: + },
24: {
25: "name": "session_input",
26: "entityType": "tables"
27: @@ -68,6 +72,10 @@
28: "name": "session_message",
29: "entityType": ...
<path>/tmp/opencode/session-recovery-final.patch</path>
<type>file</type>
<content>
1387: + timestamp: DateTime.makeUnsafe(row.time_created),
1388: + messageID: id,
1389: + prompt: decodePrompt(row.prompt),
1390: + delivery: row.delivery,
1391: + },
1392: + commit ? { commit: () => commit(row) } : undefined,
1393: + )
1394: .pipe(
1395: Effect.catchDefect((defect) =>
1396: defect instanceof LifecycleConflict
1397: @@ -247,6 +386,7 @@ export const promoteSteers = Effect.fn("SessionInput.promoteSteers")(function* (
1398: events: EventV2.Interface,
1399: sessionID: SessionSchema.ID,
1400: cutoff: number,
1401: + commit?: (row: typeof SessionInputTable.$inferSelect) => Effect.Effect<void>,
1402: ) {
1403: const rows = yield* db
1404: .select()
1405: @@ -262,13 +402,14 @@ export const promoteSteers = Effect.fn("SessionInput.promoteSteers")(function* (
1406: ...
<path>/tmp/opencode/session-recovery-final.patch</path>
<type>file</type>
<content>
2524: export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) => {
2525: + const publishEvent = <D extends EventV2.Definition>(
2526: + definition: D,
2527: + data: EventV2.Data<D>,
2528: + commit: () => Effect.Effect<void> = input.eventCommit,
2529: + ) =>
2530: + definition.durable !== undefined
2531: + ? events.publish(definition, data, { commit })
2532: + : commit().pipe(Effect.andThen(events.publish(definition, data)))
2533: const tools = new Map<
2534: string,
2535: {
2536: @@ -73,15 +85,24 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
2537:
2538: const startAssistant = Effect.fnUntraced(function* () {
2539: if (assistantMessageID !== undefined) return assistantMessageID
2540: - assistantMessageID = SessionMessage.ID.create()
2541: + const created = SessionMessage.ID.create()
2542: ...
Found 32 matches /tmp/opencode/session-recovery-final.patch: Line 1: diff --git a/packages/core/schema.json b/packages/core/schema.json Line 410: diff --git a/packages/core/src/database/migration.gen.ts b/packages/core/src/database/migration.gen.ts Line 421: diff --git a/packages/core/src/database/migration/20260914170650_session-recovery.ts b/packages/core/src/database/migration/20260914170650_session-recovery.ts Line 477: diff --git a/packages/core/src/database/schema.gen.ts b/packages/core/src/database/schema.gen.ts Line 563: diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts Line 595: diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts Line 684: diff --git a/packages/core/src/session/context-epoch.ts b/packages/core/src/session/context-epoch.ts Line 860: diff --git a/packages/core/src/session/execution/claim.ts b/packages/core/src/session/execution/claim.ts Line 932: diff --git ...
<path>/tmp/opencode/session-recovery-final.patch</path>
<type>file</type>
<content>
3806: + const assistantID = SessionMessage.ID.make("msg_recovery_other")
3807: + yield* db
3808: + .insert(ProjectTable)
3809: + .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
3810: + .run()
3811: + yield* db
3812: + .insert(SessionTable)
3813: + .values(
3814: + [sessionID, other].map((id) => ({
3815: + id,
3816: + project_id: Project.ID.global,
3817: + slug: id,
3818: + directory: "/project",
3819: + title: "test",
3820: + version: "test",
3821: + })),
3822: + )
3823: + .run()
3824: + yield* events.publish(SessionEvent.Step.Started, {
3825: + sessionID: other,
3826: + timestamp: created,
3827: + assistantMessageID: assistantID,
3828: + agent: "build",
3829: + model,
3830: ...
<path>/tmp/opencode/session-recovery-final.patch</path>
<type>file</type>
<content>
5190: + })
5191: + yield* session.resume(sessionID)
5192: + yield* session.prompt({
5193: + sessionID,
5194: + prompt: Prompt.make({ text: "Recent exact request ".repeat(180) }),
5195: + resume: false,
5196: + })
5197: + currentModel = compactModel
5198: + requests.length = 0
5199: + responses = [
5200: + fragmentFixture("text", "text-recovery-summary", ["## Objective\n- Preserve recovery"]).completeEvents,
5201: + ]
5202: + modelResolveHook = Effect.gen(function* () {
5203: + const execution = yield* db
5204: + .select({ phase: SessionExecutionTable.phase })
5205: + .from(SessionExecutionTable)
5206: + .where(eq(SessionExecutionTable.session_id, sessionID))
5207: + .get()
5208: + .pipe(Effect.orDie)
5209: + if (execution?.phase === "continue") return yield* ...
<path>/tmp/opencode/session-recovery-final.patch</path>
<type>file</type>
<content>
6734: "enum": ["steer", "queue"]
6735: + },
6736: + "requestExecution": {
6737: + "type": "boolean"
6738: }
6739: },
6740: "required": ["timestamp", "sessionID", "messageID", "prompt", "delivery"],
6741: @@ -34315,6 +34819,39 @@
6742: "required": ["id", "type", "properties"],
6743: "additionalProperties": false
6744: },
6745: + "EventSessionNextPromptExecution_requested": {
6746: + "type": "object",
6747: + "properties": {
6748: + "id": {
6749: + "type": "string",
6750: + "pattern": "^evt_"
6751: + },
6752: + "type": {
6753: + "type": "string",
6754: + "enum": ["session.next.prompt.execution_requested"]
6755: + },
6756: + "properties": {
6757: + "type": ...
ab9408c81cc9916177641ae8e9f08624fc27b5b9:packages/core/src/session/runner/publish-llm-event.ts:239: const publish = Effect.fn("SessionRunner.publishLLMEvent")(function* (
packages/core/src/database/database.ts packages/core/src/database/migration/20260323234822_events.ts packages/core/src/database/migration/20260604172448_event_sourced_session_input.ts packages/core/src/event/sql.ts packages/core/src/session/event.ts packages/core/src/session/execution.ts packages/core/src/session/execution/local.ts packages/core/src/session/input.ts packages/core/src/session/projector.ts packages/core/src/session/run-coordinator.ts packages/core/src/session/runner/publish-llm-event.ts packages/core/src/session/sql.ts
packages/core/src/event.ts packages/core/src/event/sql.ts packages/core/src/session/event.ts
export * as EventV2 from "./event"
import { Cause, Context, Effect, Layer, Option, PubSub, Queue, Schema, Stream } from "effect"
import { Event } from "@opencode-ai/schema/event"
import type { Data, Definition, Payload } from "@opencode-ai/schema/event"
import { and, asc, eq, gt, inArray } from "drizzle-orm"
import { Database } from "./database/database"
import { EventSequenceTable, EventTable } from "./event/sql"
import { Location } from "./location"
import { makeGlobalNode } from "./effect/app-node"
import { isDeepStrictEqual } from "node:util"
import { Durable } from "@opencode-ai/schema/durable-event-manifest"
export const ID = Event.ID
export type ID = import("@opencode-ai/schema/event").ID
export type { Data, Definition, Payload } from "@opencode-ai/schema/event"
export type Subscriber<D extends Definition = Definition> = (event: Payload<D>) => Effect.Effect<void>
export type Unsubscribe = Effect.Effect<void>
export const latestSequence = ...
(no output)
export * as SessionInput from "./input"
import { and, asc, eq, isNull, lte } from "drizzle-orm"
import { DateTime, Effect, Schema } from "effect"
import { Admitted, Delivery } from "@opencode-ai/schema/session-input"
import type { Database } from "../database/database"
import type { EventV2 } from "../event"
import { SessionEvent } from "./event"
import { SessionMessage } from "./message"
import { Prompt } from "./prompt"
import { SessionSchema } from "./schema"
import { SessionInputTable, SessionMessageTable } from "./sql"
type DatabaseService = Database.Interface["db"]
export { Admitted, Delivery }
const decodePrompt = Schema.decodeUnknownSync(Prompt)
const encodePrompt = Schema.encodeSync(Prompt)
const fromRow = (row: typeof SessionInputTable.$inferSelect): Admitted =>
Admitted.make({
admittedSeq: row.admitted_seq,
id: SessionMessage.ID.make(row.id),
sessionID: SessionSchema.ID.make(row.session_id),
prompt: decodePrompt(row.prompt),
delivery: ...
133- 133 executed: tool.provider?.executed === true,
134- 134 ...(tool.provider?.metadata === undefined ? {} : { metadata: tool.provider.metadata }),
135- 135 },
136- 136 })
137- 137 }
138- 138 }
139- 139 })
140- 140
141- 141 const awaitToolFibers = (fibers: FiberSet.FiberSet<void, ToolOutputStore.Error>) =>
142- 142 Effect.raceFirst(FiberSet.join(fibers), FiberSet.awaitEmpty(fibers))
143- 143
144- 144 // Match V1: declining a user prompt halts the loop instead of becoming model-facing tool output.
145- 145 const isUserDeclined = (cause: Cause.Cause<unknown>) =>
146- 146 cause.reasons.some(
147- 147 (reason) =>
148- 148 Cause.isDieReason(reason) &&
149- 149 (reason.defect instanceof PermissionV2.DeclinedError || reason.defect instanceof QuestionV2.RejectedError),
150- 150 )
151- 151
152- 152 type TurnTransition ...
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 and waits for its cleanup. */
14 readonly interrupt: (key: Key) => Effect.Effect<void>
15 }
16
17 type Entry<E> = {
18 readonly done: Deferred.Deferred<void, E>
19 owner?: Fiber.Fiber<void, never>
20 pendingWake: ...
This is your one evidence-or-blocker retry. Do NOT use lore/recall. Inspect only the immutable patch /tmp/opencode/session-recovery-final.patch, base Git object ab9408c81cc9916177641ae8e9f08624fc27b5b9, and artifact /home/byk/Code/opencode/packages/opencode/dist/server/opencode-server. Reverify required hashes: patch b670599f8dbb4d4e14a74691a286622077c87e4d5c96a9acd3e2843c2c01072c, artifact 8e567b2cce03c95263df6fd06d80de6e143858d521e70712f5dc97bbfeb3355b. Never inspect mutable worktree files as source of truth. Read-only: no edits/tests/build/commit/services/VCS mutation.
Complete the previously requested adversarial security/integrity review across all 32 patch files. Return substantive severity-ordered MUST-FIX/CONCERN findings with frozen HEAD file:line evidence and concrete sequence, or explicit PASS with residual risks and coverage gaps. Assess cross-Session identity, durable event forgery/replay, lease theft/revival and stale-owner boundaries, duplicate external calls, cancellation, wake/outbox loss, poison/batch DoS, historical PromptAdmitted compatibility and promoted retry wake safety, migration integrity, generated API drift, forbidden SessionPrompt/Core->Server coupling, accepted instruction-level external-dispatch race, and bounded clock-skew assumption. If blocked, quote exact command/tool error and affected scope. Never return empty or progress-only. End exactly MERGE or DO-NOT-MERGE.
73- 73 })
74- 74 },
75- 75 }
76- 76 }
77- 77
78- 78 export function update(adapter: Adapter, event: SessionEvent.Event) {
79- 79 type DraftAssistant = WritableDraft<SessionMessage.Assistant>
80- 80 type DraftTool = WritableDraft<SessionMessage.AssistantTool>
81- 81 type DraftText = WritableDraft<SessionMessage.AssistantText>
82- 82 type DraftReasoning = WritableDraft<SessionMessage.AssistantReasoning>
83- 83
84- 84 const latestTool = (assistant: DraftAssistant | undefined, callID?: string) =>
85- 85 assistant?.content.findLast(
86- 86 (item): item is DraftTool => item.type === "tool" && (callID === undefined || item.id === callID),
87- 87 )
88- 88
89- 89 const latestText = (assistant: DraftAssistant | undefined, textID: string) =>
90- 90 assistant?.content.findLast((item): item is DraftText => item.type === "text" && item.id === textID)
91- 91
92- 92 const latestReasoning = ...
Date: Sep 15, 2026 * 🔴 (22:56) User provided a code patch extending session recovery/event handling with `PromptAdmitted`, `SessionNextToolInputStarted`, `SyncEventSessionNextStepRecovered`, `SyncEventSessionNextToolCalled`, and the event type literal `"session.next.prompt.execution_requested"`. * 🔴 (22:56) User’s patch updated recovery-related test setup to assign `currentModel = recoveryModel…
Date: Sep 15, 2026 * 🔴 (22:56) User specified the ownership-fencing invariant “never advances the Context Epoch after ownership changes” in `packages/core/test/session-projector.test.ts`; the test expects `SessionContextEpochTable` to remain empty and the event count to remain unchanged after ownership changes. * 🔴 (22:56) User specified the retry invariant “never wakes execution when retrying …
Date: Sep 15, 2026 * 🟡 (22:55) `/tmp/opencode/session-recovery-final.patch` contains 32 diff headers: line 1 `packages/core/schema.json`; line 410 `packages/core/src/database/migration.gen.ts`; line 421 `packages/core/src/database/migration/20260914170650_session-recovery.ts`; line 477 `packages/core/src/database/schema.gen.ts`; line 563 `packages/core/src/session.ts`; line 595 `packages/core/sr…
Date: Sep 15, 2026 * 🔴 (22:54) User established the session-execution invariant “never starts a second owner while the lease is live,” covered by an `it.effect(...)` test in the `SessionExecutionLocal` suite. * 🔴 (22:54) User established the session-execution invariant “never overwrites an expired lease,” covered by an `it.effect(...)` test in the `SessionExecutionLocal` suite. * 🔴 (22:54) Use…
Date: Sep 15, 2026 * 🟡 (22:54) User supplied additional session-recovery code adding `const Phase = Schema.Literals(["ready", "safe", "unknown", "continue"])`, time-based claim handling via `Clock.currentTimeMillis`, and claim validation involving `claim.assistant_message_id` and the latest message ID. * 🟡 (22:54) The diff adds a new 305-line recovery implementation using `SessionRecoveryTable`…
Date: Sep 15, 2026 * 🔴 (22:54) User stated that an expired claim never renews itself. * 🟡 (22:54) User provided a code diff involving session infrastructure, including `session_message`, `session_execution_session_idx`, `SessionContextEpochTable`, `SessionInputTable`, `SessionRecoveryTable.wake_pending`, `execution.wake(admission.admitted.sessionID)`, and an optional `input.beforeStream()` hook.