Repository navigation
Conversation
|
Hey! Your PR title Please update it to start with one of:
Where See CONTRIBUTING.md for details. |
3 of 6 tasks
1 task done
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
3 of 6 tasks
moetemp
marked this pull request as draft
August 15, 2026 08:00
moetemp
force-pushed
the
2026/08/opencode-temporal
branch
from
August 17, 2026 08:30
15eef17 to
f85e59a
Compare
3 of 6 tasks
moetemp
force-pushed
the
2026/08/opencode-temporal-tui
branch
from
September 7, 2026 23:27
809d705 to
b2a9f42
Compare
moetemp
force-pushed
the
2026/08/opencode-temporal
branch
from
September 7, 2026 23:27
289f93b to
3434d80
Compare
moetemp
force-pushed
the
2026/08/opencode-temporal-tui
branch
from
September 14, 2026 18:11
b2a9f42 to
63a6949
Compare
moetemp
force-pushed
the
2026/08/opencode-temporal
branch
2 times, most recently
from
September 14, 2026 22:34
abca188 to
542f4c2
Compare
moetemp
force-pushed
the
2026/08/opencode-temporal-tui
branch
from
September 15, 2026 00:44
496fafe to
7459656
Compare
moetemp
force-pushed
the
2026/08/opencode-temporal
branch
from
September 15, 2026 00:44
542f4c2 to
1e2f3e7
Compare
Phase 1 of a drop-in durability layer: an opencode session becomes a durable Temporal workflow that owns the conversation and prompt queue and drives each turn against the shipping opencode server over HTTP. The turn runs server-side (prompt_async), so it survives a worker crash; recovery re-attaches and is idempotent on the user-message count. A crash demo kills the worker mid-turn and the session still completes: the runTurn activity re-drives on a fresh worker (attempt 2) with a single prompt sent. opencode's loop, tools, model, storage, and API are untouched. Phase 2 (a durable SessionExecution on the v2 engine) is scoped in the package README.
Phase 2: make each v2 session a durable Temporal workflow. SessionExecutionTemporal implements the substitutable SessionExecution service (wake -> signalWithStart, resume -> forced signalWithStart, interrupt -> cancel signal), with the local coordinator's drain (SessionRunner.run for the whole turn) moved into a runContinuation activity that runs against the durable event log. The Temporal client and an embedded worker are co-hosted inside the server process (both run under bun). It is opt-in via OPENCODE_SESSION_EXECUTION=temporal; the one-line swap is at routes.ts, and the loop, tools, model, storage, and HTTP API are untouched. Verified: with it enabled, prompting a v2 session drove a full turn to completion (step.ended) and Temporal recorded a completed per-session workflow (session-exec-<id>).
Added the engine-level crash-recovery harness (kill the whole server mid-turn, restart, the turn continues from the event log) and updated the README: Phase 2 is built and verified, with the run recipe, the code layout, and the known limits (resume does not yet return the typed RunError; active is process-local; startup needs Temporal reachable).
resume previously drove a forced run fire-and-forget and returned void, so a run failure was swallowed. It now drives the run through a Temporal Update-with-Start and awaits the result: a genuine run error is thrown non-retryable by the activity (so only crashes/timeouts still retry), rejects the update, and the layer maps it to a RunError (carried as ContextSnapshotDecodeError with the original text in details). The per-session workflow is now long-lived with an idle timeout so update-with-start can always reach it. Verified (scripts/resume-check.ts): resume resolves on a healthy session and rejects on a failing one. Full typecheck stays green (30/30).
active listed a process-local set of sessions this process had started, so it was empty after a restart. It now queries Temporal for the open per-session workflows (WorkflowType 'sessionExecution', Running) and maps their ids back to session ids, so it reflects durable state and survives a restart. The process-local set is gone.
resume previously surfaced run failures as a generic ContextSnapshotDecodeError carrier. The activity now encodes the error through a Schema.Union of every RunError member (run-error-codec.ts) into the non-retryable failure details, and the layer walks the failure chain and decodes it back into the exact tagged instance (e.g. LLMError with its reason), falling back to the carrier only if decoding fails. Verified: a unit round-trip reconstructs an LLMError faithfully, and scripts/resume-check.ts shows a failing resume rejects with the encoded _tag = LLM.Error reaching the caller.
The v2 engine event-sources to SQLite, so a session was resumable only on the host with the file. Point every worker at one shared store and any worker resumes: a new libSQL SqlClient backend (sqlite.libsql.ts, over @libsql/client) selected by OPENCODE_DB_URL gives a networked SQLite (sqld/Turso) across hosts, and OPENCODE_DB already shares a file same-host. Same SQLite dialect, so the schema and all migrations are unchanged; the local-only PRAGMAs are skipped for the shared backend. Verified: a full turn runs against a libSQL file: store (migrations + event log), and scripts/shared-store-failover.sh shows turn 1 on worker A, A killed, then a fresh worker B recalls turn 1's code word (two distinct worker identities) purely from the shared store. Remote-URL transaction atomicity is documented as the remaining networked-writer step. Full typecheck 30/30.
…ot a quirk. The 0-activities symptom was the condition-timeout scope leak, not the delivery mode. A default (steer) prompt drives a turn to completion once the leak is fixed.
The supervisor redesign reverted the OPENCODE_SESSION_IDLE_TIMEOUT forwarding and removed the file the contract lib typed against. The override rides as a third workflow argument now (continue-as-new keeps it), the lib types against the Temporal node, and the interrupt scenario expects idle retirement since an interrupted supervisor keeps serving.
The suite is the executable definition of the executor seam, so it lives where any driver package can import it; the local run is now a one-line conformance call. The old test-lib path re-exports the moved harness so the rest of the test tree keeps its import.
Core now carries only the executor seam, the built-in local executor, and the executor-agnostic toolkit (step runner, event fencing, error codec, worktree materializer); everything Temporal, including the per-step activity drain, lives in @opencode-ai/temporal, which the server wires in as one dependency. The step contract types moved into the drain where they belonged.
protocol.ts is the one source of truth for the workflow type, signal and update names, the workflow-id scheme, and the connection defaults; config.ts is a service read at layer build, so an embedder or a test injects settings instead of racing module-load env reads.
Positional arguments turn signature evolution into a breaking change mid-history; a single record lets new settings ride along, and a continue-as-new run carries it forward unchanged.
workflow-core became supervisor.ts when local mode stopped driving it; SupervisorRuntime and makeSupervisor say what the interface and the factory actually build.
The package reads as what it is now: one executor behind core's seam, held to the conformance suite, with a porting recipe for engines that want the same shape.
The suite mocks the LLM client, so the model descriptor's endpoint and token are never used for I/O; a real-looking OpenAI URL invited the wrong conclusion. An RFC 2606 .invalid host cannot be mistaken for a live dependency and cannot resolve if the mock is ever removed.
The resume scenarios left a durable executor's session running forever: the test's task queue dies with the process, so nothing ever processes the idle timer's task. Each scenario now waits out the retirement while its worker still exists.
With OPENCODE_TEMPORAL_ROLE=client the turn runs in a separate worker process, and its session.next.* events live on that process's bus; the TUI only observes its own daemon's admission. A reply therefore rendered one prompt late, when the next admission triggered a re-read. A followed session now re-reads until its latest assistant message settles.
The read model can show an assistant's completed flag before the settled row's rewritten content, so a follow that trusted one read stopped with the reply's tail missing. It now stops only after two identical settled reads, and it emits part removals when the settled rewrite coalesces streamed parts under new ids, so no stale text lingers.
A question raised inside an activity on one worker was parked on an in-memory deferred, so no other process could list or answer it and the step stayed blocked. Deterministic ids let a re-driven attempt adopt the same row or return answers that already landed, which also makes the question tool safe to mark idempotent for crash-resume.
The sync store only knew the v1 question event names, so the v2 engine's question.v2.* events populated nothing and the dialog never opened. The reply path also called a v1 route that 404s on the v2 daemon. In temporal mode the question tool runs inside a worker activity, so its asked event never reaches the daemon's stream. The session projector now polls the durable question list during a follow and projects the ask and answer lifecycle, so a worker-raised question opens the dialog and the reply routes back to unblock the turn.
The rebase kept dev's lockfile wherever it conflicted, which left this branch's own dependencies out of it.
Every file under packages/core/src is a public entry point, and two of these imported bun:test. The codec has no reader outside the Temporal package.
The install tool re-sorted the whole file for one added dependency, which turned a one-line change into a twenty-line diff.
Nothing branched on ran: the supervisor reads continue and nothing else. The continuation tail was the same seven lines in the crash-resume path and the step path.
… comments. Three files stood up the same environment, worker and workflow by hand. The comments they and workflow.ts carried described which fix a test caught rather than what it asserts.
Three files carried the same thirteen-node graph, the same fixtures and the same session seed, and each drifted from the others by a node or two for no reason.
…eates it. Both migrations are new here, so no store has applied one without the other.
… notes. The README narrated verification runs, a file layout that had moved and another branch's design. The two docs files were notes to self from review rounds and named files that no longer exist.
The branch was wrapped by hand at a narrower width than the repository formats at, so a maintainer's formatter would have rewritten every file it touched.
… read the durable tail on a tick. A turn that never stops stepping is a bug only the supervisor can see, since every step is its own activity and each one succeeds. The drain count alone misses a long stepped turn's history, so the server's own suggestion is read too. The serve process now subscribes with the polling event layer, because standalone workers append to a log its wake cannot see.
moetemp
force-pushed
the
2026/08/opencode-temporal-tui
branch
from
September 15, 2026 01:01
7459656 to
b43650c
Compare
moetemp
force-pushed
the
2026/08/opencode-temporal
branch
from
September 15, 2026 01:01
1e2f3e7 to
d04430c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue for this PR
Closes anomalyco#42678
Status: WIP. Review continues, but the correctness issues independent review (Codex, several rounds) found are folded in as of
82e1b69ca5: local mode runs on the provenSessionRunCoordinator; the Temporal supervisor was redesigned (interrupt cancels only the turn and keeps serving, resume joins the in-flight drain, the timed-condition scope leak is gone); the event-log owner token is unique per activity execution; interrupt delivery failures surface as defects. A deterministic harness (fake-runtime plus@temporalio/testing) and the two-driver contract suite back it. Fine to share internally as WIP; not ready for external delivery while review is open.Type of change
What does this PR do?
This PR adds a Temporal durable-execution layer to opencode (a fork of anomalyco/opencode dev at 4643e65).
It makes an opencode session a durable Temporal workflow: a coding session survives worker loss, runs detached, and resumes on any worker from a shared store. The shape is a plugin: core carries the
SessionExecutionseam, the built-in local executor (the provenSessionRunCoordinator), the executor-agnostic toolkit (step runner, event fencing, error codec, worktree materializer), and a conformance suite that defines the seam executably; everything Temporal lives in@opencode-ai/temporal, one dependency the server wires in, selected byOPENCODE_SESSION_EXECUTION=temporalwith one activity per step. On top of that: a shared libSQL event store with verified atomic writes, standalone workers (OPENCODE_TEMPORAL_ROLE), cross-process migrations, log-based crash resume with per-tool idempotency, durable permission asks replyable from any process, worktree materialization from snapshot packs, and turn loop bounds. The serve-wrapper increment lives in its own PR.The base branch
2026/08/opencode-temporal-tuicarries the TUI bridge (upstream anomalyco#42658) on the upstream commit this work sits on, so the diff shows only the Temporal work. Start atpackages/temporal/README.mdfor run recipes, verified claims, and the honest limits (thequestiontool, remote streaming throughput).How did you verify your code works?
Typecheck passes on
core,server, andcli. The driver-contract suite passes against both drivers (the Temporal run is opt-in against a dev server); the worktree-materializer and runner suites pass. The runnable reproductions (crash test, failover, worker smoke, demo) live in the stacked #4.Screenshots / recordings
Not a UI change.
Checklist
Since this description was written
The branch now sits on
moe/event-tick(#13) andmoe/fork-ci(#12), so the live event tick and the fork's CI workflow are their own PRs below this one, and this diff shows only the executor. The supervisor gained a step ceiling and rolls a run over on the server's advice (continueAsNewSuggested), because a stepped turn is thousands of history events and a drain count cannot see that. The event tick is wired in this package's composition root (EventV2.pollingNode), which is where a standalone worker's events have to reach a serve process's tail.Everything above this PR in the stack adds one thing each: #14 brings a stale worktree forward, #7 splits a step into three activities, #15 to #18 route and pin work and pick a deployment, #8 and #19 give a session a client-free start and a follower, #20 to #22 add the container check, the worktree quarantine and the turn budget.