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
9b08a31
Added a Temporal durable-session layer over opencode serve.
moedash Aug 8, 2026
2c0ed2a
Added a Temporal-backed SessionExecution to the v2 engine.
moedash Aug 8, 2026
65644d6
Verified v2 crash recovery and documented Phase 2.
moedash Aug 8, 2026
a07c05b
Typed the SessionExecution active set as SessionID.
moedash Aug 8, 2026
41c0e36
Made resume await the run and surface its error as a RunError.
moedash Aug 8, 2026
119d703
Backed SessionExecution active with Temporal visibility.
moedash Aug 8, 2026
0ee8818
Reconstructed the exact tagged RunError across the Temporal boundary.
moedash Aug 8, 2026
bffcd74
Added a shared, durable event store so any worker resumes any session.
moedash Aug 9, 2026
eb1fc45
Added a per-step Temporal turn (each step its own activity).
moedash Aug 9, 2026
ce0cee4
Made remote libSQL writes atomic via interactive transactions.
moedash Aug 10, 2026
ed0359a
Updated the shared-store caveat for atomic remote writes.
moedash Aug 10, 2026
33b0f6a
Closed interrupted tools on every step re-drive, not just the first.
moedash Aug 10, 2026
57e9bea
Resumed a crashed step from the log instead of re-running its tools.
moedash Aug 10, 2026
d60c89e
Documented the resumable per-step turn in the temporal README.
moedash Aug 10, 2026
945b390
Documented the real cross-host resume boundary.
moedash Aug 10, 2026
ba508e6
Ran the Temporal worker standalone, decoupled from the HTTP server.
moedash Aug 10, 2026
9b6863d
Serialized migrations across processes with a BEGIN IMMEDIATE lock.
moedash Aug 10, 2026
fd1a0e1
Re-ran idempotent tools on resume instead of failing them.
moedash Aug 10, 2026
b77ff56
Bounded the turn's step loop and the repeated-identical-call stuck loop.
moedash Aug 10, 2026
9916d56
Made permission asks durable and replyable from any process.
moedash Aug 10, 2026
a7fea75
Verified networked-libSQL transaction atomicity against a live sqld.
moedash Aug 10, 2026
2f64ae7
Added the worktree-portability design note.
moedash Aug 10, 2026
e6dadee
Recovered wake-driven turns that a re-drive silently dropped.
moedash Aug 10, 2026
a5d759e
Raised the activity backstop to 12 hours; recorded cancellations as s…
moedash Aug 10, 2026
6cd2f60
Keyed tool asks deterministically so re-drives adopt the same row.
moedash Aug 10, 2026
cb15555
Fixed two workflow races around drains and interrupts.
moedash Aug 10, 2026
03c0db4
Hardened resume, interrupt, and active against races and mode drift.
moedash Aug 10, 2026
df6fb0c
Covered libSQL auto-commit statements with the transaction permit.
moedash Aug 10, 2026
82ac2df
Fixed the review's low-severity batch.
moedash Aug 10, 2026
37280d3
Added the AI-399 local-options evaluation draft with measurements.
moedash Aug 10, 2026
a7dc137
Resolved the language-shim reference in the AI-399 draft.
moedash Aug 10, 2026
57e2c7b
Resolved the dev-server redistribution question in the AI-399 draft.
moedash Aug 10, 2026
cb327de
Resolved the desktop signal/update surface in the AI-399 draft.
moedash Aug 10, 2026
628c02b
Finalized the AI-399 evaluation with research findings.
moedash Aug 10, 2026
bc62a90
Defined the appliance archetype on first use.
moedash Aug 11, 2026
2096942
Clarified that the dev-server pairing quantifies the rejected alterna…
moedash Aug 11, 2026
2fa468c
Reframed the ambient-check pattern as the compatibility variant.
moedash Aug 11, 2026
fbfe6e3
Ran the session supervisor in-process with a six-primitive micro-driver.
moedash Aug 11, 2026
bc9202a
Documented the one-supervisor-two-drivers demonstration and the SDK-s…
moedash Aug 11, 2026
d44de70
Collapsed the factory to two modes: local and temporal.
moedash Aug 11, 2026
e0e586e
Dropped the bundled-server pairing from the measurements.
moedash Aug 11, 2026
2f10a04
Replaced raw control bytes in source with escape sequences.
moedash Aug 11, 2026
daa74d5
Guarded permission status transitions; adopted same-process retries.
moedash Aug 11, 2026
0edd1a5
Retried busy errors on the shared store; let reads skip the permit.
moedash Aug 11, 2026
52c4372
Bounded workflow history and closed two idle-boundary wake losses.
moedash Aug 11, 2026
658755c
Served the step-entry checks from one projected-history load.
moedash Aug 11, 2026
981f941
Applied foreign keys best-effort on the shared backend; small hygiene.
moedash Aug 11, 2026
96b49c8
Read the active-session store checks concurrently.
moedash Aug 12, 2026
598a3c8
Typed the user-decline halt across the durable boundary.
moedash Aug 12, 2026
4146c27
Gave the local driver the same 12h drain backstop as Temporal mode.
moedash Aug 12, 2026
8d15027
Swept abandoned pending permission rows; deflaked concurrent open.
moedash Aug 12, 2026
bb3ec94
Retried the cold WAL switch instead of trusting busy_timeout.
moedash Aug 12, 2026
5227d92
Fenced the event log behind the running attempt.
moedash Aug 12, 2026
3c4fbc4
Corrected the remote throughput limit and the fenced-overlap note.
moedash Aug 12, 2026
28aacd1
Moved the serve-wrapper increment to its own branch.
moedash Aug 12, 2026
35d847a
Rebuilt missing worktrees from shared-store snapshot packs.
moedash Aug 12, 2026
b21f64d
Added a how-it-fits-together overview to the README.
moedash Aug 12, 2026
994a9ab
Aligned the docs and scripts with the per-step reality.
moedash Aug 12, 2026
48cd02c
Removed the unreachable whole-turn execution path.
moedash Aug 12, 2026
ec9e4f8
Cleaned the docs' voice; fixed a stale mode count.
moedash Aug 12, 2026
e69e48a
Waited out the catalog's async population in model resolution.
moedash Aug 13, 2026
c174c1c
Added a one-command tmux demo for the temporal mode.
moedash Aug 13, 2026
dbe6b58
Computed the demo's UI URL from the server port.
moedash Aug 13, 2026
3bad481
Said plainly that the driver pane finishing is the expected end state.
moedash Aug 13, 2026
5bcb0eb
Made the packaged CLI carry the temporal client path.
moedash Aug 13, 2026
3d2820c
Waited out the integration state before sending requests without auth.
moedash Aug 13, 2026
261d98a
Added a what-it-is row to the comparison table.
moedash Aug 14, 2026
e8c17a3
Removed a duplicated dependency key.
moedash Aug 14, 2026
8c140c0
Dropped the poll ticker and duration parser from the local driver.
moedash Aug 14, 2026
b5f7711
Rewrote the local session executor as a native coordinator.
jssmith Aug 14, 2026
28ca42b
Parameterized the local executor test into a driver-contract suite.
jssmith Aug 14, 2026
ccc2fe5
Described the two-loop (one-drain) design in comments and README.
jssmith Aug 15, 2026
64a161f
Honored OPENCODE_SESSION_IDLE_TIMEOUT in temporal mode.
moedash Aug 15, 2026
85add01
Pointed the driver-contract suite at the Temporal driver.
moedash Aug 15, 2026
aa73d9a
Replaced the local driver's duration table with effect's Duration.
moedash Aug 15, 2026
1d9103c
Pruned the internal evaluation docs from the branch.
moedash Aug 15, 2026
4acaa94
Split the verification scripts out of the upstream-curated branch.
moedash Aug 15, 2026
f941724
Run local sessions on the proven SessionRunCoordinator.
jssmith Aug 15, 2026
d2b9636
Add a Temporal test harness and fix a condition-cancellation blocker.
jssmith Aug 15, 2026
4add544
Fix the event-log owner token to be unique per activity execution.
jssmith Aug 15, 2026
f8e7141
Surface genuine interrupt-delivery failures instead of swallowing them.
jssmith Aug 15, 2026
f42e1f9
Count all drains toward continue-as-new, including resume-driven ones.
jssmith Aug 15, 2026
b35d627
Document the remaining Temporal supervisor coordination follow-ups.
jssmith Aug 15, 2026
a964bca
Redesign the Temporal supervisor: interrupt keeps serving, resume joins.
jssmith Aug 15, 2026
82d5beb
Test the supervisor redesign: fake-runtime + real-Temporal harness.
jssmith Aug 15, 2026
3ff5202
Update the Temporal supervisor follow-ups doc: deep items now fixed.
jssmith Aug 15, 2026
5cde171
Add a branch summary describing the local pivot and Temporal-mode fixes.
jssmith Aug 15, 2026
c52b967
Correct the branch doc: default prompt delivery is steer and works, n…
jssmith Aug 15, 2026
82e1b69
Carried the idle override and the contract suite through the redesign.
moedash Aug 15, 2026
1aee29e
Promoted the conformance suite and test harness into core's source.
moedash Aug 15, 2026
621951b
Moved the Temporal executor into its own package.
moedash Aug 15, 2026
34f6f83
Collected the executor's protocol and config into their own modules.
moedash Aug 15, 2026
ca618b9
Folded the workflow's inputs into one options record.
moedash Aug 15, 2026
f5e8d31
Renamed the supervisor pieces for what they are now.
moedash Aug 15, 2026
1d23e44
Retold the README around the executor seam.
moedash Aug 15, 2026
777e950
Made the conformance suite's fixture endpoint self-evidently inert.
moedash Aug 16, 2026
df05831
Retired the conformance resume sessions before their run ends.
moedash Aug 16, 2026
86dac84
Followed cross-process turns in the TUI's event projector.
moedash Aug 17, 2026
f85e59a
Stopped fabricating an agent and model on projected user messages.
moedash Aug 17, 2026
f3cd27a
Confirmed settlement with a second read; removed coalesced parts.
moedash Aug 17, 2026
6370983
Updated the branch docs' supervisor paths after the package move.
moedash Aug 17, 2026
259a0f9
Made question asks durable rows, mirroring permission requests.
moedash Aug 17, 2026
289f93b
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
379 changes: 362 additions & 17 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