feat(0.13.0): durable-run substrate - #20

Merged
tangletools merged 3 commits into
mainfrom
feat/durable-runs
May 20, 2026
Merged

feat(0.13.0): durable-run substrate#20
tangletools merged 3 commits into
mainfrom
feat/durable-runs

Conversation

@tangletools

Copy link
Copy Markdown
Contributor

Summary

Production agents need step-level durability — survive worker crashes, deploy rolls, OOM, rate-limit cascades. The 0.12.1 transport-level retry was the floor; this PR is the substrate.

Model directly inspired by Absurd (Postgres) and Cloudflare Workflows: split a run into ordered idempotent steps, persist each result before the next runs, replay from store on resume.

Public surface

```ts
const { result } = await runDurable({
runId: 'chat-session-42', // idempotency key (e.g. session id)
manifest: { projectId, task, input },
store: new FileSystemDurableRunStore(path), // or InMemoryDurableRunStore
taskFn: async (ctx) => {
const payment = await ctx.step('process-payment', async () => { ... })
const ship = await ctx.awaitEvent('shipment.packed', { timeoutMs: 60_000 })
return { payment, tracking: ship.trackingNumber }
},
})
```

Boundary disciplines — fail loud, no silent shortcuts

  • Step results MUST be JSON-serializable (class instances rejected at hash time)
  • Step intents MUST be stable across replays → `DurableRunDivergenceError`
  • Same runId + different inputs → `DurableRunInputMismatchError`
  • Lease conflict → `DurableRunLeaseHeldError`
  • `ctx.now()` / `ctx.uuid()` checkpointed once — replays return cached values
  • Lease renewal heartbeat (every leaseMs/3); lease loss aborts the current step

Ships

  • `DurableRunStore` contract + typed error taxonomy
  • `InMemoryDurableRunStore` (dev)
  • `FileSystemDurableRunStore` (eval harness — one dir per run, append-only steps.jsonl + events.jsonl, atomic-rename run.json + lease.json)
  • `runDurable` wrapper with lease heartbeat + abort propagation
  • `canonicalHash` / `manifestHash` / `stepId` / `deriveWorkerId` helpers

Tests

  • `pnpm typecheck` clean
  • `pnpm test` — 165 / 165 pass (21 new durable tests)
  • Identical test matrix runs against both in-memory + filesystem stores
  • Crash recovery: fail mid-stream, restart, verify completed step NOT re-executed
  • Lease takeover: workerA holds, expires, workerB acquires
  • Concurrent emit race: first-emit-wins enforced
  • awaitEvent + emit concurrent: receives payload mid-flight; replay returns cached
  • Deterministic ctx.now / ctx.uuid stable across replay
  • Manifest hash stable across object insertion order

Follow-ups (separate PRs, queued)

  1. `D1DurableRunStore` + versioned `.sql` migration → Cloudflare prod path
  2. Cloudflare Workflows adapter (each chat turn = a Workflow step, native CF durability)
  3. Wire legal-agent onto the substrate as end-to-end demo

Production agents need step-level durability — survive worker crashes,
deploy rolls, OOM, transient transport errors, rate-limit cascades.
0.12.1's transport-level retry was the floor; this is the substrate.
The model — inspired by Absurd and Cloudflare Workflows — splits a run
into ordered, idempotent **steps**. Each step's result is persisted before
the next runs. On resume, the runner replays prior steps (returning cached
values without re-execution) until it reaches the first unfinished step.
Surface:
ctx.step('process-payment', async () => { ... }) // checkpointed
ctx.awaitEvent('shipment.packed:42') // race-free
ctx.emitEvent('shipment.packed:42', payload) // first-emit-wins
ctx.now() / ctx.uuid() // deterministic
Concurrency: lease-based exclusivity. One worker per run at a time; lease
renewal on heartbeat; takeover after lease expiry. Committed steps survive.
Boundary disciplines (fail-loud, not silent):
- Step results MUST be JSON-serializable — class instances rejected
- Step intents MUST be stable across replays — divergence throws
- Same runId + different manifest hash → DurableRunInputMismatchError
- Step input fingerprints hashed canonically (sorted-key JSON)
Ships:
- DurableRunStore contract + typed error taxonomy
- InMemoryDurableRunStore (dev)
- FileSystemDurableRunStore (eval harness — one dir per run, append-only
steps.jsonl + events.jsonl, atomic-rename run.json + lease.json)
- runDurable(ctx => ...) wrapper with lease heartbeat + abort-signal
propagation + crash-safe failure recording
- canonicalHash + manifestHash + stepId + deriveWorkerId helpers
Follow-ups (separate PRs, queued):
- D1DurableRunStore + versioned .sql migration (Cloudflare prod path)
- Cloudflare Workflows adapter (each turn = a Workflow step)
- Wire one agent (legal) onto the substrate as end-to-end demo
21 new tests covering: fresh runs, replay-skips-fn, manifest mismatch,
step divergence, lease takeover after expiry, awaitEvent race + timeout,
first-emit-wins, deterministic ctx.now/uuid stability across replay. All
21 pass identically against in-memory AND filesystem stores via a shared
test matrix. Total suite: 165 tests, all passing.
Three pieces added on top of the InMemory + FileSystem stores:
1. D1DurableRunStore — the production path for Cloudflare Workers.
- All operations mapped to D1 prepared statements
- Lease takeover via conditional UPDATE (atomic under SQLite locking)
- First-emit-wins enforced by PK (run_id, key) on durable_events
- Structural D1DatabaseLike interface — zero @cloudflare/workers-types dep
- Versioned schema.sql ships in the package (durable_schema_info table)
2. better-sqlite3 in devDependencies — drives the D1DurableRunStore
test matrix against a real SQLite engine. The full 10-test contract
suite now runs identically against InMemory, FileSystem, and D1
(real SQL, real UNIQUE constraints, real CASE expressions). 31 total
tests in the durable matrix.
3. runOnWorkflowStep — Cloudflare Workflows entrypoint adapter that
converts a WorkflowStep into a DurableContext. Each ctx.step
delegates to step.do; ctx.awaitEvent delegates to step.waitForEvent;
ctx.now / ctx.uuid checkpoint through step.do for replay stability.
Pure structural typing — no cloudflare:workers runtime dep.
Deployment patterns (pick one per agent, do not mix):
A. Plain Worker + D1DurableRunStore — default. Survives isolate
restart via D1 lease takeover. Use for chat sessions.
B. Cloudflare Workflows + runOnWorkflowStep — for long tasks
(minutes to hours), platform-managed retries, dashboard
observability. Workflows handles the outer durability.
175 tests pass (was 144).
@tangletools
tangletools merged commit a48cb65 into mainMay 20, 2026
1 check passed
@tangletools
tangletools deleted the feat/durable-runs branch May 20, 2026 14:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tangletools@drewstone
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(0.13.0): durable-run substrate - #20

