Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
103 commits
Select commit Hold shift + click to select a range
0eff4fd
Added a Temporal durable-session layer over opencode serve.
moedash Aug 8, 2026
460b935
Added a Temporal-backed SessionExecution to the v2 engine.
moedash Aug 8, 2026
0d74c1b
Verified v2 crash recovery and documented Phase 2.
moedash Aug 8, 2026
29750b3
Typed the SessionExecution active set as SessionID.
moedash Aug 8, 2026
0c5841d
Made resume await the run and surface its error as a RunError.
moedash Aug 8, 2026
247a13f
Backed SessionExecution active with Temporal visibility.
moedash Aug 8, 2026
e238ab9
Reconstructed the exact tagged RunError across the Temporal boundary.
moedash Aug 8, 2026
7ee285c
Added a shared, durable event store so any worker resumes any session.
moedash Aug 9, 2026
affff64
Added a per-step Temporal turn (each step its own activity).
moedash Aug 9, 2026
dc8343e
Made remote libSQL writes atomic via interactive transactions.
moedash Aug 10, 2026
0ba0e61
Updated the shared-store caveat for atomic remote writes.
moedash Aug 10, 2026
8207cd6
Closed interrupted tools on every step re-drive, not just the first.
moedash Aug 10, 2026
89814d6
Resumed a crashed step from the log instead of re-running its tools.
moedash Aug 10, 2026
5913894
Documented the resumable per-step turn in the temporal README.
moedash Aug 10, 2026
27fb1b8
Documented the real cross-host resume boundary.
moedash Aug 10, 2026
eeac1a3
Ran the Temporal worker standalone, decoupled from the HTTP server.
moedash Aug 10, 2026
5d3b963
Serialized migrations across processes with a BEGIN IMMEDIATE lock.
moedash Aug 10, 2026
da67f4a
Re-ran idempotent tools on resume instead of failing them.
moedash Aug 10, 2026
d2373cf
Bounded the turn's step loop and the repeated-identical-call stuck loop.
moedash Aug 10, 2026
0226e0b
Made permission asks durable and replyable from any process.
moedash Aug 10, 2026
6c5ab76
Verified networked-libSQL transaction atomicity against a live sqld.
moedash Aug 10, 2026
08938f2
Added the worktree-portability design note.
moedash Aug 10, 2026
3e68017
Recovered wake-driven turns that a re-drive silently dropped.
moedash Aug 10, 2026
6970498
Raised the activity backstop to 12 hours; recorded cancellations as s…
moedash Aug 10, 2026
478afee
Keyed tool asks deterministically so re-drives adopt the same row.
moedash Aug 10, 2026
18d0972
Fixed two workflow races around drains and interrupts.
moedash Aug 10, 2026
d4e8504
Hardened resume, interrupt, and active against races and mode drift.
moedash Aug 10, 2026
969032c
Covered libSQL auto-commit statements with the transaction permit.
moedash Aug 10, 2026
5d8f2d9
Fixed the review's low-severity batch.
moedash Aug 10, 2026
041dd9c
Added the AI-399 local-options evaluation draft with measurements.
moedash Aug 10, 2026
e9e418c
Resolved the language-shim reference in the AI-399 draft.
moedash Aug 10, 2026
c7e70f1
Resolved the dev-server redistribution question in the AI-399 draft.
moedash Aug 10, 2026
d3ba35e
Resolved the desktop signal/update surface in the AI-399 draft.
moedash Aug 10, 2026
40cc196
Finalized the AI-399 evaluation with research findings.
moedash Aug 10, 2026
d23d583
Defined the appliance archetype on first use.
moedash Aug 11, 2026
3af5c8e
Clarified that the dev-server pairing quantifies the rejected alterna…
moedash Aug 11, 2026
f7a7d9e
Reframed the ambient-check pattern as the compatibility variant.
moedash Aug 11, 2026
5697c26
Ran the session supervisor in-process with a six-primitive micro-driver.
moedash Aug 11, 2026
decec77
Documented the one-supervisor-two-drivers demonstration and the SDK-s…
moedash Aug 11, 2026
b77ef40
Collapsed the factory to two modes: local and temporal.
moedash Aug 11, 2026
2ba9360
Dropped the bundled-server pairing from the measurements.
moedash Aug 11, 2026
129273a
Replaced raw control bytes in source with escape sequences.
moedash Aug 11, 2026
5c1ebfa
Guarded permission status transitions; adopted same-process retries.
moedash Aug 11, 2026
aa1f91b
Retried busy errors on the shared store; let reads skip the permit.
moedash Aug 11, 2026
7a10299
Bounded workflow history and closed two idle-boundary wake losses.
moedash Aug 11, 2026
ba2a43c
Served the step-entry checks from one projected-history load.
moedash Aug 11, 2026
0aca281
Applied foreign keys best-effort on the shared backend; small hygiene.
moedash Aug 11, 2026
c862234
Read the active-session store checks concurrently.
moedash Aug 12, 2026
5b30ac2
Typed the user-decline halt across the durable boundary.
moedash Aug 12, 2026
7576b31
Gave the local driver the same 12h drain backstop as Temporal mode.
moedash Aug 12, 2026
2e98df8
Swept abandoned pending permission rows; deflaked concurrent open.
moedash Aug 12, 2026
3b89eea
Retried the cold WAL switch instead of trusting busy_timeout.
moedash Aug 12, 2026
64cf634
Fenced the event log behind the running attempt.
moedash Aug 12, 2026
98b8cce
Corrected the remote throughput limit and the fenced-overlap note.
moedash Aug 12, 2026
fbc3f6c
Moved the serve-wrapper increment to its own branch.
moedash Aug 12, 2026
57b78f8
Rebuilt missing worktrees from shared-store snapshot packs.
moedash Aug 12, 2026
13e3d23
Added a how-it-fits-together overview to the README.
moedash Aug 12, 2026
511c939
Aligned the docs and scripts with the per-step reality.
moedash Aug 12, 2026
b343fb4
Removed the unreachable whole-turn execution path.
moedash Aug 12, 2026
16b50f5
Cleaned the docs' voice; fixed a stale mode count.
moedash Aug 12, 2026
56fec48
Waited out the catalog's async population in model resolution.
moedash Aug 13, 2026
311c0e5
Added a one-command tmux demo for the temporal mode.
moedash Aug 13, 2026
d4ee507
Computed the demo's UI URL from the server port.
moedash Aug 13, 2026
8f80aa0
Said plainly that the driver pane finishing is the expected end state.
moedash Aug 13, 2026
bfe88ce
Made the packaged CLI carry the temporal client path.
moedash Aug 13, 2026
78dcbd0
Waited out the integration state before sending requests without auth.
moedash Aug 13, 2026
f0b523a
Added a what-it-is row to the comparison table.
moedash Aug 14, 2026
7fc100e
Removed a duplicated dependency key.
moedash Aug 14, 2026
242bdee
Dropped the poll ticker and duration parser from the local driver.
moedash Aug 14, 2026
3e1b684
Rewrote the local session executor as a native coordinator.
jssmith Aug 14, 2026
8aa7e5e
Parameterized the local executor test into a driver-contract suite.
jssmith Aug 14, 2026
ef65928
Described the two-loop (one-drain) design in comments and README.
jssmith Aug 15, 2026
874d2f6
Honored OPENCODE_SESSION_IDLE_TIMEOUT in temporal mode.
moedash Aug 15, 2026
90bf050
Pointed the driver-contract suite at the Temporal driver.
moedash Aug 15, 2026
cc32317
Replaced the local driver's duration table with effect's Duration.
moedash Aug 15, 2026
fdf0bdf
Pruned the internal evaluation docs from the branch.
moedash Aug 15, 2026
8d28f4f
Split the verification scripts out of the upstream-curated branch.
moedash Aug 15, 2026
3d49f73
Run local sessions on the proven SessionRunCoordinator.
jssmith Aug 15, 2026
551559f
Add a Temporal test harness and fix a condition-cancellation blocker.
jssmith Aug 15, 2026
0444bcc
Fix the event-log owner token to be unique per activity execution.
jssmith Aug 15, 2026
b7fa5d6
Surface genuine interrupt-delivery failures instead of swallowing them.
jssmith Aug 15, 2026
f8f50d2
Count all drains toward continue-as-new, including resume-driven ones.
jssmith Aug 15, 2026
06cd5f4
Document the remaining Temporal supervisor coordination follow-ups.
jssmith Aug 15, 2026
e650696
Redesign the Temporal supervisor: interrupt keeps serving, resume joins.
jssmith Aug 15, 2026
0f90321
Test the supervisor redesign: fake-runtime + real-Temporal harness.
jssmith Aug 15, 2026
17d95f4
Update the Temporal supervisor follow-ups doc: deep items now fixed.
jssmith Aug 15, 2026
f356496
Add a branch summary describing the local pivot and Temporal-mode fixes.
jssmith Aug 15, 2026
6d142f0
Correct the branch doc: default prompt delivery is steer and works, n…
jssmith Aug 15, 2026
9b9ae68
Carried the idle override and the contract suite through the redesign.
moedash Aug 15, 2026
abad924
Promoted the conformance suite and test harness into core's source.
moedash Aug 15, 2026
86cb69c
Moved the Temporal executor into its own package.
moedash Aug 15, 2026
99f5f7c
Collected the executor's protocol and config into their own modules.
moedash Aug 15, 2026
029d4e2
Folded the workflow's inputs into one options record.
moedash Aug 15, 2026
73fe320
Renamed the supervisor pieces for what they are now.
moedash Aug 15, 2026
873f71e
Retold the README around the executor seam.
moedash Aug 15, 2026
af00e10
Made the conformance suite's fixture endpoint self-evidently inert.
moedash Aug 16, 2026
acb702b
Retired the conformance resume sessions before their run ends.
moedash Aug 16, 2026
70ad63e
Followed cross-process turns in the TUI's event projector.
moedash Aug 17, 2026
2121ff4
Stopped fabricating an agent and model on projected user messages.
moedash Aug 17, 2026
cb1fc5c
Confirmed settlement with a second read; removed coalesced parts.
moedash Aug 17, 2026
08b12e7
Updated the branch docs' supervisor paths after the package move.
moedash Aug 17, 2026
2fd6ee0
Made question asks durable rows, mirroring permission requests.
moedash Aug 17, 2026
3434d80
Rendered question prompts in the TUI, local and cross-process.
moedash Aug 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
385 changes: 365 additions & 20 deletions bun.lock

