Dashboard › opencode › Session 0PHj7sMqYEP2
0PHj7sMqYEP2myv7nPerform a final independent security/pentest review of the exact immutable Session V2 recovery candidate. READ ONLY: never modify repository files, refs, index, services, builds, tests, or artifacts. Base commit: ab9408c81cc9916177641ae8e9f08624fc27b5b9. Patch: /tmp/opencode/session-recovery-final-v6.patch. Expected SHA-256: a5c1bace440d1b6acb383c4fcffc52714a9373892e75b4f23bdbbf2dfdad8e16. SEA: /home/byk/Code/opencode/packages/opencode/dist/server/opencode-server, expected SHA-256 d09c992a0f4cce15e73dd158fe60413420109c821afed6e121d11cdad9c7b583. Verify hashes, clean patch application to immutable base, ancestry, exact 33-file list, and inspect every changed hunk from immutable objects/patch only, not mutable worktree state.
Threat-model hostile/malformed durable rows, concurrent nodes, stale owners, lease expiry/skew, cancellation/finalizer stalls, provider/tool crash windows, event forgery/replay, cross-Session IDs, retry ID reuse, wake amplification/starvation, recovery input leakage, migration tampering, and dependency-boundary violations. The core security invariant is fail closed: never replay an uncertain provider request or old local tool call; never let two owners execute the same Session; never surrender ownership while an old ownership chain/finalizer remains active; never discard unknown tool/provider state as safe.
Prioritize the latest blockers: verify Step.Interrupted projection only renews/fences and never clears claim before SessionExecutionLocal coordinator cleanup/finalizers. Verify cancellation heartbeat retains exact claim throughout cleanup, cannot revive expired ownership, and cannot cancel/short-circuit cleanup on heartbeat loss. Verify exact claim clear after cleanup rejects replacement rows. Verify Step.Failed/provider error never marks safe until all local fibers and unresolved hosted/local tools have durable terminal outcomes; reconciliation failure after Step.Failed leaves unknown; defects after StepFinished never publish Step.Ended or ...
I will review only the supplied patch, immutable Git objects, and exact SEA bytes. The required repo-setup skill is not available in this session, so I will follow the repository’s checked-in guidance directly; I will not touch the worktree, index, refs, services, tests, builds, or candidate artifacts.
<skill_content name="jj-guide"> # Skill: jj-guide # jj Guide for AI Agents Jujutsu (jj) is a Git-compatible VCS with mutable commits, automatic snapshotting, no staging area, and first-class conflicts. This skill teaches you how to use it safely from a non-interactive agent environment. If `.jj/` exists in the repo root, this is a jj repo. **Use `jj` commands, not `git`.** In a colocated repo (`.jj/` *and* `.git/`), git tools can read the state, but mutations should go through `jj` so the operation log stays consistent. ## Critical Rules - **NEVER** use interactive flags (`-i`, `--interactive`). TUI prompts hang in agent environments. This applies to `jj split -i`, `jj squash -i`, `jj commit -i`, `jj resolve`, `jj diffedit`, etc. - **ALWAYS** pass `-m "msg"` when describing/committing. Without `-m`, an editor opens and hangs. - **VERIFY** mutations with `jj st` and `jj log` after `squash`, `abandon`, `rebase`, `restore`, `commit`. ...
a5c1bace440d1b6acb383c4fcffc52714a9373892e75b4f23bdbbf2dfdad8e16 /tmp/opencode/session-recovery-final-v6.patch d09c992a0f4cce15e73dd158fe60413420109c821afed6e121d11cdad9c7b583 /home/byk/Code/opencode/packages/opencode/dist/server/opencode-server commit ab9408c81cc9916177641ae8e9f08624fc27b5b9 2e9407768d1401a25b69febb36657638a779053a d3323b963b9ef2de41a7ff9884299c74b93886ba 1789377773 feat(app): adapt vertical tab density ancestry_exit=0
Found 33 matches /tmp/opencode/session-recovery-final-v6.patch: Line 1: diff --git a/packages/core/schema.json b/packages/core/schema.json Line 866: diff --git a/packages/core/src/database/migration.gen.ts b/packages/core/src/database/migration.gen.ts Line 877: diff --git a/packages/core/src/database/migration/20260914170650_session-recovery.ts b/packages/core/src/database/migration/20260914170650_session-recovery.ts Line 938: diff --git a/packages/core/src/database/schema.gen.ts b/packages/core/src/database/schema.gen.ts Line 1036: diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts Line 1068: diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts Line 1157: diff --git a/packages/core/src/session/context-epoch.ts b/packages/core/src/session/context-epoch.ts Line 1333: diff --git a/packages/core/src/session/execution/local.ts b/packages/core/src/session/execution/local.ts Line 1519: diff --git ...
Found 100 matches (more matches available) /tmp/opencode/session-recovery-final-v6.patch: Line 2: index d0eeeebd5c..dcc34e4652 100644 Line 3: --- a/packages/core/schema.json Line 4: +++ b/packages/core/schema.json Line 867: index e6ea4eaa14..77948f1529 100644 Line 868: --- a/packages/core/src/database/migration.gen.ts Line 869: +++ b/packages/core/src/database/migration.gen.ts Line 878: new file mode 100644 Line 879: index 0000000000..b7a60dec0c Line 880: --- /dev/null Line 881: +++ b/packages/core/src/database/migration/20260914170650_session-recovery.ts Line 939: index ed60fde6c5..c6dc22c630 100644 Line 940: --- a/packages/core/src/database/schema.gen.ts Line 941: +++ b/packages/core/src/database/schema.gen.ts Line 1037: index 2dabfb2d6f..fc5868837a 100644 Line 1038: --- a/packages/core/src/session.ts Line 1039: +++ b/packages/core/src/session.ts Line 1069: index f714633295..92a934c44f 100644 Line 1070: --- ...
<path>/home/byk/Code/opencode/CONTRIBUTING.md</path> <type>file</type> <content> 1: # Contributing to OpenCode 2: 3: We want to make it easy for you to contribute to OpenCode. Here are the most common type of changes that get merged: 4: 5: - Bug fixes 6: - Additional LSPs / Formatters 7: - Improvements to LLM performance 8: - Support for new providers 9: - Fixes for environment-specific quirks 10: - Missing standard behavior 11: - Documentation improvements 12: 13: However, any UI or core product feature must go through a design review with the core team before implementation. 14: 15: If you are unsure if a PR would be accepted, feel free to ask a maintainer or look for issues with any of the following labels: 16: 17: - [`help wanted`](https://github.com/anomalyco/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Ahelp-wanted) 18: - [`good first issue`](https://github.com/anomalyco/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22) 19: - ...
<path>/home/byk/Code/opencode/packages/core/package.json</path>
<type>file</type>
<content>
1: {
2: "$schema": "https://json.schemastore.org/package.json",
3: "version": "1.18.30",
4: "name": "@opencode-ai/core",
5: "type": "module",
6: "license": "MIT",
7: "private": true,
8: "scripts": {
9: "db": "bun drizzle-kit",
10: "migration": "bun run script/migration.ts",
11: "fix-node-pty": "bun run script/fix-node-pty.ts",
12: "test": "bun test --only-failures",
13: "typecheck": "tsgo --noEmit"
14: },
15: "bin": {
16: "opencode": "./bin/opencode"
17: },
18: "exports": {
19: "./effect/layer-node": "./src/effect/layer-node.ts",
20: "./effect/app-node": "./src/effect/app-node.ts",
21: "./session/runner": "./src/session/runner/index.ts",
22: "./system-context": "./src/system-context/index.ts",
23: "./*": "./src/*.ts"
24: },
25: "imports": {
26: "#sqlite": {
27: "bun": "./src/database/sqlite.bun.ts",
28: ...
/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/opencode/test/server/AGENTS.md /home/byk/Code/opencode/packages/ui/AGENTS.md /home/byk/Code/opencode/packages/stats/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/effect-drizzle-sqlite/AGENTS.md /home/byk/Code/opencode/packages/llm/AGENTS.md /home/byk/Code/opencode/packages/core/src/tool/AGENTS.md /home/byk/Code/opencode/packages/app/AGENTS.md /home/byk/Code/opencode/packages/desktop/AGENTS.md /home/byk/Code/opencode/packages/app/e2e/performance/AGENTS.md /home/byk/Code/opencode/packages/app/e2e/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/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. ...
<skill_content name="effect">
# Skill: effect
# Effect
This codebase uses Effect for typed, composable TypeScript services, schemas, and workflows.
## Source Of Truth
Use the current Effect v4 / effect-smol source, not memory or older Effect v2/v3 examples.
1. If `.opencode/references/effect-smol` is missing, clone `https://github.com/Effect-TS/effect-smol` there. Do this in the project, not in the skill folder.
2. Search `.opencode/references/effect-smol` for exact APIs, examples, tests, and naming patterns before answering or implementing Effect-specific code.
3. Also inspect existing repo code for local house style before introducing new patterns.
4. Prefer answers and implementations backed by specific source files or nearby repo examples.
## Guidelines
- Prefer current Effect v4 APIs and project-local patterns over old blog posts, examples, or package-memory guesses.
- Use `Effect.gen(function* () { ... })` for multi-step workflows.
- Use `Effect.fn("Name")` or ...
Both supplied SHA-256 values match exactly, the base object exists, and that base is an ancestor of the repository’s current Git head. The patch declares exactly 33 paths; I am now validating every hunk against the base blobs and checking each reconstructed result against the patch’s destination blob ID without materializing a tree.
{
"files": 33,
"unique": 33,
"failures": []
}
<path>/tmp/opencode/session-recovery-final-v6.patch</path>
<type>file</type>
<content>
1333: diff --git a/packages/core/src/session/execution/local.ts b/packages/core/src/session/execution/local.ts
1334: index d874c40832..62040b8a95 100644
1335: --- a/packages/core/src/session/execution/local.ts
1336: +++ b/packages/core/src/session/execution/local.ts
1337: @@ -1,46 +1,162 @@
1338: -import { Cause, Effect, Layer } from "effect"
1339: +import { and, asc, eq, isNull } from "drizzle-orm"
1340: +import { Cause, Clock, Duration, Effect, Exit, Layer, Schedule } from "effect"
1341: +import { Database } from "../../database/database"
1342: import { LocationServiceMap } from "../../location-service-map"
1343: import { makeGlobalNode } from "../../effect/app-node"
1344: import { SessionRunCoordinator } from "../run-coordinator"
1345: import { SessionRunner } from "../runner"
1346: +import { SessionMessage } from "../message"
1347: import { SessionSchema } from "../schema"
1348: import { ...
<path>/tmp/opencode/session-recovery-final-v6.patch</path>
<type>file</type>
<content>
8690: @@ -29,6 +30,7 @@ const applicationServices = LayerNode.group([
8691: httpClient,
8692: ToolOutputStore.cleanupNode,
8693: SessionV2.node,
8694: + SessionRecovery.node,
8695: PermissionSaved.node,
8696: PtyTicket.node,
8697: Credential.node,
8698: diff --git a/packages/core/src/session/execution/claim.ts b/packages/core/src/session/execution/claim.ts
8699: new file mode 100644
8700: index 0000000000..5c458a549c
8701: --- /dev/null
8702: +++ b/packages/core/src/session/execution/claim.ts
8703: @@ -0,0 +1,66 @@
8704: +export * as SessionExecutionClaim from "./claim"
8705: +
8706: +import { and, eq, gt } from "drizzle-orm"
8707: +import { Clock, Effect } from "effect"
8708: +import type { Database } from "../../database/database"
8709: +import type { SessionSchema } from "../schema"
8710: +import { SESSION_EXECUTION_LEASE_MS, SessionExecutionTable } from "../sql"
8711: +
8712: ...
<path>/tmp/opencode/session-recovery-final-v6.patch</path>
<type>file</type>
<content>
2600: + name: "session-recovery",
2601: + layer,
2602: + deps: [Database.node, EventV2.node, SessionProjector.node, SessionExecution.node],
2603: +})
2604: diff --git a/packages/core/src/session/run-coordinator.ts b/packages/core/src/session/run-coordinator.ts
2605: index 2f89aff9e3..ab4b833374 100644
2606: --- a/packages/core/src/session/run-coordinator.ts
2607: +++ b/packages/core/src/session/run-coordinator.ts
2608: @@ -10,13 +10,14 @@ export interface Coordinator<Key, E> {
2609: readonly run: (key: Key) => Effect.Effect<void, E>
2610: /** Registers one coalesced follow-up after newly recorded work. */
2611: readonly wake: (key: Key) => Effect.Effect<void>
2612: - /** Stops active execution and waits for its cleanup. */
2613: - readonly interrupt: (key: Key) => Effect.Effect<void>
2614: + /** Stops active execution, runs cleanup, then starts work that arrived while stopping. ...
export * as SessionRunCoordinator from "./run-coordinator"
import { Deferred, Effect, Exit, Fiber, FiberSet, Scope } from "effect"
/** Serializes execution for each key while allowing different keys to run concurrently. */
export interface Coordinator<Key, E> {
/** Snapshots keys with an execution owned by this coordinator. */
readonly active: Effect.Effect<ReadonlySet<Key>>
/** Starts execution while idle or joins the active execution. */
readonly run: (key: Key) => Effect.Effect<void, E>
/** Registers one coalesced follow-up after newly recorded work. */
readonly wake: (key: Key) => Effect.Effect<void>
/** Stops active execution and waits for its cleanup. */
readonly interrupt: (key: Key) => Effect.Effect<void>
}
type Entry<E> = {
readonly done: Deferred.Deferred<void, E>
owner?: Fiber.Fiber<void, never>
pendingWake: boolean
stopping: boolean
}
export const make = <Key, E>(options: {
readonly drain: (key: Key, force: boolean) => Effect.Effect<void, ...
<path>/tmp/opencode/session-recovery-final-v6.patch</path>
<type>file</type>
<content>
877: diff --git a/packages/core/src/database/migration/20260914170650_session-recovery.ts b/packages/core/src/database/migration/20260914170650_session-recovery.ts
878: new file mode 100644
879: index 0000000000..b7a60dec0c
880: --- /dev/null
881: +++ b/packages/core/src/database/migration/20260914170650_session-recovery.ts
882: @@ -0,0 +1,55 @@
883: +import { Effect } from "effect"
884: +import type { DatabaseMigration } from "../migration"
885: +
886: +export default {
887: + id: "20260914170650_session-recovery",
888: + up(tx) {
889: + return Effect.gen(function* () {
890: + yield* tx.run(`ALTER TABLE \`session_input\` ADD \`wake_pending\` integer DEFAULT false NOT NULL;`)
891: + yield* tx.run(`ALTER TABLE \`session\` ADD \`wake_attempts\` integer DEFAULT 0 NOT NULL;`)
892: + yield* tx.run(`
893: + CREATE TABLE \`session_execution\` (
894: + \`id\` text NOT ...
<path>/tmp/opencode/session-recovery-final-v6.patch</path>
<type>file</type>
<content>
2267: diff --git a/packages/core/src/session/recovery.ts b/packages/core/src/session/recovery.ts
2268: new file mode 100644
2269: index 0000000000..879956b9f6
2270: --- /dev/null
2271: +++ b/packages/core/src/session/recovery.ts
2272: @@ -0,0 +1,331 @@
2273: +export * as SessionRecovery from "./recovery"
2274: +
2275: +import { and, asc, desc, eq, exists, gt, isNull, lte, or, sql } from "drizzle-orm"
2276: +import { Clock, DateTime, Effect, Layer, Option, Schedule, Schema } from "effect"
2277: +import { Database } from "../database/database"
2278: +import { makeGlobalNode } from "../effect/app-node"
2279: +import { EventV2 } from "../event"
2280: +import { SessionEvent } from "./event"
2281: +import { SessionExecution } from "./execution"
2282: +import { SessionProjector } from "./projector"
2283: +import { SessionExecutionClaim } from "./execution/claim"
2284: +import { SessionInput } from ...
<path>/tmp/opencode/session-recovery-final-v6.patch</path>
<type>file</type>
<content>
2159: diff --git a/packages/core/src/session/projector.ts b/packages/core/src/session/projector.ts
2160: index 792067017d..43a9d26b12 100644
2161: --- a/packages/core/src/session/projector.ts
2162: +++ b/packages/core/src/session/projector.ts
2163: @@ -11,8 +11,17 @@ import { WorkspaceTable } from "../control-plane/workspace.sql"
2164: import { SessionMessage } from "./message"
2165: import { SessionMessageUpdater } from "./message-updater"
2166: import { SessionInput } from "./input"
2167: +import { Prompt } from "./prompt"
2168: +import { continuation } from "./recovery-id"
2169: import { WorkspaceV2 } from "../workspace"
2170: -import { MessageTable, PartTable, SessionInputTable, SessionMessageTable, SessionTable } from "./sql"
2171: +import {
2172: + MessageTable,
2173: + PartTable,
2174: + SessionInputTable,
2175: + SessionMessageTable,
2176: + SessionRecoveryTable,
2177: + ...
<path>/tmp/opencode/session-recovery-final-v6.patch</path>
<type>file</type>
<content>
1036: diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts
1037: index 2dabfb2d6f..fc5868837a 100644
1038: --- a/packages/core/src/session.ts
1039: +++ b/packages/core/src/session.ts
1040: @@ -365,11 +365,12 @@ const layer = Layer.effect(
1041: const messageID = input.id ?? SessionMessage.ID.create()
1042: const delivery = input.delivery ?? "steer"
1043: const expected = { sessionID: input.sessionID, messageID, prompt, delivery }
1044: - const admitted = yield* SessionInput.admit(db, events, {
1045: + const admission = yield* SessionInput.admit(db, events, {
1046: id: messageID,
1047: sessionID: input.sessionID,
1048: prompt,
1049: delivery,
1050: + requestExecution: input.resume !== false,
1051: }).pipe(
1052: ...
<path>/tmp/opencode/session-recovery-final-v6.patch</path>
<type>file</type>
<content>
1519: diff --git a/packages/core/src/session/input.ts b/packages/core/src/session/input.ts
1520: index 14b613678d..0b38eb2519 100644
1521: --- a/packages/core/src/session/input.ts
1522: +++ b/packages/core/src/session/input.ts
1523: @@ -1,15 +1,16 @@
1524: export * as SessionInput from "./input"
1525:
1526: -import { and, asc, eq, isNull, lte } from "drizzle-orm"
1527: -import { DateTime, Effect, Schema } from "effect"
1528: +import { and, asc, desc, eq, isNull, lte } from "drizzle-orm"
1529: +import { DateTime, Effect, Option, Schema } from "effect"
1530: import { Admitted, Delivery } from "@opencode-ai/schema/session-input"
1531: import type { Database } from "../database/database"
1532: -import type { EventV2 } from "../event"
1533: +import { EventV2 } from "../event"
1534: import { SessionEvent } from "./event"
1535: import { SessionMessage } from "./message"
1536: import { Prompt } from ...
<path>/tmp/opencode/session-recovery-final-v6.patch</path>
<type>file</type>
<content>
2065: diff --git a/packages/core/src/session/message-updater.ts b/packages/core/src/session/message-updater.ts
2066: index 46118a89fe..e2d7e6b38e 100644
2067: --- a/packages/core/src/session/message-updater.ts
2068: +++ b/packages/core/src/session/message-updater.ts
2069: @@ -2,6 +2,7 @@ import { castDraft, produce, type WritableDraft } from "immer"
2070: import { Effect } from "effect"
2071: import { SessionEvent } from "./event"
2072: import { SessionMessage } from "./message"
2073: +import { message } from "./recovery-id"
2074:
2075: export type MemoryState = {
2076: messages: SessionMessage.Message[]
2077: @@ -137,6 +138,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
2078: )
2079: },
2080: "session.next.prompt.admitted": () => Effect.void,
2081: + "session.next.prompt.execution_requested": () => Effect.void,
2082: ...
<path>/tmp/opencode/session-recovery-final-v6.patch</path>
<type>file</type>
<content>
2695: diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts
2696: index 874086a06b..bfe344ec5c 100644
2697: --- a/packages/core/src/session/runner/llm.ts
2698: +++ b/packages/core/src/session/runner/llm.ts
2699: @@ -9,6 +9,7 @@ import {
2700: type ProviderErrorEvent,
2701: } from "@opencode-ai/llm"
2702: import { Cause, DateTime, Effect, FiberSet, Layer, Option, Semaphore, Stream } from "effect"
2703: +import { and, eq } from "drizzle-orm"
2704: import { AgentV2 } from "../../agent"
2705: import { Config } from "../../config"
2706: import { Database } from "../../database/database"
2707: @@ -29,8 +30,11 @@ import { SessionCompaction } from "../compaction"
2708: import { SessionEvent } from "../event"
2709: import { SessionHistory } from "../history"
2710: import { SessionInput } from "../input"
2711: +import { SessionExecutionClaim } from ...
<path>/tmp/opencode/session-recovery-final-v6.patch</path>
<type>file</type>
<content>
3206: diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts
3207: index 33652a618c..7b329d6722 100644
3208: --- a/packages/core/src/session/runner/publish-llm-event.ts
3209: +++ b/packages/core/src/session/runner/publish-llm-event.ts
3210: @@ -11,6 +11,8 @@ type Input = {
3211: readonly agent: string
3212: readonly model: ModelV2.Ref
3213: readonly snapshot?: string
3214: + readonly startCommit: (assistantMessageID: SessionMessage.ID) => Effect.Effect<void>
3215: + readonly eventCommit: () => Effect.Effect<void>
3216: }
3217:
3218: const safe = (value: number | undefined) => Math.max(0, Number.isFinite(value) ? (value ?? ...
<path>/tmp/opencode/session-recovery-final-v6.patch</path>
<type>file</type>
<content>
7223: diff --git a/packages/schema/src/session-event.ts b/packages/schema/src/session-event.ts
7224: index 3a559c3e38..2599bbb78f 100644
7225: --- a/packages/schema/src/session-event.ts
7226: +++ b/packages/schema/src/session-event.ts
7227: @@ -94,10 +94,23 @@ export type Prompted = typeof Prompted.Type
7228: export const PromptAdmitted = Event.define({
7229: type: "session.next.prompt.admitted",
7230: ...options,
7231: - schema: PromptFields,
7232: + schema: {
7233: + ...PromptFields,
7234: + requestExecution: Schema.Boolean.pipe(optional),
7235: + },
7236: })
7237: export type PromptAdmitted = typeof PromptAdmitted.Type
7238:
7239: +export const PromptExecutionRequested = Event.define({
7240: + type: "session.next.prompt.execution_requested",
7241: + ...options,
7242: + schema: {
7243: + ...Base,
7244: + messageID: SessionMessage.ID,
7245: + },
7246: +})
7247: +export ...
<path>/tmp/opencode/session-recovery-final-v6.patch</path>
<type>file</type>
<content>
3456: diff --git a/packages/core/src/session/sql.ts b/packages/core/src/session/sql.ts
3457: index 264a1d2cca..5ffe5b0b20 100644
3458: --- a/packages/core/src/session/sql.ts
3459: +++ b/packages/core/src/session/sql.ts
3460: @@ -1,4 +1,5 @@
3461: -import { sqliteTable, text, integer, index, primaryKey, real, uniqueIndex } from "drizzle-orm/sqlite-core"
3462: +import { sql } from "drizzle-orm"
3463: +import { check, sqliteTable, text, integer, index, primaryKey, real, uniqueIndex } from "drizzle-orm/sqlite-core"
3464: import * as DatabasePath from "../database/path"
3465: import { ProjectTable } from "../project/sql"
3466: import type { SessionMessage } from "./message"
3467: @@ -57,6 +58,7 @@ export const SessionTable = sqliteTable(
3468: ...Timestamps,
3469: time_compacting: integer(),
3470: time_archived: integer(),
3471: + wake_attempts: integer().notNull().default(0),
3472: ...
<path>/tmp/opencode/session-recovery-final-v6.patch</path>
<type>file</type>
<content>
3649: diff --git a/packages/core/test/session-execution-local.test.ts b/packages/core/test/session-execution-local.test.ts
3650: new file mode 100644
3651: index 0000000000..a34c4a7c6a
3652: --- /dev/null
3653: +++ b/packages/core/test/session-execution-local.test.ts
3654: @@ -0,0 +1,283 @@
3655: +import { describe, expect } from "bun:test"
3656: +import { Deferred, Effect, Exit, Fiber, Scope } from "effect"
3657: +import { TestClock } from "effect/testing"
3658: +import { eq } from "drizzle-orm"
3659: +import { Database } from "@opencode-ai/core/database/database"
3660: +import { Project } from "@opencode-ai/core/project"
3661: +import { ProjectTable } from "@opencode-ai/core/project/sql"
3662: +import { AbsolutePath } from "@opencode-ai/core/schema"
3663: +import { SessionExecutionLocal } from "@opencode-ai/core/session/execution/local"
3664: +import { SessionSchema } from ...
Found 59 matches
/tmp/opencode/session-recovery-final-v6.patch:
Line 3582: + test("adds final recovery ownership tables to a populated predecessor", async () => {
Line 3697: + it.effect("clears the claim after a successful drain", () =>
Line 3706: + it.effect("preserves the claim when its scope closes", () =>
Line 3722: + it.effect("clears the claim after explicit interruption", () =>
Line 3736: + it.effect("never starts a second owner while the lease is live", () =>
Line 3759: + it.effect("never overwrites an expired lease", () =>
Line 3786: + it.effect("never re-enters its own uncertain live lease", () =>
Line 3813: + it.effect("renews heartbeat expiry from the current clock", () =>
Line 3836: + it.effect("never revives an expired claim on heartbeat", () =>
Line 3854: + it.effect("settles cancellation before starting a pending successor", () =>
Line 3898: + it.effect("fails cancellation when cleanup loses execution ownership", () =>
Line ...
<path>/tmp/opencode/session-recovery-final-v6.patch</path>
<type>file</type>
<content>
6088: }),
6089: )
6090: diff --git a/packages/core/test/session-run-coordinator.test.ts b/packages/core/test/session-run-coordinator.test.ts
6091: index dfbeda664c..63dfad7a71 100644
6092: --- a/packages/core/test/session-run-coordinator.test.ts
6093: +++ b/packages/core/test/session-run-coordinator.test.ts
6094: @@ -282,6 +282,51 @@ describe("SessionRunCoordinator", () => {
6095: ),
6096: )
6097:
6098: + it.effect("finalizes interruption when cleanup dies", () =>
6099: + Effect.scoped(
6100: + Effect.gen(function* () {
6101: + const firstStarted = yield* Deferred.make<void>()
6102: + const cleanupStarted = yield* Deferred.make<void>()
6103: + const cleanupGate = yield* Deferred.make<void>()
6104: + const secondStarted = yield* Deferred.make<void>()
6105: + const defect = new Error("cleanup defect")
6106: + let runs = 0
6107: + ...
<path>/tmp/opencode/session-recovery-final-v6.patch</path>
<type>file</type>
<content>
6560: + LLMEvent.stepStart({ index: 0 }),
6561: + LLMEvent.stepFinish({ index: 0, reason: "stop" }),
6562: + LLMEvent.finish({ reason: "stop" }),
6563: + ]
6564: + requests.length = 0
6565: +
6566: + yield* SessionRecovery.recover()
6567: + while (requests.length === 0) yield* Effect.yieldNow
6568: + const executionService = yield* SessionExecution.Service
6569: + while ((yield* executionService.active).size > 0) yield* Effect.yieldNow
6570: +
6571: + const request = requests[0]
6572: + if (!request) return yield* Effect.die("Redriven provider request not found")
6573: + expect(userTexts(request)).toEqual(["run after restart"])
6574: + expect(
6575: + yield* db.select().from(SessionInputTable).where(eq(SessionInputTable.id, message.id)).get(),
6576: + ).toMatchObject({
6577: + wake_pending: false,
6578: + ...
200 const entries = yield* SessionHistory.entriesForRunner(db, session.id, system.baselineSeq)
201 const context = entries.map((entry) => entry.message)
202 const isLastStep = agent.info?.steps !== undefined && currentStep >= agent.info.steps
203 const toolMaterialization = isLastStep ? undefined : yield* tools.materialize(agent.info?.permissions)
204 const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id
205 const request = LLM.request({
206 model,
207 http: {
208 headers: {
209 "x-session-affinity": session.id,
210 "X-Session-Id": session.id,
211 ...(session.parentID ? { "x-parent-session-id": session.parentID } : {}),
212 },
213 },
214 providerOptions: { openai: { promptCacheKey } },
215 system: [agent.info?.system, system.baseline]
216 .filter((part): ...
Found 84 matches
/tmp/opencode/session-recovery-final-v6.patch:
Line 3040: - tokens: stepSettlement.tokens,
Line 3052: + tokens: stepSettlement.tokens,
Line 3568: import { DatabaseMigration } from "@opencode-ai/core/database/migration"
Line 3569: import { migrations } from "@opencode-ai/core/database/migration.gen"
Line 3570: @@ -16,6 +16,7 @@ import eventSourcedSessionInputMigration from "@opencode-ai/core/database/migrat
Line 3571: import contextEpochAgentMigration from "@opencode-ai/core/database/migration/20260605042240_add_context_epoch_agent"
Line 3572: import simplifyIntegrationCredentialsMigration from "@opencode-ai/core/database/migration/20260611192811_lush_chimera"
Line 3573: import simplifySessionInputMigration from "@opencode-ai/core/database/migration/20260622202450_simplify_session_input"
Line 3574: +import sessionRecoveryMigration from "@opencode-ai/core/database/migration/20260914170650_session-recovery"
...
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 ...
No files found
packages/core/src/event.ts packages/core/src/event/sql.ts packages/core/src/session/event.ts packages/core/src/session/runner/publish-llm-event.ts
1 export * as EventV2 from "./event"
2
3 import { Cause, Context, Effect, Layer, Option, PubSub, Queue, Schema, Stream } from "effect"
4 import { Event } from "@opencode-ai/schema/event"
5 import type { Data, Definition, Payload } from "@opencode-ai/schema/event"
6 import { and, asc, eq, gt, inArray } from "drizzle-orm"
7 import { Database } from "./database/database"
8 import { EventSequenceTable, EventTable } from "./event/sql"
9 import { Location } from "./location"
10 import { makeGlobalNode } from "./effect/app-node"
11 import { isDeepStrictEqual } from "node:util"
12 import { Durable } from "@opencode-ai/schema/durable-event-manifest"
13
14 export const ID = Event.ID
15 export type ID = import("@opencode-ai/schema/event").ID
16 export type { Data, Definition, Payload } from "@opencode-ai/schema/event"
17
18 export type Subscriber<D extends Definition = Definition> = (event: Payload<D>) => ...
350 }),
351 { behavior: "immediate" },
352 )
353 .pipe(Effect.orDie)
354 if (committed) {
355 yield* Effect.forEach(
356 pubsub.durable.get(committed.aggregateID) ?? [],
357 (wake) => PubSub.publish(wake, undefined),
358 { discard: true },
359 )
360 }
361 return committed
362 }),
363 )
364 }
365 }
366 })
367 }
368
369 function publishEvent<D extends Definition>(definition: D, event: Payload<D>, commit?: PublishOptions["commit"]) {
370 return Effect.gen(function* () {
371 if (!definition?.durable && commit)
372 return yield* Effect.die(
373 new ...
ab9408c81cc9916177641ae8e9f08624fc27b5b9:packages/core/src/event.ts:140: options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
ab9408c81cc9916177641ae8e9f08624fc27b5b9:packages/core/src/event.ts:144: options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
ab9408c81cc9916177641ae8e9f08624fc27b5b9:packages/core/src/event.ts:147: readonly claim: (aggregateID: string, ownerID: string) => Effect.Effect<void>
ab9408c81cc9916177641ae8e9f08624fc27b5b9:packages/core/src/event.ts:211: readonly ownerID?: string
ab9408c81cc9916177641ae8e9f08624fc27b5b9:packages/core/src/event.ts:212: readonly strictOwner?: boolean
ab9408c81cc9916177641ae8e9f08624fc27b5b9:packages/core/src/event.ts:244: .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id })
ab9408c81cc9916177641ae8e9f08624fc27b5b9:packages/core/src/event.ts:254: ...
320 (yield* db
321 .select()
322 .from(EventSequenceTable)
323 .where(inArray(EventSequenceTable.aggregate_id, sessionIDs))
324 .all()
325 .pipe(Effect.orDie)).map((row) => [row.aggregate_id, row.seq]),
326 )
327 : {}
328
329 const response = yield* http.execute(
330 HttpClientRequest.post(route(url, "/sync/history"), {
331 headers: new Headers(headers),
332 body: HttpBody.jsonUnsafe(state),
333 }),
334 )
335
336 if (response.status < 200 || response.status >= 300) {
337 const body = yield* response.text
338 return yield* new SyncHttpError({
339 message: `Workspace history HTTP failure: ${response.status} ${body}`,
340 status: response.status,
341 body,
342 })
343 }
344
345 const history = ...
ab9408c81cc9916177641ae8e9f08624fc27b5b9:packages/core/src/session/projector.ts:428: .delete(SessionMessageTable)
400 })
401 .where(eq(SessionTable.id, event.data.sessionID))
402 .run()
403 .pipe(Effect.orDie, Effect.asVoid),
404 )
405 yield* events.project(SessionEvent.RevertEvent.Cleared, (event) =>
406 db
407 .update(SessionTable)
408 .set({ revert: null, time_updated: DateTime.toEpochMillis(event.data.timestamp) })
409 .where(eq(SessionTable.id, event.data.sessionID))
410 .run()
411 .pipe(Effect.orDie, Effect.asVoid),
412 )
413 yield* events.project(SessionEvent.RevertEvent.Committed, (event) =>
414 Effect.gen(function* () {
415 const boundary = yield* db
416 .select({ seq: SessionMessageTable.seq })
417 .from(SessionMessageTable)
418 .where(
419 and(
420 eq(SessionMessageTable.session_id, event.data.sessionID),
421 eq(SessionMessageTable.id, ...
<path>/tmp/opencode/session-recovery-final-v6.patch</path>
<type>file</type>
<content>
1: diff --git a/packages/core/schema.json b/packages/core/schema.json
2: index d0eeeebd5c..dcc34e4652 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": "ec4e81da-4921-4488-85cd-745c52ddce17",
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-v6.patch</path>
<type>file</type>
<content>
451: + },
452: + {
453: + "columns": [
454: + "assistant_message_id"
455: + ],
456: + "tableTo": "session_message",
457: + "columnsTo": [
458: + "id"
459: + ],
460: + "onUpdate": "NO ACTION",
461: + "onDelete": "CASCADE",
462: + "nameExplicit": false,
463: + "name": "fk_session_recovery_assistant_message_id_session_message_id_fk",
464: + "entityType": "fks",
465: + "table": "session_recovery"
466: + },
467: + {
468: + "columns": [
469: + "continuation_message_id"
470: + ],
471: + "tableTo": "session_input",
472: + "columnsTo": [
473: + "id"
474: + ],
475: + "onUpdate": "NO ACTION",
476: + "onDelete": "CASCADE",
477: + "nameExplicit": false,
478: + "name": "fk_session_recovery_continuation_message_id_session_input_id_fk",
479: + "entityType": "fks",
480: + ...
518 47 packages/core/schema.json 1 0 packages/core/src/database/migration.gen.ts 55 0 packages/core/src/database/migration/20260914170650_session-recovery.ts 45 0 packages/core/src/database/schema.gen.ts 5 4 packages/core/src/session.ts 31 16 packages/core/src/session/compaction.ts 54 34 packages/core/src/session/context-epoch.ts 135 19 packages/core/src/session/execution/local.ts 342 31 packages/core/src/session/input.ts 69 0 packages/core/src/session/message-updater.ts 77 1 packages/core/src/session/projector.ts 331 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 244 95 packages/core/src/session/runner/llm.ts 76 29 packages/core/src/session/runner/publish-llm-event.ts 68 1 packages/core/src/session/sql.ts 66 1 packages/core/test/database-migration.test.ts 283 0 packages/core/test/session-execution-local.test.ts 1780 3 packages/core/test/session-projector.test.ts 247 25 packages/core/test/session-p...
packages/core/schema.json | 565 ++++++ packages/core/src/database/migration.gen.ts | 1 .../migration/20260914170650_session-recovery.ts | 55 + packages/core/src/database/schema.gen.ts | 45 + packages/core/src/session.ts | 9 packages/core/src/session/compaction.ts | 47 - packages/core/src/session/context-epoch.ts | 88 + packages/core/src/session/execution/local.ts | 154 ++ packages/core/src/session/input.ts | 373 ++++ packages/core/src/session/message-updater.ts | 69 + packages/core/src/session/projector.ts | 78 + packages/core/src/session/recovery.ts | 331 ++++ packages/core/src/session/run-coordinator.ts | 35 packages/core/src/session/runner/index.ts | 4 packages/core/src/session/runner/llm.ts | 339 +++- .../core/src/session/runner/publish-llm-event.ts | 105 + ...
Found 4 matches /tmp/opencode/session-recovery-final-v6.patch: Line 6324: +let runnerFinalizer = Effect.void Line 6343: + Effect.ensuring(Effect.suspend(() => runnerFinalizer)), Line 6391: + runnerFinalizer = Effect.void Line 7130: + runnerFinalizer = Deferred.succeed(finalizerStarted, undefined).pipe(Effect.andThen(Deferred.await(releaseFinalizer)))
<path>/tmp/opencode/session-recovery-final-v6.patch</path>
<type>file</type>
<content>
6280: })
6281: expect(decoded.result).toMatchObject({ type: "content" })
6282: diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts
6283: index cc58b43b29..8b1344390a 100644
6284: --- a/packages/core/test/session-runner.test.ts
6285: +++ b/packages/core/test/session-runner.test.ts
6286: @@ -31,7 +31,8 @@ import { SessionMessage } from "@opencode-ai/core/session/message"
6287: import { Prompt } from "@opencode-ai/core/session/prompt"
6288: import { SessionProjector } from "@opencode-ai/core/session/projector"
6289: import { SessionExecution } from "@opencode-ai/core/session/execution"
6290: -import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator"
6291: +import { SessionExecutionLocal } from "@opencode-ai/core/session/execution/local"
6292: +import { SessionRecovery } from "@opencode-ai/core/session/recovery"
6293: ...
1 import {
2 LLM,
3 LLMClient,
4 LLMError,
5 LLMEvent,
6 Message,
7 SystemPart,
8 isContextOverflowFailure,
9 type ProviderErrorEvent,
10 } from "@opencode-ai/llm"
11 import { Cause, DateTime, Effect, FiberSet, Layer, Option, Semaphore, Stream } from "effect"
12 import { AgentV2 } from "../../agent"
13 import { Config } from "../../config"
14 import { Database } from "../../database/database"
15 import { EventV2 } from "../../event"
16 import { Location } from "../../location"
17 import { ModelV2 } from "../../model"
18 import { PermissionV2 } from "../../permission"
19 import { ProviderV2 } from "../../provider"
20 import { QuestionV2 } from "../../question"
21 import { SystemContext } from "../../system-context/index"
22 import { SystemContextRegistry } from "../../system-context/registry"
23 import { SkillGuidance } from "../../skill/guidance"
24 import { ...
130 callID: tool.id,
131 error: { type: "unknown", message: "Tool execution interrupted" },
132 provider: {
133 executed: tool.provider?.executed === true,
134 ...(tool.provider?.metadata === undefined ? {} : { metadata: tool.provider.metadata }),
135 },
136 })
137 }
138 }
139 })
140
141 const awaitToolFibers = (fibers: FiberSet.FiberSet<void, ToolOutputStore.Error>) =>
142 Effect.raceFirst(FiberSet.join(fibers), FiberSet.awaitEmpty(fibers))
143
144 // Match V1: declining a user prompt halts the loop instead of becoming model-facing tool output.
145 const isUserDeclined = (cause: Cause.Cause<unknown>) =>
146 cause.reasons.some(
147 (reason) =>
148 Cause.isDieReason(reason) &&
149 (reason.defect instanceof PermissionV2.DeclinedError || reason.defect instanceof ...
Found 1 matches /home/byk/.local/share/opencode/repos/github.com/Effect-TS/effect-smol/packages/effect/src/FiberSet.ts: Line 680: export const join = <A, E>(self: FiberSet<A, E>): Effect.Effect<void, E> =>
<path>/home/byk/.local/share/opencode/repos/github.com/Effect-TS/effect-smol/packages/effect/src/FiberSet.ts</path>
<type>file</type>
<content>
630: )
631:
632: /**
633: * Gets the number of fibers currently in the FiberSet.
634: *
635: * **Example** (Checking the set size)
636: *
637: * ```ts
638: * import { Effect, FiberSet } from "effect"
639: *
640: * const program = Effect.gen(function*() {
641: * const set = yield* FiberSet.make()
642: *
643: * console.log(yield* FiberSet.size(set)) // 0
644: *
645: * // Add some fibers
646: * yield* FiberSet.run(set, Effect.never)
647: * yield* FiberSet.run(set, Effect.never)
648: *
649: * console.log(yield* FiberSet.size(set)) // 2
650: * })
651: * ```
652: *
653: * @category combinators
654: * @since 2.0.0
655: */
656: export const size = <A, E>(self: FiberSet<A, E>): Effect.Effect<number> =>
657: Effect.sync(() => self.state._tag === "Closed" ? 0 : self.state.backing.size)
658:
659: /**
660: * Joins ...
Found 1 matches /home/byk/.local/share/opencode/repos/github.com/Effect-TS/effect-smol/packages/effect/src/FiberSet.ts: Line 414: export const clear = <A, E>(self: FiberSet<A, E>): Effect.Effect<void> =>
<path>/home/byk/.local/share/opencode/repos/github.com/Effect-TS/effect-smol/packages/effect/src/FiberSet.ts</path>
<type>file</type>
<content>
400: * yield* FiberSet.run(set, Effect.never)
401: *
402: * console.log(yield* FiberSet.size(set)) // 2
403: *
404: * // Clear all fibers
405: * yield* FiberSet.clear(set)
406: *
407: * console.log(yield* FiberSet.size(set)) // 0
408: * })
409: * ```
410: *
411: * @category combinators
412: * @since 2.0.0
413: */
414: export const clear = <A, E>(self: FiberSet<A, E>): Effect.Effect<void> =>
415: Effect.suspend(() => {
416: if (self.state._tag === "Closed") {
417: return Effect.void
418: }
419: return Fiber.interruptAllAs(self.state.backing, internalFiberId)
420: })
421:
422: const constInterruptedFiber = (function() {
423: let fiber: Fiber.Fiber<never, never> | undefined = undefined
424: return () => {
425: if (fiber === undefined) {
426: fiber = Effect.runFork(Effect.interrupt)
427: ...
Found 66 matches
/tmp/opencode/session-recovery-final-v6.patch:
Line 1050: + requestExecution: input.resume !== false,
Line 1571: + readonly requestExecution: boolean
Line 1579: + const shouldWake = input.requestExecution ? yield* requestExecution(db, events, input) : false
Line 1589: + requestExecution: input.requestExecution,
Line 1618: + shouldWake: input.requestExecution && shouldWake,
Line 1627: + if (!input.requestExecution) return Effect.succeed({ admitted: stored, shouldWake: false })
Line 1628: + return requestExecution(db, events, input).pipe(
Line 1637: +const requestExecution = Effect.fn("SessionInput.requestExecution")(function* (
Line 1714: + readonly requestExecution: boolean
Line 1722: + wake_pending: input.requestExecution,
Line 2081: + "session.next.prompt.execution_requested": () => Effect.void,
Line 2089: + "session.next.step.interrupted": (event) => {
...
<path>/tmp/opencode/session-recovery-final-v6.patch</path>
<type>file</type>
<content>
7202: diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts
7203: index fb9d2db656..748e8aa43b 100644
7204: --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts
7205: +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts
7206: @@ -66,6 +66,7 @@ import { SessionProjector } from "@opencode-ai/core/session/projector"
7207: import { SessionV2 } from "@opencode-ai/core/session"
7208: import { SessionExecution } from "@opencode-ai/core/session/execution"
7209: import * as SessionExecutionLocal from "@opencode-ai/core/session/execution/local"
7210: +import { SessionRecovery } from "@opencode-ai/core/session/recovery"
7211: import { lazy } from "@/util/lazy"
7212: import { CorsConfig, isAllowedCorsOrigin, type CorsOptions } from "@opencode-ai/server/cors"
7213: import { serveUIEffect } ...
Found 100 matches (more matches available)
/tmp/opencode/session-recovery-final-v6.patch:
Line 1255: + yield* SessionExecutionClaim.transaction(
Line 1289: + const updated = yield* SessionExecutionClaim.transaction(
Line 1320: + const updated = yield* SessionExecutionClaim.transaction(
Line 1351: +import { SESSION_EXECUTION_LEASE_MS, SessionExecutionTable, SessionRecoveryTable } from "../sql"
Line 1370: + .insert(SessionExecutionTable)
Line 1380: + .returning({ id: SessionExecutionTable.id })
Line 1404: + SessionExecutionClaim.update(db, claim).pipe(
Line 1417: + yield* SessionExecutionClaim.clear(db, claim)
Line 1422: + .select({ phase: SessionExecutionTable.phase })
Line 1423: + .from(SessionExecutionTable)
Line 1426: + eq(SessionExecutionTable.session_id, claim.sessionID),
Line 1427: + eq(SessionExecutionTable.id, claim.executionID),
Line 1428: + ...
Found 28 matches /tmp/opencode/session-recovery-final-v6.patch: Line 214: + "name": "wake_attempts", Line 251: + "name": "wake_attempts", Line 831: + "value": "wake_attempts", Line 891: + yield* tx.run(`ALTER TABLE \`session\` ADD \`wake_attempts\` integer DEFAULT 0 NOT NULL;`) Line 915: + \`wake_attempts\` integer DEFAULT 0 NOT NULL, Line 933: + `CREATE INDEX \`session_recovery_wake_pending_error_attempts_created_execution_idx\` ON \`session_recovery\` (\`wake_pending\`, \`recovery_error\`, \`wake_attempts\`, \`time_created\`, \`execution_id\`);`, Line 984: + \`wake_attempts\` integer DEFAULT 0 NOT NULL, Line 1000: + \`wake_attempts\` integer DEFAULT 0 NOT NULL, Line 1031: + `CREATE INDEX \`session_recovery_wake_pending_error_attempts_created_execution_idx\` ON \`session_recovery\` (\`wake_pending\`,\`recovery_error\`,\`wake_attempts\`,\`time_created\`,\`execution_id\`);`, Line 2471: + ...
62040b8a95=128 bfe344ec5c=128 879956b9f6=128 5ffe5b0b20=128
The clean-apply proof succeeded for all 33 paths: every base blob prefix matches the stated base tree, every hunk context/deletion matches, and every reconstructed file hashes to the patch’s destination blob ID. Two safety areas now need adversarial scrutiny: the deliberate-decline ownership handoff and whether tool-fiber failure can mark a provider failure safe before every local fiber has stopped.
<path>/tmp/opencode/session-recovery-final-v6.patch</path>
<type>file</type>
<content>
3938: diff --git a/packages/core/test/session-projector.test.ts b/packages/core/test/session-projector.test.ts
3939: index 7ebcd97314..b5635bc33b 100644
3940: --- a/packages/core/test/session-projector.test.ts
3941: +++ b/packages/core/test/session-projector.test.ts
3942: @@ -1,5 +1,6 @@
3943: import { describe, expect } from "bun:test"
3944: -import { DateTime, Effect, Schema } from "effect"
3945: +import { DateTime, Deferred, Effect, Exit, Fiber, Schema } from "effect"
3946: +import { TestClock } from "effect/testing"
3947: import { asc, eq, sql } from "drizzle-orm"
3948: import { Database } from "@opencode-ai/core/database/database"
3949: import { LayerNode } from "@opencode-ai/core/effect/layer-node"
3950: @@ -17,15 +18,33 @@ import { SessionMessage } from "@opencode-ai/core/session/message"
3951: import { Prompt } from "@opencode-ai/core/session/prompt"
3952: import { ...
<path>/tmp/opencode/session-recovery-final-v6.patch</path>
<type>file</type>
<content>
4867: + it.effect("redrives one existing recovery after its replacement claim crashes before promotion", () =>
4868: + Effect.gen(function* () {
4869: + const { db } = yield* Database.Service
4870: + const events = yield* EventV2.Service
4871: + yield* db
4872: + .insert(ProjectTable)
4873: + .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
4874: + .run()
4875: + yield* db
4876: + .insert(SessionTable)
4877: + .values({
4878: + id: sessionID,
4879: + project_id: Project.ID.global,
4880: + slug: "test",
4881: + directory: "/project",
4882: + title: "test",
4883: + version: "test",
4884: + })
4885: + .run()
4886: + yield* events.publish(SessionEvent.Step.Recovered, {
4887: + sessionID,
4888: + timestamp: ...
Found 100 matches (more matches available) /tmp/opencode/session-recovery-final-v6.patch: Line 45: + "type": "text", Line 48: + "default": null, Line 55: + "type": "text", Line 58: + "default": null, Line 65: + "type": "text", Line 68: + "default": null, Line 75: + "type": "text", Line 78: + "default": null, Line 85: + "type": "text", Line 88: + "default": null, Line 95: + "type": "integer", Line 98: + "default": null, Line 105: + "type": "integer", Line 108: + "default": null, Line 115: + "type": "text", Line 118: + "default": null, Line 132: + "type": "integer", Line 135: + "default": "false", Line 149: + "type": "text", Line 152: + "default": null, Line 159: + "type": "text", Line 162: + "default": null, Line 169: + "type": "text", Line 172: + "default": null, Line 179: + "type": ...
=== packages/core/src/session/runner/llm.ts ===
140: assistantMessageID: message.id,
141: callID: tool.id,
142: error: { type: "unknown", message: "Tool execution interrupted" },
143: provider: {
144: executed: tool.provider?.executed === true,
145: ...(tool.provider?.metadata === undefined ? {} : { metadata: tool.provider.metadata }),
146: },
147: },
148: { commit: () => SessionExecutionClaim.update(db, claim) },
149: )
150: }
151: }
152: })
153:
154: const awaitToolFibers = (fibers: FiberSet.FiberSet<void, ToolOutputStore.Error>) =>
155: Effect.raceFirst(FiberSet.join(fibers), FiberSet.awaitEmpty(fibers))
156:
157: // Match V1: declining a user prompt halts the loop instead of becoming model-facing tool output.
158: const isUserDeclined = (cause: Cause.Cause<unknown>) =>
159: cause.reasons.some(
160: ...
=== packages/core/src/session/execution/claim.ts ===
20: constructor(readonly claim: Claim) {
21: super(`Session execution claim lost: ${claim.executionID}`)
22: }
23: }
24:
25: const where = (claim: Claim, now: number) =>
26: and(
27: eq(SessionExecutionTable.session_id, claim.sessionID),
28: eq(SessionExecutionTable.id, claim.executionID),
29: eq(SessionExecutionTable.owner_id, claim.ownerID),
30: gt(SessionExecutionTable.expires_at, now),
31: )
32:
33: /**
34: * Lease timestamps use node wall clocks. The lease interval must remain larger than the deployment's
35: * maximum clock skew plus one renewal interval. Execution IDs fence a node immediately after takeover;
36: * an expired claim never renews itself.
37: */
38: export const update = Effect.fn("SessionExecutionClaim.update")(function* (
39: db: DatabaseService,
40: claim: Claim,
41: values: Values = {},
42: ) {
43: const now = yield* Clock.currentTimeMillis
44: const updated = ...
3380: LLMEvent.finish({ reason: "tool-calls" }),
3381: ],
3382: [
3383: LLMEvent.stepStart({ index: 0 }),
3384: LLMEvent.stepFinish({ index: 0, reason: "stop" }),
3385: LLMEvent.finish({ reason: "stop" }),
3386: ],
3387: ]
3388:
3389: yield* session.resume(sessionID)
3390:
3391: expect(requests).toHaveLength(2)
3392: expect(yield* session.context(sessionID)).toMatchObject([
3393: { type: "user", text: "Call blocked" },
3394: {
3395: type: "assistant",
3396: content: [
3397: { type: "tool", id: "call-blocked", state: { status: "error", error: { message: "Permission blocked" } } },
3398: ],
3399: },
3400: { type: "assistant", finish: "stop" },
3401: ])
3402: }),
3403: )
3404:
3405: it.effect("interrupts runner continuation when permission approval is declined", () =>
3406: Effect.gen(function* () {
3407: ...
The explicit interruption path keeps the exact claim alive through coordinator cleanup and rejects a replacement row on final clear. A separate path bypasses that guarantee: a user-declined tool deletes the claim inside the runner before the outer runner finalizer completes. I also confirmed that FiberSet.join fails on the first failed tool fiber, so the current race can classify provider failure as safe while sibling local-tool fibers remain active.
a5c1bace440d1b6acb383c4fcffc52714a9373892e75b4f23bdbbf2dfdad8e16 /tmp/opencode/session-recovery-final-v6.patch d09c992a0f4cce15e73dd158fe60413420109c821afed6e121d11cdad9c7b583 /home/byk/Code/opencode/packages/opencode/dist/server/opencode-server commit ab9408c81cc9916177641ae8e9f08624fc27b5b9 2e9407768d1401a25b69febb36657638a779053a d3323b963b9ef2de41a7ff9884299c74b93886ba
MUST-FIX
User decline releases ownership before runner finalizers finish. packages/core/src/session/runner/llm.ts:393-397 publishes Step.Interrupted, deletes the exact claim, then returns an interrupt. The outer runner and location scopes have not finished (packages/core/src/session/execution/local.ts:145-150). A blocked outer finalizer leaves no unique Session claim, so another node can acquire and execute the Session while the old ownership chain remains active. The existing test only checks eventual deletion at packages/core/test/session-runner.test.ts:3405-3457; it never blocks an outer finalizer. The clear is exactly fenced and cannot delete a replacement owner, but it surrenders the current ownership too early.
A provider failure can become safe while sibling local-tool fibers remain active. awaitToolFibers races first failure against set emptiness at packages/core/src/session/runner/llm.ts:154-155. FiberSet.join fails when the first tool fiber fails; it does not wait for sibling fibers. The failure branch marks unsettled tools terminal at lines 410-416 and writes phase: "safe" at lines 417-420 without clearing and awaiting the remaining fibers. A blocked sibling can still execute or stall in a finalizer after its durable state says failed. This violates the requirement that provider failure never becomes safe until every local fiber and tool has a durable terminal outcome. Add a regression with two tools: one fails, one remains blocked through a finalizer; the claim must never become safe until the blocked fiber finishes.
CONCERN
wake_attempts values can defeat bounded fairness permanently. Both counters lack non-negative and upper-bound checks at packages/core/src/session/sql.ts:176-205 and in the migration. Recovery sorts by attacker-controlled counters at packages/core/src/session/recovery.ts:194-208 and increments them through JavaScript arithmetic at lines 232-245 and 256-278. ...Date: Sep 16, 2026 * 🔴 (13:39) User asserted that an expired Session execution claim “never renews itself.” * 🔴 (13:39) `packages/core/src/session/execution/claim.ts:25-31` defines `where(claim, now)` to fence operations by exact `session_id`, `id`, `owner_id`, and `expires_at > now`. * 🔴 (13:39) `packages/core/src/session/execution/claim.ts:33-36` states that lease timestamps use node wall cl…
Date: Sep 16, 2026 * 🔴 (13:37) User asserted recovery must “never recover a pre-promotion claim when input commits at the recovery boundary.” * 🔴 (13:37) User asserted recovery must “never promote a recovery superseded before scanner validation.” * 🔴 (13:37) User asserted recovery must “never promote a quarantined recovery continuation as an ordinary steer.” * 🔴 (13:37) User asserted recovery…
Date: Sep 16, 2026 * 🔴 (13:34) Effect’s `FiberSet.clear(self)` implementation at `/home/byk/.local/share/opencode/repos/github.com/Effect-TS/effect-smol/packages/effect/src/FiberSet.ts:414-420` returns `Effect.void` when the set state is `"Closed"`; otherwise it calls `Fiber.interruptAllAs(self.state.backing, internalFiberId)`. * 🔴 (13:35) In `/tmp/opencode/session-recovery-final-v6.patch`, `pa…
Date: Sep 16, 2026 * 🔴 (13:31) In `/tmp/opencode/session-recovery-final-v6.patch`, `packages/core/test/session-runner.test.ts` replaces the `SessionRunCoordinator` import with `SessionExecutionLocal` and `SessionRecovery`, adds `SessionExecutionTable` and `SessionRecoveryTable`, adds `sql` from `drizzle-orm`, and defines `const executionID = "test-execution"`. * 🔴 (13:31) The patched test execu…
Date: Sep 16, 2026 * 🔴 (13:29) In `packages/core/src/event.ts`, `publishEvent<D extends Definition>(definition, event, commit?)` dies with `InvalidDurableEventError` and message `"Local commit hooks require a durable event"` when a commit hook is supplied for a non-durable definition. * 🔴 (13:29) For durable definitions, `publishEvent()` calls `commitDurableEvent(definition, event as Payload, u…
Date: Sep 16, 2026 * 🔴 (13:29) `packages/core/src/event.ts` exports `EventV2` from `"./event"` and re-exports `Data`, `Definition`, and `Payload` from `@opencode-ai/schema/event`; it aliases `ID` to `Event.ID`. * 🔴 (13:29) `packages/core/src/event.ts` imports Effect primitives `Cause`, `Context`, `Effect`, `Layer`, `Option`, `PubSub`, `Queue`, `Schema`, and `Stream`, plus Drizzle operators `and…
Date: Sep 16, 2026 * 🔴 (13:28) User’s execution-ownership requirement is that the system `"never dispatches a provider after ownership changes"`. * 🔴 (13:28) User’s execution-ownership requirement is that the system `"never publishes streamed output after ownership changes"`. * 🔴 (13:28) User’s execution-ownership requirement is that the system `"never starts a local tool after ownership chang…
Date: Sep 16, 2026 * 🔴 (13:27) User’s patch updates `packages/core/src/session/sql.ts`: imports `sql` from `drizzle-orm` and `check` from `drizzle-orm/sqlite-core`; adds `SessionTable.wake_attempts` as non-null integer defaulting to `0`. * 🔴 (13:27) User’s patch adds `SessionInputTable.wake_pending` as a non-null boolean integer defaulting to `false`, plus index `session_input_wake_pending_prom…
Date: Sep 16, 2026 * 🔴 (13:27) User’s patch changes `packages/core/src/session/runner/publish-llm-event.ts` so `createLLMEventPublisher` input includes `startCommit: (assistantMessageID: SessionMessage.ID) => Effect.Effect<void>`. * 🔴 (13:27) User’s `createLLMEventPublisher` patch introduces `publishEvent(...)`, which distinguishes event definitions having `durable` behavior and supports publis…
Date: Sep 16, 2026 * 🔴 (13:26) User’s patch changes `packages/core/src/session/message-updater.ts` to import `message` from `./recovery-id` and handle `"session.next.prompt.execution_requested"` as a no-op in the in-memory message adapter. * 🔴 (13:26) User’s `"session.next.step.interrupted"` message update marks the owned assistant message completed at `event.data.timestamp`, sets `finish = "er…
Date: Sep 16, 2026 * 🔴 (13:24) User directed that Effect-specific work must use current Effect v4 / `effect-smol` source rather than memory or Effect v2/v3 examples; if `.opencode/references/effect-smol` is absent, clone `https://github.com/Effect-TS/effect-smol` into that project path, then search it and nearby repository code for exact APIs, tests, examples, naming patterns, and local house st…
Date: Sep 16, 2026 * 🔴 (13:23) User established jj workflow rules for repositories containing `.jj/`: use `jj` rather than `git` for mutations; never use interactive flags (`-i`, `--interactive`) or `jj resolve`; always pass `-m "msg"` when describing/committing; verify mutations with `jj st` and `jj log`; prefer stable letter-based change IDs over hexadecimal commit IDs; never rebase or describ…