Merged
tangletools merged 3 commits into
mainfrom
feat/durable-runs
May 20, 2026
Merged

feat(0.13.0): durable-run substrate#20
tangletools merged 3 commits into
mainfrom
feat/durable-runs

Conversation

@tangletools

Copy link
Copy Markdown
Contributor

Summary

Production agents need step-level durability — survive worker crashes, deploy rolls, OOM, rate-limit cascades. The 0.12.1 transport-level retry was the floor; this PR is the substrate.

Model directly inspired by Absurd (Postgres) and Cloudflare Workflows: split a run into ordered idempotent steps, persist each result before the next runs, replay from store on resume.

Public surface

```ts
const { result } = await runDurable({
runId: 'chat-session-42', // idempotency key (e.g. session id)
manifest: { projectId, task, input },
store: new FileSystemDurableRunStore(path), // or InMemoryDurableRunStore
taskFn: async (ctx) => {
const payment = await ctx.step('process-payment', async () => { ... })
const ship = await ctx.awaitEvent('shipment.packed', { timeoutMs: 60_000 })
return { payment, tracking: ship.trackingNumber }
},
})
```

Boundary disciplines — fail loud, no silent shortcuts

  • Step results MUST be JSON-serializable (class instances rejected at hash time)
  • Step intents MUST be stable across replays → `DurableRunDivergenceError`
  • Same runId + different inputs → `DurableRunInputMismatchError`
  • Lease conflict → `DurableRunLeaseHeldError`
  • `ctx.now()` / `ctx.uuid()` checkpointed once — replays return cached values
  • Lease renewal heartbeat (every leaseMs/3); lease loss aborts the current step

Ships

  • `DurableRunStore` contract + typed error taxonomy
  • `InMemoryDurableRunStore` (dev)
  • `FileSystemDurableRunStore` (eval harness — one dir per run, append-only steps.jsonl + events.jsonl, atomic-rename run.json + lease.json)
  • `runDurable` wrapper with lease heartbeat + abort propagation
  • `canonicalHash` / `manifestHash` / `stepId` / `deriveWorkerId` helpers

Tests

  • `pnpm typecheck` clean
  • `pnpm test` — 165 / 165 pass (21 new durable tests)
  • Identical test matrix runs against both in-memory + filesystem stores
  • Crash recovery: fail mid-stream, restart, verify completed step NOT re-executed
  • Lease takeover: workerA holds, expires, workerB acquires
  • Concurrent emit race: first-emit-wins enforced
  • awaitEvent + emit concurrent: receives payload mid-flight; replay returns cached
  • Deterministic ctx.now / ctx.uuid stable across replay
  • Manifest hash stable across object insertion order

Follow-ups (separate PRs, queued)

  1. `D1DurableRunStore` + versioned `.sql` migration → Cloudflare prod path
  2. Cloudflare Workflows adapter (each chat turn = a Workflow step, native CF durability)
  3. Wire legal-agent onto the substrate as end-to-end demo

Production agents need step-level durability — survive worker crashes,
deploy rolls, OOM, transient transport errors, rate-limit cascades.
0.12.1's transport-level retry was the floor; this is the substrate.
The model — inspired by Absurd and Cloudflare Workflows — splits a run
into ordered, idempotent **steps**. Each step's result is persisted before
the next runs. On resume, the runner replays prior steps (returning cached
values without re-execution) until it reaches the first unfinished step.
Surface:
ctx.step('process-payment', async () => { ... }) // checkpointed
ctx.awaitEvent('shipment.packed:42') // race-free
ctx.emitEvent('shipment.packed:42', payload) // first-emit-wins
ctx.now() / ctx.uuid() // deterministic
Concurrency: lease-based exclusivity. One worker per run at a time; lease
renewal on heartbeat; takeover after lease expiry. Committed steps survive.
Boundary disciplines (fail-loud, not silent):
- Step results MUST be JSON-serializable — class instances rejected
- Step intents MUST be stable across replays — divergence throws
- Same runId + different manifest hash → DurableRunInputMismatchError
- Step input fingerprints hashed canonically (sorted-key JSON)
Ships:
- DurableRunStore contract + typed error taxonomy
- InMemoryDurableRunStore (dev)
- FileSystemDurableRunStore (eval harness — one dir per run, append-only
steps.jsonl + events.jsonl, atomic-rename run.json + lease.json)
- runDurable(ctx => ...) wrapper with lease heartbeat + abort-signal
propagation + crash-safe failure recording
- canonicalHash + manifestHash + stepId + deriveWorkerId helpers
Follow-ups (separate PRs, queued):
- D1DurableRunStore + versioned .sql migration (Cloudflare prod path)
- Cloudflare Workflows adapter (each turn = a Workflow step)
- Wire one agent (legal) onto the substrate as end-to-end demo
21 new tests covering: fresh runs, replay-skips-fn, manifest mismatch,
step divergence, lease takeover after expiry, awaitEvent race + timeout,
first-emit-wins, deterministic ctx.now/uuid stability across replay. All
21 pass identically against in-memory AND filesystem stores via a shared
test matrix. Total suite: 165 tests, all passing.
Three pieces added on top of the InMemory + FileSystem stores:
1. D1DurableRunStore — the production path for Cloudflare Workers.
- All operations mapped to D1 prepared statements
- Lease takeover via conditional UPDATE (atomic under SQLite locking)
- First-emit-wins enforced by PK (run_id, key) on durable_events
- Structural D1DatabaseLike interface — zero @cloudflare/workers-types dep
- Versioned schema.sql ships in the package (durable_schema_info table)
2. better-sqlite3 in devDependencies — drives the D1DurableRunStore
test matrix against a real SQLite engine. The full 10-test contract
suite now runs identically against InMemory, FileSystem, and D1
(real SQL, real UNIQUE constraints, real CASE expressions). 31 total
tests in the durable matrix.
3. runOnWorkflowStep — Cloudflare Workflows entrypoint adapter that
converts a WorkflowStep into a DurableContext. Each ctx.step
delegates to step.do; ctx.awaitEvent delegates to step.waitForEvent;
ctx.now / ctx.uuid checkpoint through step.do for replay stability.
Pure structural typing — no cloudflare:workers runtime dep.
Deployment patterns (pick one per agent, do not mix):
A. Plain Worker + D1DurableRunStore — default. Survives isolate
restart via D1 lease takeover. Use for chat sessions.
B. Cloudflare Workflows + runOnWorkflowStep — for long tasks
(minutes to hours), platform-managed retries, dashboard
observability. Workflows handles the outer durability.
175 tests pass (was 144).
@tangletools
tangletools merged commit a48cb65 into mainMay 20, 2026
1 check passed
@tangletools
tangletools deleted the feat/durable-runs branch May 20, 2026 14:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tangletools@drewstone
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(0.13.0): durable-run substrate - #20