Large diffs are not rendered by default.

Empty file modified packages/cli/bin/lildax.cjs
100644 → 100755
Empty file.
7 changes: 6 additions & 1 deletion packages/cli/script/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ for (const item of targets) {
entrypoints: ["./src/index.ts"],
tsconfig: "./tsconfig.json",
plugins: [plugin],
external: ["node-gyp"],
// lightningcss (via ui -> tailwind) has a conditional native-binding require the bundler
// cannot resolve; the TUI never runs that path, so it stays external. @temporalio/worker
// drags webpack and swc (it bundles the workflow from source at startup), which cannot ride
// a compiled binary; a packaged serve runs OPENCODE_TEMPORAL_ROLE=client next to standalone
// workers instead.
external: ["node-gyp", "lightningcss", "@temporalio/worker"],
format: "esm",
minify: true,
sourcemap: sourcemapsFlag ? "linked" : "none",
Expand Down
95 changes: 88 additions & 7 deletions packages/cli/src/tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,13 +213,15 @@ function v1AssistantInfo(sessionID: string, item: any) {
}

function v1UserInfo(sessionID: string, item: any) {
// No fabricated agent or model: the prompt bar syncs its selection from the last user message,
// so an invented value would clobber the user's choice (and an empty model renders a
// "Model / is not valid" toast). An empty agent short-circuits that sync.
return {
id: item.id,
sessionID,
role: "user",
time: { created: epoch(item.time?.created) },
agent: "build",
model: { providerID: "", modelID: "" },
agent: "",
}
}

Expand Down Expand Up @@ -455,8 +457,28 @@ async function globalEventStream(origin: string, directory: string | null, heade
// its session.next.* stream into them: admissions and text deltas map directly for low latency,
// and every other session.next.* event schedules a debounced re-read of the session so the store
// converges on what the daemon persisted (tools, reasoning, tokens, titles).
//
// Cross-process turns stream nothing here: 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 only
// locally observable moment is this daemon's own admission, so a followed session keeps re-reading
// until its latest assistant message settles; without that, a reply would only render when the
// NEXT local event happened to trigger a re-read.
function createSessionProjector(origin: string, headers: HeadersInit | undefined, emit: (dir: string, payload: unknown) => void) {
const timers = new Map<string, ReturnType<typeof setTimeout>>()
const FOLLOW_INTERVAL = 1200
// A turn longer than this stops refreshing a cross-process TUI until the next admission.
const FOLLOW_DEADLINE = 15 * 60 * 1000
// deadline plus the signature of the last settled read: the read model can show the completed
// flag before the settled row's rewritten content, so a follow only stops after two identical
// settled reads.
const follows = new Map<string, { deadline: number; confirmed: string | null }>()
// Part ids emitted per message, so a settled rewrite that coalesces parts under new ids also
// removes the stale ones from the store instead of leaving duplicated text.
const emittedParts = new Map<string, Map<string, Set<string>>>()
// Question ids surfaced per session. A question raised inside a cross-process activity publishes
// its asked event on the worker's bus, never here, so the dialog would never open. The durable
// row is readable from this daemon, so poll it and project the asked/answered lifecycle.
const questionSeen = new Map<string, Set<string>>()
// The SDK validates every frame against the event schema; a nonconforming frame kills the
// stream, so synthetic events carry the required id and full property sets.
let counter = 0
Expand All @@ -465,27 +487,83 @@ function createSessionProjector(origin: string, headers: HeadersInit | undefined
type,
properties,
})
const resync = (sessionID: string, dir: string) => {
const resync = (sessionID: string, dir: string, delay = 250) => {
clearTimeout(timers.get(sessionID))
timers.set(
sessionID,
setTimeout(async () => {
timers.delete(sessionID)
let settled = false
let signature = ""
let pendingQuestions = 0
try {
const [info, items] = await Promise.all([
v2(origin, `/api/session/${sessionID}`, null, headers),
v2(origin, `/api/session/${sessionID}/message`, null, headers),
])
emit(dir, legacyEvent("session.updated", { sessionID, info: v1Session(info) }))
for (const message of v1Messages(sessionID, items as any[])) {
const messages = v1Messages(sessionID, items as any[])
const seen = emittedParts.get(sessionID) ?? new Map<string, Set<string>>()
emittedParts.set(sessionID, seen)
for (const message of messages) {
emit(dir, legacyEvent("message.updated", { sessionID, info: message.info }))
for (const part of message.parts)
const ids = new Set<string>()
for (const part of message.parts) {
ids.add(part.id)
emit(dir, legacyEvent("message.part.updated", { sessionID, part, time: Date.now() }))
}
for (const stale of seen.get(message.info.id) ?? []) {
if (!ids.has(stale))
emit(
dir,
legacyEvent("message.part.removed", { sessionID, messageID: message.info.id, partID: stale }),
)
}
seen.set(message.info.id, ids)
}
const latest = messages.at(-1)?.info
settled = latest?.role === "assistant" && Boolean(latest.time?.completed)
signature = messages
.map((m) => `${m.info.id}:${m.parts.map((part) => `${part.id}=${part.text?.length ?? part.state?.status ?? ""}`).join(",")}`)
.join(";")
} catch {}
}, 250),
try {
const pending = (await v2(origin, `/api/session/${sessionID}/question`, null, headers)) as any[]
const seen = questionSeen.get(sessionID) ?? new Set<string>()
const current = new Set<string>()
for (const question of pending) {
current.add(question.id)
emit(dir, legacyEvent("question.v2.asked", { id: question.id, sessionID, questions: question.questions, tool: question.tool }))
}
// A question that left the list was answered or rejected elsewhere; clear it from the store.
for (const stale of seen) {
if (!current.has(stale))
emit(dir, legacyEvent("question.v2.replied", { sessionID, requestID: stale, answers: [] }))
}
questionSeen.set(sessionID, current)
pendingQuestions = current.size
} catch {}
const follow = follows.get(sessionID)
if (follow === undefined) return
if (Date.now() > follow.deadline) {
follows.delete(sessionID)
return
}
// A pending question keeps the follow alive even once the assistant message looks settled:
// the turn is blocked on the answer, so the next change only lands after the user replies.
if (settled && follow.confirmed === signature && pendingQuestions === 0) {
follows.delete(sessionID)
return
}
follow.confirmed = settled ? signature : null
resync(sessionID, dir, FOLLOW_INTERVAL)
}, delay),
)
}
const follow = (sessionID: string, dir: string) => {
follows.set(sessionID, { deadline: Date.now() + FOLLOW_DEADLINE, confirmed: null })
resync(sessionID, dir)
}
const handle = (event: any) => {
const type = event?.type
if (typeof type !== "string") return
Expand Down Expand Up @@ -517,6 +595,7 @@ function createSessionProjector(origin: string, headers: HeadersInit | undefined
time: Date.now(),
}),
)
follow(sessionID, dir)
return
}
if (type === "session.next.step.started" && data.assistantMessageID) {
Expand Down Expand Up @@ -557,12 +636,14 @@ function createSessionProjector(origin: string, headers: HeadersInit | undefined
)
return
}
resync(sessionID, dir)
follow(sessionID, dir)
}
return Object.assign(handle, {
dispose() {
for (const timer of timers.values()) clearTimeout(timer)
timers.clear()
follows.clear()
emittedParts.clear()
},
})
}
Expand Down
27 changes: 14 additions & 13 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@
}
},
"devDependencies": {
"@opencode-ai/http-recorder": "workspace:*",
"@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-darwin-x64": "2.5.1",
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
"@parcel/watcher-linux-arm64-musl": "2.5.1",
"@parcel/watcher-linux-x64-glibc": "2.5.1",
"@parcel/watcher-linux-x64-musl": "2.5.1",
"@parcel/watcher-win32-arm64": "2.5.1",
"@parcel/watcher-win32-x64": "2.5.1",
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@types/cross-spawn": "catalog:",
Expand All @@ -49,15 +58,6 @@
"@types/semver": "catalog:",
"@types/turndown": "5.0.5",
"@types/which": "3.0.4",
"@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-darwin-x64": "2.5.1",
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
"@parcel/watcher-linux-arm64-musl": "2.5.1",
"@parcel/watcher-linux-x64-glibc": "2.5.1",
"@parcel/watcher-linux-x64-musl": "2.5.1",
"@parcel/watcher-win32-arm64": "2.5.1",
"@parcel/watcher-win32-x64": "2.5.1",
"@opencode-ai/http-recorder": "workspace:*",
"drizzle-kit": "catalog:"
},
"dependencies": {
Expand Down Expand Up @@ -85,22 +85,23 @@
"@effect/opentelemetry": "catalog:",
"@effect/platform-node": "catalog:",
"@effect/sql-sqlite-bun": "catalog:",
"@lydell/node-pty": "catalog:",
"@ff-labs/fff-bun": "0.9.4",
"@libsql/client": "^0.17.0",
"@lydell/node-pty": "catalog:",
"@npmcli/arborist": "9.4.0",
"@npmcli/config": "10.8.1",
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
"@opencode-ai/effect-sqlite-node": "workspace:*",
"@opencode-ai/llm": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@openrouter/ai-sdk-provider": "2.9.0",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/context-async-hooks": "2.6.1",
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
"@opentelemetry/sdk-trace-base": "2.6.1",
"@parcel/watcher": "2.5.1",
"@silvia-odwyer/photon-node": "0.3.4",
"@openrouter/ai-sdk-provider": "2.9.0",
"ai-gateway-provider": "3.2.0",
"bun-pty": "0.4.8",
"cross-spawn": "catalog:",
Expand All @@ -113,8 +114,8 @@
"google-auth-library": "10.5.0",
"gray-matter": "4.0.3",
"htmlparser2": "8.0.2",
"immer": "11.1.4",
"ignore": "7.0.5",
"immer": "11.1.4",
"jsonc-parser": "3.3.1",
"mime-types": "3.0.2",
"minimatch": "10.2.5",
Expand Down
Loading
Loading