Merged
tangletools merged 3 commits into
mainfrom
feat/durable-runs
May 20, 2026
Merged

feat(0.13.0): durable-run substrate#20
tangletools merged 3 commits into
mainfrom
feat/durable-runs

Conversation

@tangletools

Copy link
Copy Markdown
Contributor

Summary

Production agents need step-level durability — survive worker crashes, deploy rolls, OOM, rate-limit cascades. The 0.12.1 transport-level retry was the floor; this PR is the substrate.

Model directly inspired by Absurd (Postgres) and Cloudflare Workflows: split a run into ordered idempotent steps, persist each result before the next runs, replay from store on resume.

Public surface

```ts
const { result } = await runDurable({
runId: 'chat-session-42', // idempotency key (e.g. session id)
manifest: { projectId, task, input },
store: new FileSystemDurableRunStore(path), // or InMemoryDurableRunStore
taskFn: async (ctx) => {
const payment = await ctx.step('process-payment', async () => { ... })
const ship = await ctx.awaitEvent('shipment.packed', { timeoutMs: 60_000 })
return { payment, tracking: ship.trackingNumber }
},
})
```

Boundary disciplines — fail loud, no silent shortcuts

  • Step results MUST be JSON-serializable (class instances rejected at hash time)
  • Step intents MUST be stable across replays → `DurableRunDivergenceError`
  • Same runId + different inputs → `DurableRunInputMismatchError`
  • Lease conflict → `DurableRunLeaseHeldError`
  • `ctx.now()` / `ctx.uuid()` checkpointed once — replays return cached values
  • Lease renewal heartbeat (every leaseMs/3); lease loss aborts the current step

Ships

  • `DurableRunStore` contract + typed error taxonomy
  • `InMemoryDurableRunStore` (dev)
  • `FileSystemDurableRunStore` (eval harness — one dir per run, append-only steps.jsonl + events.jsonl, atomic-rename run.json + lease.json)
  • `runDurable` wrapper with lease heartbeat + abort propagation
  • `canonicalHash` / `manifestHash` / `stepId` / `deriveWorkerId` helpers

Tests

  • `pnpm typecheck` clean
  • `pnpm test` — 165 / 165 pass (21 new durable tests)
  • Identical test matrix runs against both in-memory + filesystem stores
  • Crash recovery: fail mid-stream, restart, verify completed step NOT re-executed
  • Lease takeover: workerA holds, expires, workerB acquires
  • Concurrent emit race: first-emit-wins enforced
  • awaitEvent + emit concurrent: receives payload mid-flight; replay returns cached
  • Deterministic ctx.now / ctx.uuid stable across replay
  • Manifest hash stable across object insertion order

Follow-ups (separate PRs, queued)

  1. `D1DurableRunStore` + versioned `.sql` migration → Cloudflare prod path
  2. Cloudflare Workflows adapter (each chat turn = a Workflow step, native CF durability)
  3. Wire legal-agent onto the substrate as end-to-end demo

Production agents need step-level durability — survive worker crashes,
deploy rolls, OOM, transient transport errors, rate-limit cascades.
0.12.1's transport-level retry was the floor; this is the substrate.
The model — inspired by Absurd and Cloudflare Workflows — splits a run
into ordered, idempotent **steps**. Each step's result is persisted before
the next runs. On resume, the runner replays prior steps (returning cached
values without re-execution) until it reaches the first unfinished step.
Surface:
ctx.step('process-payment', async () => { ... }) // checkpointed
ctx.awaitEvent('shipment.packed:42') // race-free
ctx.emitEvent('shipment.packed:42', payload) // first-emit-wins
ctx.now() / ctx.uuid() // deterministic
Concurrency: lease-based exclusivity. One worker per run at a time; lease
renewal on heartbeat; takeover after lease expiry. Committed steps survive.
Boundary disciplines (fail-loud, not silent):
- Step results MUST be JSON-serializable — class instances rejected
- Step intents MUST be stable across replays — divergence throws
- Same runId + different manifest hash → DurableRunInputMismatchError
- Step input fingerprints hashed canonically (sorted-key JSON)
Ships:
- DurableRunStore contract + typed error taxonomy
- InMemoryDurableRunStore (dev)
- FileSystemDurableRunStore (eval harness — one dir per run, append-only
steps.jsonl + events.jsonl, atomic-rename run.json + lease.json)
- runDurable(ctx => ...) wrapper with lease heartbeat + abort-signal
propagation + crash-safe failure recording
- canonicalHash + manifestHash + stepId + deriveWorkerId helpers
Follow-ups (separate PRs, queued):
- D1DurableRunStore + versioned .sql migration (Cloudflare prod path)
- Cloudflare Workflows adapter (each turn = a Workflow step)
- Wire one agent (legal) onto the substrate as end-to-end demo
21 new tests covering: fresh runs, replay-skips-fn, manifest mismatch,
step divergence, lease takeover after expiry, awaitEvent race + timeout,
first-emit-wins, deterministic ctx.now/uuid stability across replay. All
21 pass identically against in-memory AND filesystem stores via a shared
test matrix. Total suite: 165 tests, all passing.
Three pieces added on top of the InMemory + FileSystem stores:
1. D1DurableRunStore — the production path for Cloudflare Workers.
- All operations mapped to D1 prepared statements
- Lease takeover via conditional UPDATE (atomic under SQLite locking)
- First-emit-wins enforced by PK (run_id, key) on durable_events
- Structural D1DatabaseLike interface — zero @cloudflare/workers-types dep
- Versioned schema.sql ships in the package (durable_schema_info table)
2. better-sqlite3 in devDependencies — drives the D1DurableRunStore
test matrix against a real SQLite engine. The full 10-test contract
suite now runs identically against InMemory, FileSystem, and D1
(real SQL, real UNIQUE constraints, real CASE expressions). 31 total
tests in the durable matrix.
3. runOnWorkflowStep — Cloudflare Workflows entrypoint adapter that
converts a WorkflowStep into a DurableContext. Each ctx.step
delegates to step.do; ctx.awaitEvent delegates to step.waitForEvent;
ctx.now / ctx.uuid checkpoint through step.do for replay stability.
Pure structural typing — no cloudflare:workers runtime dep.
Deployment patterns (pick one per agent, do not mix):
A. Plain Worker + D1DurableRunStore — default. Survives isolate
restart via D1 lease takeover. Use for chat sessions.
B. Cloudflare Workflows + runOnWorkflowStep — for long tasks
(minutes to hours), platform-managed retries, dashboard
observability. Workflows handles the outer durability.
175 tests pass (was 144).
@tangletools
tangletools merged commit a48cb65 into mainMay 20, 2026
1 check passed
@tangletools
tangletools deleted the feat/durable-runs branch May 20, 2026 14:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tangletools@drewstone
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(0.13.0): durable-run substrate - #20

Merged
tangletools merged 3 commits into
mainfrom
feat/durable-runs
May 20, 2026
Merged

feat(0.13.0): durable-run substrate#20
tangletools merged 3 commits into
mainfrom
feat/durable-runs

Conversation

@tangletools

Copy link
Copy Markdown
Contributor

Summary

Production agents need step-level durability — survive worker crashes, deploy rolls, OOM, rate-limit cascades. The 0.12.1 transport-level retry was the floor; this PR is the substrate.

Model directly inspired by Absurd (Postgres) and Cloudflare Workflows: split a run into ordered idempotent steps, persist each result before the next runs, replay from store on resume.

Public surface

```ts
const { result } = await runDurable({
runId: 'chat-session-42', // idempotency key (e.g. session id)
manifest: { projectId, task, input },
store: new FileSystemDurableRunStore(path), // or InMemoryDurableRunStore
taskFn: async (ctx) => {
const payment = await ctx.step('process-payment', async () => { ... })
const ship = await ctx.awaitEvent('shipment.packed', { timeoutMs: 60_000 })
return { payment, tracking: ship.trackingNumber }
},
})
```

Boundary disciplines — fail loud, no silent shortcuts

  • Step results MUST be JSON-serializable (class instances rejected at hash time)
  • Step intents MUST be stable across replays → `DurableRunDivergenceError`
  • Same runId + different inputs → `DurableRunInputMismatchError`
  • Lease conflict → `DurableRunLeaseHeldError`
  • `ctx.now()` / `ctx.uuid()` checkpointed once — replays return cached values
  • Lease renewal heartbeat (every leaseMs/3); lease loss aborts the current step

Ships

  • `DurableRunStore` contract + typed error taxonomy
  • `InMemoryDurableRunStore` (dev)
  • `FileSystemDurableRunStore` (eval harness — one dir per run, append-only steps.jsonl + events.jsonl, atomic-rename run.json + lease.json)
  • `runDurable` wrapper with lease heartbeat + abort propagation
  • `canonicalHash` / `manifestHash` / `stepId` / `deriveWorkerId` helpers

Tests

  • `pnpm typecheck` clean
  • `pnpm test` — 165 / 165 pass (21 new durable tests)
  • Identical test matrix runs against both in-memory + filesystem stores
  • Crash recovery: fail mid-stream, restart, verify completed step NOT re-executed
  • Lease takeover: workerA holds, expires, workerB acquires
  • Concurrent emit race: first-emit-wins enforced
  • awaitEvent + emit concurrent: receives payload mid-flight; replay returns cached
  • Deterministic ctx.now / ctx.uuid stable across replay
  • Manifest hash stable across object insertion order

Follow-ups (separate PRs, queued)

  1. `D1DurableRunStore` + versioned `.sql` migration → Cloudflare prod path
  2. Cloudflare Workflows adapter (each chat turn = a Workflow step, native CF durability)
  3. Wire legal-agent onto the substrate as end-to-end demo

Production agents need step-level durability — survive worker crashes,
deploy rolls, OOM, transient transport errors, rate-limit cascades.
0.12.1's transport-level retry was the floor; this is the substrate.
The model — inspired by Absurd and Cloudflare Workflows — splits a run
into ordered, idempotent **steps**. Each step's result is persisted before
the next runs. On resume, the runner replays prior steps (returning cached
values without re-execution) until it reaches the first unfinished step.
Surface:
ctx.step('process-payment', async () => { ... }) // checkpointed
ctx.awaitEvent('shipment.packed:42') // race-free
ctx.emitEvent('shipment.packed:42', payload) // first-emit-wins
ctx.now() / ctx.uuid() // deterministic
Concurrency: lease-based exclusivity. One worker per run at a time; lease
renewal on heartbeat; takeover after lease expiry. Committed steps survive.
Boundary disciplines (fail-loud, not silent):
- Step results MUST be JSON-serializable — class instances rejected
- Step intents MUST be stable across replays — divergence throws
- Same runId + different manifest hash → DurableRunInputMismatchError
- Step input fingerprints hashed canonically (sorted-key JSON)
Ships:
- DurableRunStore contract + typed error taxonomy
- InMemoryDurableRunStore (dev)
- FileSystemDurableRunStore (eval harness — one dir per run, append-only
steps.jsonl + events.jsonl, atomic-rename run.json + lease.json)
- runDurable(ctx => ...) wrapper with lease heartbeat + abort-signal
propagation + crash-safe failure recording
- canonicalHash + manifestHash + stepId + deriveWorkerId helpers
Follow-ups (separate PRs, queued):
- D1DurableRunStore + versioned .sql migration (Cloudflare prod path)
- Cloudflare Workflows adapter (each turn = a Workflow step)
- Wire one agent (legal) onto the substrate as end-to-end demo
21 new tests covering: fresh runs, replay-skips-fn, manifest mismatch,
step divergence, lease takeover after expiry, awaitEvent race + timeout,
first-emit-wins, deterministic ctx.now/uuid stability across replay. All
21 pass identically against in-memory AND filesystem stores via a shared
test matrix. Total suite: 165 tests, all passing.
Three pieces added on top of the InMemory + FileSystem stores:
1. D1DurableRunStore — the production path for Cloudflare Workers.
- All operations mapped to D1 prepared statements
- Lease takeover via conditional UPDATE (atomic under SQLite locking)
- First-emit-wins enforced by PK (run_id, key) on durable_events
- Structural D1DatabaseLike interface — zero @cloudflare/workers-types dep
- Versioned schema.sql ships in the package (durable_schema_info table)
2. better-sqlite3 in devDependencies — drives the D1DurableRunStore
test matrix against a real SQLite engine. The full 10-test contract
suite now runs identically against InMemory, FileSystem, and D1
(real SQL, real UNIQUE constraints, real CASE expressions). 31 total
tests in the durable matrix.
3. runOnWorkflowStep — Cloudflare Workflows entrypoint adapter that
converts a WorkflowStep into a DurableContext. Each ctx.step
delegates to step.do; ctx.awaitEvent delegates to step.waitForEvent;
ctx.now / ctx.uuid checkpoint through step.do for replay stability.
Pure structural typing — no cloudflare:workers runtime dep.
Deployment patterns (pick one per agent, do not mix):
A. Plain Worker + D1DurableRunStore — default. Survives isolate
restart via D1 lease takeover. Use for chat sessions.
B. Cloudflare Workflows + runOnWorkflowStep — for long tasks
(minutes to hours), platform-managed retries, dashboard
observability. Workflows handles the outer durability.
175 tests pass (was 144).
@tangletools
tangletools merged commit a48cb65 into mainMay 20, 2026
1 check passed
@tangletools
tangletools deleted the feat/durable-runs branch May 20, 2026 14:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tangletools@drewstone
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(0.13.0): durable-run substrate - #20

Merged
tangletools merged 3 commits into
mainfrom
feat/durable-runs
May 20, 2026
Merged

feat(0.13.0): durable-run substrate#20
tangletools merged 3 commits into
mainfrom
feat/durable-runs

Conversation

@tangletools

Copy link
Copy Markdown
Contributor

Summary

Production agents need step-level durability — survive worker crashes, deploy rolls, OOM, rate-limit cascades. The 0.12.1 transport-level retry was the floor; this PR is the substrate.

Model directly inspired by Absurd (Postgres) and Cloudflare Workflows: split a run into ordered idempotent steps, persist each result before the next runs, replay from store on resume.

Public surface

```ts
const { result } = await runDurable({
runId: 'chat-session-42', // idempotency key (e.g. session id)
manifest: { projectId, task, input },
store: new FileSystemDurableRunStore(path), // or InMemoryDurableRunStore
taskFn: async (ctx) => {
const payment = await ctx.step('process-payment', async () => { ... })
const ship = await ctx.awaitEvent('shipment.packed', { timeoutMs: 60_000 })
return { payment, tracking: ship.trackingNumber }
},
})
```

Boundary disciplines — fail loud, no silent shortcuts

  • Step results MUST be JSON-serializable (class instances rejected at hash time)
  • Step intents MUST be stable across replays → `DurableRunDivergenceError`
  • Same runId + different inputs → `DurableRunInputMismatchError`
  • Lease conflict → `DurableRunLeaseHeldError`
  • `ctx.now()` / `ctx.uuid()` checkpointed once — replays return cached values
  • Lease renewal heartbeat (every leaseMs/3); lease loss aborts the current step

Ships

  • `DurableRunStore` contract + typed error taxonomy
  • `InMemoryDurableRunStore` (dev)
  • `FileSystemDurableRunStore` (eval harness — one dir per run, append-only steps.jsonl + events.jsonl, atomic-rename run.json + lease.json)
  • `runDurable` wrapper with lease heartbeat + abort propagation
  • `canonicalHash` / `manifestHash` / `stepId` / `deriveWorkerId` helpers

Tests

  • `pnpm typecheck` clean
  • `pnpm test` — 165 / 165 pass (21 new durable tests)
  • Identical test matrix runs against both in-memory + filesystem stores
  • Crash recovery: fail mid-stream, restart, verify completed step NOT re-executed
  • Lease takeover: workerA holds, expires, workerB acquires
  • Concurrent emit race: first-emit-wins enforced
  • awaitEvent + emit concurrent: receives payload mid-flight; replay returns cached
  • Deterministic ctx.now / ctx.uuid stable across replay
  • Manifest hash stable across object insertion order

Follow-ups (separate PRs, queued)

  1. `D1DurableRunStore` + versioned `.sql` migration → Cloudflare prod path
  2. Cloudflare Workflows adapter (each chat turn = a Workflow step, native CF durability)
  3. Wire legal-agent onto the substrate as end-to-end demo

Production agents need step-level durability — survive worker crashes,
deploy rolls, OOM, transient transport errors, rate-limit cascades.
0.12.1's transport-level retry was the floor; this is the substrate.
The model — inspired by Absurd and Cloudflare Workflows — splits a run
into ordered, idempotent **steps**. Each step's result is persisted before
the next runs. On resume, the runner replays prior steps (returning cached
values without re-execution) until it reaches the first unfinished step.
Surface:
ctx.step('process-payment', async () => { ... }) // checkpointed
ctx.awaitEvent('shipment.packed:42') // race-free
ctx.emitEvent('shipment.packed:42', payload) // first-emit-wins
ctx.now() / ctx.uuid() // deterministic
Concurrency: lease-based exclusivity. One worker per run at a time; lease
renewal on heartbeat; takeover after lease expiry. Committed steps survive.
Boundary disciplines (fail-loud, not silent):
- Step results MUST be JSON-serializable — class instances rejected
- Step intents MUST be stable across replays — divergence throws
- Same runId + different manifest hash → DurableRunInputMismatchError
- Step input fingerprints hashed canonically (sorted-key JSON)
Ships:
- DurableRunStore contract + typed error taxonomy
- InMemoryDurableRunStore (dev)
- FileSystemDurableRunStore (eval harness — one dir per run, append-only
steps.jsonl + events.jsonl, atomic-rename run.json + lease.json)
- runDurable(ctx => ...) wrapper with lease heartbeat + abort-signal
propagation + crash-safe failure recording
- canonicalHash + manifestHash + stepId + deriveWorkerId helpers
Follow-ups (separate PRs, queued):
- D1DurableRunStore + versioned .sql migration (Cloudflare prod path)
- Cloudflare Workflows adapter (each turn = a Workflow step)
- Wire one agent (legal) onto the substrate as end-to-end demo
21 new tests covering: fresh runs, replay-skips-fn, manifest mismatch,
step divergence, lease takeover after expiry, awaitEvent race + timeout,
first-emit-wins, deterministic ctx.now/uuid stability across replay. All
21 pass identically against in-memory AND filesystem stores via a shared
test matrix. Total suite: 165 tests, all passing.
Three pieces added on top of the InMemory + FileSystem stores:
1. D1DurableRunStore — the production path for Cloudflare Workers.
- All operations mapped to D1 prepared statements
- Lease takeover via conditional UPDATE (atomic under SQLite locking)
- First-emit-wins enforced by PK (run_id, key) on durable_events
- Structural D1DatabaseLike interface — zero @cloudflare/workers-types dep
- Versioned schema.sql ships in the package (durable_schema_info table)
2. better-sqlite3 in devDependencies — drives the D1DurableRunStore
test matrix against a real SQLite engine. The full 10-test contract
suite now runs identically against InMemory, FileSystem, and D1
(real SQL, real UNIQUE constraints, real CASE expressions). 31 total
tests in the durable matrix.
3. runOnWorkflowStep — Cloudflare Workflows entrypoint adapter that
converts a WorkflowStep into a DurableContext. Each ctx.step
delegates to step.do; ctx.awaitEvent delegates to step.waitForEvent;
ctx.now / ctx.uuid checkpoint through step.do for replay stability.
Pure structural typing — no cloudflare:workers runtime dep.
Deployment patterns (pick one per agent, do not mix):
A. Plain Worker + D1DurableRunStore — default. Survives isolate
restart via D1 lease takeover. Use for chat sessions.
B. Cloudflare Workflows + runOnWorkflowStep — for long tasks
(minutes to hours), platform-managed retries, dashboard
observability. Workflows handles the outer durability.
175 tests pass (was 144).
@tangletools
tangletools merged commit a48cb65 into mainMay 20, 2026
1 check passed
@tangletools
tangletools deleted the feat/durable-runs branch May 20, 2026 14:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tangletools@drewstone
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(0.13.0): durable-run substrate - #20

Merged
tangletools merged 3 commits into
mainfrom
feat/durable-runs
May 20, 2026
Merged

feat(0.13.0): durable-run substrate#20
tangletools merged 3 commits into
mainfrom
feat/durable-runs

Conversation

@tangletools

Copy link
Copy Markdown
Contributor

Summary

Production agents need step-level durability — survive worker crashes, deploy rolls, OOM, rate-limit cascades. The 0.12.1 transport-level retry was the floor; this PR is the substrate.

Model directly inspired by Absurd (Postgres) and Cloudflare Workflows: split a run into ordered idempotent steps, persist each result before the next runs, replay from store on resume.

Public surface

```ts
const { result } = await runDurable({
runId: 'chat-session-42', // idempotency key (e.g. session id)
manifest: { projectId, task, input },
store: new FileSystemDurableRunStore(path), // or InMemoryDurableRunStore
taskFn: async (ctx) => {
const payment = await ctx.step('process-payment', async () => { ... })
const ship = await ctx.awaitEvent('shipment.packed', { timeoutMs: 60_000 })
return { payment, tracking: ship.trackingNumber }
},
})
```

Boundary disciplines — fail loud, no silent shortcuts

  • Step results MUST be JSON-serializable (class instances rejected at hash time)
  • Step intents MUST be stable across replays → `DurableRunDivergenceError`
  • Same runId + different inputs → `DurableRunInputMismatchError`
  • Lease conflict → `DurableRunLeaseHeldError`
  • `ctx.now()` / `ctx.uuid()` checkpointed once — replays return cached values
  • Lease renewal heartbeat (every leaseMs/3); lease loss aborts the current step

Ships

  • `DurableRunStore` contract + typed error taxonomy
  • `InMemoryDurableRunStore` (dev)
  • `FileSystemDurableRunStore` (eval harness — one dir per run, append-only steps.jsonl + events.jsonl, atomic-rename run.json + lease.json)
  • `runDurable` wrapper with lease heartbeat + abort propagation
  • `canonicalHash` / `manifestHash` / `stepId` / `deriveWorkerId` helpers

Tests

  • `pnpm typecheck` clean
  • `pnpm test` — 165 / 165 pass (21 new durable tests)
  • Identical test matrix runs against both in-memory + filesystem stores
  • Crash recovery: fail mid-stream, restart, verify completed step NOT re-executed
  • Lease takeover: workerA holds, expires, workerB acquires
  • Concurrent emit race: first-emit-wins enforced
  • awaitEvent + emit concurrent: receives payload mid-flight; replay returns cached
  • Deterministic ctx.now / ctx.uuid stable across replay
  • Manifest hash stable across object insertion order

Follow-ups (separate PRs, queued)

  1. `D1DurableRunStore` + versioned `.sql` migration → Cloudflare prod path
  2. Cloudflare Workflows adapter (each chat turn = a Workflow step, native CF durability)
  3. Wire legal-agent onto the substrate as end-to-end demo

Production agents need step-level durability — survive worker crashes,
deploy rolls, OOM, transient transport errors, rate-limit cascades.
0.12.1's transport-level retry was the floor; this is the substrate.
The model — inspired by Absurd and Cloudflare Workflows — splits a run
into ordered, idempotent **steps**. Each step's result is persisted before
the next runs. On resume, the runner replays prior steps (returning cached
values without re-execution) until it reaches the first unfinished step.
Surface:
ctx.step('process-payment', async () => { ... }) // checkpointed
ctx.awaitEvent('shipment.packed:42') // race-free
ctx.emitEvent('shipment.packed:42', payload) // first-emit-wins
ctx.now() / ctx.uuid() // deterministic
Concurrency: lease-based exclusivity. One worker per run at a time; lease
renewal on heartbeat; takeover after lease expiry. Committed steps survive.
Boundary disciplines (fail-loud, not silent):
- Step results MUST be JSON-serializable — class instances rejected
- Step intents MUST be stable across replays — divergence throws
- Same runId + different manifest hash → DurableRunInputMismatchError
- Step input fingerprints hashed canonically (sorted-key JSON)
Ships:
- DurableRunStore contract + typed error taxonomy
- InMemoryDurableRunStore (dev)
- FileSystemDurableRunStore (eval harness — one dir per run, append-only
steps.jsonl + events.jsonl, atomic-rename run.json + lease.json)
- runDurable(ctx => ...) wrapper with lease heartbeat + abort-signal
propagation + crash-safe failure recording
- canonicalHash + manifestHash + stepId + deriveWorkerId helpers
Follow-ups (separate PRs, queued):
- D1DurableRunStore + versioned .sql migration (Cloudflare prod path)
- Cloudflare Workflows adapter (each turn = a Workflow step)
- Wire one agent (legal) onto the substrate as end-to-end demo
21 new tests covering: fresh runs, replay-skips-fn, manifest mismatch,
step divergence, lease takeover after expiry, awaitEvent race + timeout,
first-emit-wins, deterministic ctx.now/uuid stability across replay. All
21 pass identically against in-memory AND filesystem stores via a shared
test matrix. Total suite: 165 tests, all passing.
Three pieces added on top of the InMemory + FileSystem stores:
1. D1DurableRunStore — the production path for Cloudflare Workers.
- All operations mapped to D1 prepared statements
- Lease takeover via conditional UPDATE (atomic under SQLite locking)
- First-emit-wins enforced by PK (run_id, key) on durable_events
- Structural D1DatabaseLike interface — zero @cloudflare/workers-types dep
- Versioned schema.sql ships in the package (durable_schema_info table)
2. better-sqlite3 in devDependencies — drives the D1DurableRunStore
test matrix against a real SQLite engine. The full 10-test contract
suite now runs identically against InMemory, FileSystem, and D1
(real SQL, real UNIQUE constraints, real CASE expressions). 31 total
tests in the durable matrix.
3. runOnWorkflowStep — Cloudflare Workflows entrypoint adapter that
converts a WorkflowStep into a DurableContext. Each ctx.step
delegates to step.do; ctx.awaitEvent delegates to step.waitForEvent;
ctx.now / ctx.uuid checkpoint through step.do for replay stability.
Pure structural typing — no cloudflare:workers runtime dep.
Deployment patterns (pick one per agent, do not mix):
A. Plain Worker + D1DurableRunStore — default. Survives isolate
restart via D1 lease takeover. Use for chat sessions.
B. Cloudflare Workflows + runOnWorkflowStep — for long tasks
(minutes to hours), platform-managed retries, dashboard
observability. Workflows handles the outer durability.
175 tests pass (was 144).
@tangletools
tangletools merged commit a48cb65 into mainMay 20, 2026
1 check passed
@tangletools
tangletools deleted the feat/durable-runs branch May 20, 2026 14:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tangletools@drewstone
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(0.13.0): durable-run substrate - #20

Merged
tangletools merged 3 commits into
mainfrom
feat/durable-runs
May 20, 2026
Merged

feat(0.13.0): durable-run substrate#20
tangletools merged 3 commits into
mainfrom
feat/durable-runs

Conversation

@tangletools

Copy link
Copy Markdown
Contributor

Summary

Production agents need step-level durability — survive worker crashes, deploy rolls, OOM, rate-limit cascades. The 0.12.1 transport-level retry was the floor; this PR is the substrate.

Model directly inspired by Absurd (Postgres) and Cloudflare Workflows: split a run into ordered idempotent steps, persist each result before the next runs, replay from store on resume.

Public surface

```ts
const { result } = await runDurable({
runId: 'chat-session-42', // idempotency key (e.g. session id)
manifest: { projectId, task, input },
store: new FileSystemDurableRunStore(path), // or InMemoryDurableRunStore
taskFn: async (ctx) => {
const payment = await ctx.step('process-payment', async () => { ... })
const ship = await ctx.awaitEvent('shipment.packed', { timeoutMs: 60_000 })
return { payment, tracking: ship.trackingNumber }
},
})
```

Boundary disciplines — fail loud, no silent shortcuts

  • Step results MUST be JSON-serializable (class instances rejected at hash time)
  • Step intents MUST be stable across replays → `DurableRunDivergenceError`
  • Same runId + different inputs → `DurableRunInputMismatchError`
  • Lease conflict → `DurableRunLeaseHeldError`
  • `ctx.now()` / `ctx.uuid()` checkpointed once — replays return cached values
  • Lease renewal heartbeat (every leaseMs/3); lease loss aborts the current step

Ships

  • `DurableRunStore` contract + typed error taxonomy
  • `InMemoryDurableRunStore` (dev)
  • `FileSystemDurableRunStore` (eval harness — one dir per run, append-only steps.jsonl + events.jsonl, atomic-rename run.json + lease.json)
  • `runDurable` wrapper with lease heartbeat + abort propagation
  • `canonicalHash` / `manifestHash` / `stepId` / `deriveWorkerId` helpers

Tests

  • `pnpm typecheck` clean
  • `pnpm test` — 165 / 165 pass (21 new durable tests)
  • Identical test matrix runs against both in-memory + filesystem stores
  • Crash recovery: fail mid-stream, restart, verify completed step NOT re-executed
  • Lease takeover: workerA holds, expires, workerB acquires
  • Concurrent emit race: first-emit-wins enforced
  • awaitEvent + emit concurrent: receives payload mid-flight; replay returns cached
  • Deterministic ctx.now / ctx.uuid stable across replay
  • Manifest hash stable across object insertion order

Follow-ups (separate PRs, queued)

  1. `D1DurableRunStore` + versioned `.sql` migration → Cloudflare prod path
  2. Cloudflare Workflows adapter (each chat turn = a Workflow step, native CF durability)
  3. Wire legal-agent onto the substrate as end-to-end demo

Production agents need step-level durability — survive worker crashes,
deploy rolls, OOM, transient transport errors, rate-limit cascades.
0.12.1's transport-level retry was the floor; this is the substrate.
The model — inspired by Absurd and Cloudflare Workflows — splits a run
into ordered, idempotent **steps**. Each step's result is persisted before
the next runs. On resume, the runner replays prior steps (returning cached
values without re-execution) until it reaches the first unfinished step.
Surface:
ctx.step('process-payment', async () => { ... }) // checkpointed
ctx.awaitEvent('shipment.packed:42') // race-free
ctx.emitEvent('shipment.packed:42', payload) // first-emit-wins
ctx.now() / ctx.uuid() // deterministic
Concurrency: lease-based exclusivity. One worker per run at a time; lease
renewal on heartbeat; takeover after lease expiry. Committed steps survive.
Boundary disciplines (fail-loud, not silent):
- Step results MUST be JSON-serializable — class instances rejected
- Step intents MUST be stable across replays — divergence throws
- Same runId + different manifest hash → DurableRunInputMismatchError
- Step input fingerprints hashed canonically (sorted-key JSON)
Ships:
- DurableRunStore contract + typed error taxonomy
- InMemoryDurableRunStore (dev)
- FileSystemDurableRunStore (eval harness — one dir per run, append-only
steps.jsonl + events.jsonl, atomic-rename run.json + lease.json)
- runDurable(ctx => ...) wrapper with lease heartbeat + abort-signal
propagation + crash-safe failure recording
- canonicalHash + manifestHash + stepId + deriveWorkerId helpers
Follow-ups (separate PRs, queued):
- D1DurableRunStore + versioned .sql migration (Cloudflare prod path)
- Cloudflare Workflows adapter (each turn = a Workflow step)
- Wire one agent (legal) onto the substrate as end-to-end demo
21 new tests covering: fresh runs, replay-skips-fn, manifest mismatch,
step divergence, lease takeover after expiry, awaitEvent race + timeout,
first-emit-wins, deterministic ctx.now/uuid stability across replay. All
21 pass identically against in-memory AND filesystem stores via a shared
test matrix. Total suite: 165 tests, all passing.
Three pieces added on top of the InMemory + FileSystem stores:
1. D1DurableRunStore — the production path for Cloudflare Workers.
- All operations mapped to D1 prepared statements
- Lease takeover via conditional UPDATE (atomic under SQLite locking)
- First-emit-wins enforced by PK (run_id, key) on durable_events
- Structural D1DatabaseLike interface — zero @cloudflare/workers-types dep
- Versioned schema.sql ships in the package (durable_schema_info table)
2. better-sqlite3 in devDependencies — drives the D1DurableRunStore
test matrix against a real SQLite engine. The full 10-test contract
suite now runs identically against InMemory, FileSystem, and D1
(real SQL, real UNIQUE constraints, real CASE expressions). 31 total
tests in the durable matrix.
3. runOnWorkflowStep — Cloudflare Workflows entrypoint adapter that
converts a WorkflowStep into a DurableContext. Each ctx.step
delegates to step.do; ctx.awaitEvent delegates to step.waitForEvent;
ctx.now / ctx.uuid checkpoint through step.do for replay stability.
Pure structural typing — no cloudflare:workers runtime dep.
Deployment patterns (pick one per agent, do not mix):
A. Plain Worker + D1DurableRunStore — default. Survives isolate
restart via D1 lease takeover. Use for chat sessions.
B. Cloudflare Workflows + runOnWorkflowStep — for long tasks
(minutes to hours), platform-managed retries, dashboard
observability. Workflows handles the outer durability.
175 tests pass (was 144).
@tangletools
tangletools merged commit a48cb65 into mainMay 20, 2026
1 check passed
@tangletools
tangletools deleted the feat/durable-runs branch May 20, 2026 14:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tangletools@drewstone
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(0.13.0): durable-run substrate - #20

Merged
tangletools merged 3 commits into
mainfrom
feat/durable-runs
May 20, 2026
Merged

feat(0.13.0): durable-run substrate#20
tangletools merged 3 commits into
mainfrom
feat/durable-runs

Conversation

@tangletools

Copy link
Copy Markdown
Contributor

Summary

Production agents need step-level durability — survive worker crashes, deploy rolls, OOM, rate-limit cascades. The 0.12.1 transport-level retry was the floor; this PR is the substrate.

Model directly inspired by Absurd (Postgres) and Cloudflare Workflows: split a run into ordered idempotent steps, persist each result before the next runs, replay from store on resume.

Public surface

```ts
const { result } = await runDurable({
runId: 'chat-session-42', // idempotency key (e.g. session id)
manifest: { projectId, task, input },
store: new FileSystemDurableRunStore(path), // or InMemoryDurableRunStore
taskFn: async (ctx) => {
const payment = await ctx.step('process-payment', async () => { ... })
const ship = await ctx.awaitEvent('shipment.packed', { timeoutMs: 60_000 })
return { payment, tracking: ship.trackingNumber }
},
})
```

Boundary disciplines — fail loud, no silent shortcuts

  • Step results MUST be JSON-serializable (class instances rejected at hash time)
  • Step intents MUST be stable across replays → `DurableRunDivergenceError`
  • Same runId + different inputs → `DurableRunInputMismatchError`
  • Lease conflict → `DurableRunLeaseHeldError`
  • `ctx.now()` / `ctx.uuid()` checkpointed once — replays return cached values
  • Lease renewal heartbeat (every leaseMs/3); lease loss aborts the current step

Ships

  • `DurableRunStore` contract + typed error taxonomy
  • `InMemoryDurableRunStore` (dev)
  • `FileSystemDurableRunStore` (eval harness — one dir per run, append-only steps.jsonl + events.jsonl, atomic-rename run.json + lease.json)
  • `runDurable` wrapper with lease heartbeat + abort propagation
  • `canonicalHash` / `manifestHash` / `stepId` / `deriveWorkerId` helpers

Tests

  • `pnpm typecheck` clean
  • `pnpm test` — 165 / 165 pass (21 new durable tests)
  • Identical test matrix runs against both in-memory + filesystem stores
  • Crash recovery: fail mid-stream, restart, verify completed step NOT re-executed
  • Lease takeover: workerA holds, expires, workerB acquires
  • Concurrent emit race: first-emit-wins enforced
  • awaitEvent + emit concurrent: receives payload mid-flight; replay returns cached
  • Deterministic ctx.now / ctx.uuid stable across replay
  • Manifest hash stable across object insertion order

Follow-ups (separate PRs, queued)

  1. `D1DurableRunStore` + versioned `.sql` migration → Cloudflare prod path
  2. Cloudflare Workflows adapter (each chat turn = a Workflow step, native CF durability)
  3. Wire legal-agent onto the substrate as end-to-end demo

Production agents need step-level durability — survive worker crashes,
deploy rolls, OOM, transient transport errors, rate-limit cascades.
0.12.1's transport-level retry was the floor; this is the substrate.
The model — inspired by Absurd and Cloudflare Workflows — splits a run
into ordered, idempotent **steps**. Each step's result is persisted before
the next runs. On resume, the runner replays prior steps (returning cached
values without re-execution) until it reaches the first unfinished step.
Surface:
ctx.step('process-payment', async () => { ... }) // checkpointed
ctx.awaitEvent('shipment.packed:42') // race-free
ctx.emitEvent('shipment.packed:42', payload) // first-emit-wins
ctx.now() / ctx.uuid() // deterministic
Concurrency: lease-based exclusivity. One worker per run at a time; lease
renewal on heartbeat; takeover after lease expiry. Committed steps survive.
Boundary disciplines (fail-loud, not silent):
- Step results MUST be JSON-serializable — class instances rejected
- Step intents MUST be stable across replays — divergence throws
- Same runId + different manifest hash → DurableRunInputMismatchError
- Step input fingerprints hashed canonically (sorted-key JSON)
Ships:
- DurableRunStore contract + typed error taxonomy
- InMemoryDurableRunStore (dev)
- FileSystemDurableRunStore (eval harness — one dir per run, append-only
steps.jsonl + events.jsonl, atomic-rename run.json + lease.json)
- runDurable(ctx => ...) wrapper with lease heartbeat + abort-signal
propagation + crash-safe failure recording
- canonicalHash + manifestHash + stepId + deriveWorkerId helpers
Follow-ups (separate PRs, queued):
- D1DurableRunStore + versioned .sql migration (Cloudflare prod path)
- Cloudflare Workflows adapter (each turn = a Workflow step)
- Wire one agent (legal) onto the substrate as end-to-end demo
21 new tests covering: fresh runs, replay-skips-fn, manifest mismatch,
step divergence, lease takeover after expiry, awaitEvent race + timeout,
first-emit-wins, deterministic ctx.now/uuid stability across replay. All
21 pass identically against in-memory AND filesystem stores via a shared
test matrix. Total suite: 165 tests, all passing.
Three pieces added on top of the InMemory + FileSystem stores:
1. D1DurableRunStore — the production path for Cloudflare Workers.
- All operations mapped to D1 prepared statements
- Lease takeover via conditional UPDATE (atomic under SQLite locking)
- First-emit-wins enforced by PK (run_id, key) on durable_events
- Structural D1DatabaseLike interface — zero @cloudflare/workers-types dep
- Versioned schema.sql ships in the package (durable_schema_info table)
2. better-sqlite3 in devDependencies — drives the D1DurableRunStore
test matrix against a real SQLite engine. The full 10-test contract
suite now runs identically against InMemory, FileSystem, and D1
(real SQL, real UNIQUE constraints, real CASE expressions). 31 total
tests in the durable matrix.
3. runOnWorkflowStep — Cloudflare Workflows entrypoint adapter that
converts a WorkflowStep into a DurableContext. Each ctx.step
delegates to step.do; ctx.awaitEvent delegates to step.waitForEvent;
ctx.now / ctx.uuid checkpoint through step.do for replay stability.
Pure structural typing — no cloudflare:workers runtime dep.
Deployment patterns (pick one per agent, do not mix):
A. Plain Worker + D1DurableRunStore — default. Survives isolate
restart via D1 lease takeover. Use for chat sessions.
B. Cloudflare Workflows + runOnWorkflowStep — for long tasks
(minutes to hours), platform-managed retries, dashboard
observability. Workflows handles the outer durability.
175 tests pass (was 144).
@tangletools
tangletools merged commit a48cb65 into mainMay 20, 2026
1 check passed
@tangletools
tangletools deleted the feat/durable-runs branch May 20, 2026 14:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tangletools@drewstone