Durable Long-Running Agentic Processes — architecture assessment

Operator provider pauses: optional expiry runs through the existing jobs facade, with ScheduledJob holding the operation identity and resume time. A five-minute reconciliation job repairs overdue timers after restart or event delivery failure. A matching timer checks health and the current operation inside the serialized resume path; it cannot lift a newer or indefinite pause. See the provider control guide.

Current authority implementation: durable process reconnects read tool grants from the canonical coworker Agent row on each request, with revocation tombstones taking precedence. Known alias rows are not an alternate authority source. Administrator reconciliation of legacy differences requires explicit approval.

Review execution recovery: an incomplete design or plan reviewer panel keeps the saved artifact and last completed semantic round. Missing or inconclusive reviewers do not approve work, create document findings, consume revision rounds, or trigger document regeneration. The review panel and readiness strip show the execution wait separately from completed individual verdicts. Retry only after reviewer execution recovers; original execution records remain intact. See the MCP recovery contract.

Date: 2026-06-09 Status: Assessment / direction-setting. Not a build spec. The phased recommendations in §8 become backlog items, each promoted through Build Studio in the normal way. Audience: Operator (Mark), platform architects, the Build Studio sub-agents, and any coworker that owns a process which runs longer than a single request. Scope: Why Build Studio has been hard to make resilient and simple; what Cursor, Linear, and the durable-execution industry do instead; what substrate DPF already has (the durable engine and the operator-facing experience layer); and a single generalized pattern — the Durable Agentic Process (DAP) — that should govern Build Studio, marketing campaigns, monthly financial close, and every other long-horizon process. Companion docs: platform-overview.md (runtime), ai-coworker-development-principles.md (how coworkers are designed), dpf-patterns.md (DPF-novel patterns), autonomy-and-wwmd.md (decision gates), 2026-06-09-dap-experience-layer-design.md (UX design of layer 6 — trust, attention, situational awareness; grounded in the HCI literature).

Substrate verification (EA review, 2026-06-09): Every code claim below was checked against the live tree. Confirmed: lib/integrate contains zero inngest.createFunction calls; build-pipeline.ts carries the “replaces the fire-and-forget autoExecuteBuild with resumable step checkpoints” header; the three boot hooks exist in instrumentation.ts; MAX_MERGE_RETRIES = 1 at build-orchestrator.ts:1352; TaskRun carries status/source/lastHeartbeatAt with a [status, lastHeartbeatAt] watchdog index (build-delivery.prisma); Inngest runs as a separate self-hosted container (docker-compose.yml:666). Findings that sharpen the recommendation rather than weaken it: §3.1 (out-of-process Inngest is what makes the thesis hold), §7.4 caveat (builds don’t own a TaskRun yet — the migration stands one up), §7.5/§7.6 (Inngest’s replay/determinism constraints, the absence of native versioning, and the single-engine concentration risk are the real costs — §7.6 raises open design question #1), §7.3 (external GL postings need an explicit dedup mechanism — Inngest’s at-least-once is not exactly-once on its own), and §9.7 (Decision B’s volatile market numbers independently re-checked; three softened).

External-claims verification (2026-06-09): The load-bearing external figures were independently checked against primary sources (§10). Confirmed: Inngest’s hard limits (4MB/step, 32MB/run-state — the binding one, 1000 steps, ≤2h/step) and its HTTP-per-step execution model (§7.5); the Anthropic 2026-06-15 billing split moving programmatic/headless Claude Code onto a metered credit pool at API rates (§9.3); prompt-cache reads at 0.1× input / 90% off with a default 5-min TTL (§9.6). The open-weight quality figures (§9.4) check out by model identity (DeepSeek V4-Pro, Kimi K2.6, GLM-5.1, MiniMax M3 are real June-2026 releases) but the precise point-gaps are benchmark-sensitive — treated as directional, with the strategic conclusion shown to survive the noise. Two corrections folded in below: §7.5 now states the real Inngest numbers instead of “validate later,” and adds the recovery-granularity / decouple-the-long-step risk (a 40-min CLI dispatch wrapped as one step re-runs wholesale on a portal recycle) — the single architectural point the prior draft understated.


0. TL;DR

  1. The engine is not missing. DPF already runs Inngest as a durable-execution engine (step.run journaling, retries, concurrency limits, cron) and already has a generic long-running-process substrate: the TaskRun lifecycle, the QuiescenceRun state machine, the StallEvent heartbeat watchdog, and the marketing ScheduledOutboundAction dispatcher. This is, point-for-point, the architecture the rest of the industry converged on in 2025–2026.

  2. Build Studio is the outlier. Its orchestrator (apps/web/lib/build/build-orchestrator.ts) is a bespoke, fire-and-forget, in-portal loop that runs outside the durable substrate. It re-implements — partially and fragilely — the very things Inngest already does: resume (by title match, not journal), retry (single attempt, then silent loss), and crash recovery (boot-time reconciliation hooks bolted on in instrumentation.ts). That is the root cause of “no task-level resume,” “stranded builds,” and “self-upgrade kills the build.”

  3. Cursor, Linear, and Anthropic all teach the same lesson. Make the task itself a durable object that outlives any process or VM (Cursor → Temporal; Linear → AgentSession; Anthropic → “resumable execution with retry logic and regular checkpoints”). Decouple the agent loop from the machine. Give the process an explicit, user-visible state machine with first-class awaiting-input, error, and stale states. Keep a human as the accountable owner at a non-bypassable gate. Enforce liveness contracts so stuck agents are detected, not silent.

  4. The fix is convergence, not a new framework. Don’t adopt Temporal. Move Build Studio’s orchestration onto the durable substrate DPF already runs, and promote that substrate into a named, reusable Durable Agentic Process pattern. Then marketing campaigns and monthly close are instances of the same pattern, not new bespoke pipelines.

  5. Durability the operator can’t see isn’t finished. The same convergence covers the experience: a user-visible state machine (Linear), an editable plan (Cursor Plan Mode), and detected-not-silent liveness are UX contracts, not just backend ones. DPF has already built a rich operator-facing layer for exactly this — the Build Studio operational panel, TruthSourceBadge, ProcessGraph, stall-recovery actions, the decision-gate panel — but, precisely mirroring the durability story, it is bespoke to Build Studio (components/build/*). The DAP pattern must generalize the experience layer too (§3.2, §6 layer 6), or every new instance re-invents its console — and likely re-introduces the silent-failure UX the build layer already fixed.

This assessment respects verify-substrate-before-proposing-new: the recommendation is to use what exists, and the single new thing proposed (a thin ProcessRun generalization of TaskRun) is justified in §7.4 against the substrate that is already there.


1. The problem, stated precisely

Build Studio is a five-phase agentic pipeline (ideate → plan → build → review → ship, plus terminal complete/failed; a build rebased onto main at ship returns to review to re-earn its evidence on the new tree, BI-76BD65E6) defined in apps/web/lib/explore/feature-build-types.ts:441 with the transition matrix at feature-build-types.ts:657. The build phase is driven by runBuildOrchestrator() at apps/web/lib/build/build-orchestrator.ts:951, entered fire-and-forget via autoExecuteBuild() (defined at apps/web/lib/actions/build.ts:735, called at :710/:876/:908/:1149).

We have spent months hardening it and it remains fragile. The concrete, observed failure modes:

Symptom Mechanism in the code today Memory / PR trail
No task-level resume on restart Task results stored by title, matched on resume by string equality (getCompletedTaskTitles() build-orchestrator.ts:712); a re-plan that renames/reorders tasks breaks the match → duplicate or skipped work. No per-step journal. known limitation
Stranded builds autoExecuteBuild() is fire-and-forget in the portal process; a portal recycle kills it mid-flight. Recovery is a boot hook (resumeStrandedBuildsOnBoot instrumentation.ts:305) that re-dispatches stale rows — i.e. crash recovery is bolted on outside the execution model. —
Contradictory checkpoint states buildExecState can land in self-contradictory shapes (missing-step, error-without-fail, complete-no-verify); a second boot hook recoverContradictoryBuildExecStatesOnBoot instrumentation.ts:214 classifies and clears them. —
Self-upgrade kills in-session work A bundle-hash change recycles the portal container; the in-portal orchestrator dies. Quiescence + boot reconciliation (reconcileSelfUpgradeRunsOnBoot instrumentation.ts:119) patch around it. self-upgrade-kills-in-session-ux
Lost task evidence taskResults write uses optimistic locking with MAX_MERGE_RETRIES = 1 (build-orchestrator.ts); on a second collision the write is silently dropped and the build proceeds as if it succeeded. —
Runaway / silent agent runs Specialist runs have a retry cap but no firm per-task timeout; a hung CLI can consume most of the 40-min orchestrator budget. Mechanism questions previously spun 200 iterations. mechanism-question-grounding-gap

Every row is the same shape of bug: the orchestrator is a long-running stateful loop that the runtime can kill at any instant, and durability has been retrofitted around it instead of being a property of how it executes. We keep adding boot-time reconciliation hooks because the execution model gives us no checkpoints to resume from.


2. What the industry does instead (2025–2026 state of the art)

Full citations in §10. The convergence is striking — execution-layer vendors (Cursor) and coordination-layer vendors (Linear) arrived at the same primitives from opposite ends.

2.1 Cursor — durable execution decoupled from the machine

Cursor’s Cloud/Background Agents are the most load-bearing public example. Their June 2026 engineering retrospective is explicit:

2.2 Linear — the task as a durable, observable session

Linear’s Agent Interaction Protocol (SDK shipped 2025-07-30) is the coordination-layer mirror image:

2.3 Durable-execution engines and Anthropic’s guidance

2.4 The shared pattern

Both Cursor and Linear, and the durable-execution field generally, converge on six primitives:

  1. A durable unit of work that outlives any single process (Temporal workflow / AgentSession / journaled run).
  2. An explicit, user-visible status state machine with awaiting-input, error, and stale as first-class states.
  3. A non-bypassable human accountability gate at the consequential boundary (merge / publish / post).
  4. Reasoning decoupled from execution so each can fail, hibernate, and resume independently.
  5. Isolated, reproducible execution environments to make parallelism safe.
  6. Liveness/heartbeat contracts so stuck work is detected, not silent.

Plus: plan-as-editable-artifact, bounded autonomy via external circuit breakers, and deploy-without-killing-in-flight-work.

Note that four of the six — the visible state machine (2), the human gate (3), plan-as-artifact, and detected liveness (6) — are experience contracts as much as execution ones. They are satisfied only if the operator can see the state, act on the gate, edit the plan, and notice the stall. A journal no human reads is observability for engineers, not a product surface — which is why Linear treats state as a product surface and Cursor ships Plan Mode as UI, not config.


3. What DPF already has (verify-substrate-first)

The good news, and the reframe: DPF already implements most of §2.4. It is not missing the engine. It is missing the discipline of putting all long-running work on the engine, and the discipline of naming the pattern so it is reused.

Industry primitive DPF substrate that already exists Where
Durable execution engine (journal + retry + cron) Inngest — step.run journaling, per-function retries, concurrency limits, and the cron + event function registries in queue/functions/index.ts (59 scheduled and 35 event functions as of 2026-08-16). Counts drift with every new job, so treat lib/operate/scheduled-jobs/catalog.ts as the authority — a parity test fails the build if a scheduled function is missing from it. Since 2026-09-26 every function and send site goes through the @/lib/jobs facade, which admits only the job features in use; the Inngest client is its adapter, and a CI guard (scripts/check-no-direct-job-engine-import.mjs) refuses engine imports anywhere else (job engine spec §6) apps/web/lib/jobs/index.ts, apps/web/lib/jobs/inngest-adapter.ts, apps/web/lib/queue/functions/index.ts
Durable unit of work / session TaskRun — universal task tracker with status enum (submitted·working·input-required·auth-required·completed·failed·canceled·rejected·archived), source, heartbeat schema.prisma TaskRun; apps/web/lib/tak/autonomous-work-run.ts
Explicit multi-state state machine + checkpoints QuiescenceRun — pending → preparing → draining → ready-to-swap → swapping → completed, enteredStateAt JSON timestamps, snapshots, heartbeat apps/web/lib/queue/functions/quiescence-run.ts
Liveness / staleness contract StallEvent + TaskRun.lastHeartbeatAt + taskrun-watchdog cron (retry/abandon/escalate) apps/web/lib/queue/functions/taskrun-watchdog.ts
HITL suspend/resume + approval gate WWMD decision gate (principle_decide) wired into phase advancement; approval state recorded on the row. The plan→build and ship gates additionally run advisory acumen consults — each acumen impacted by the phase’s planned file paths is asked through its WSID profession gate, and the results are attached to the gate result and recorded as DI-ledger rows. Advisory by contract: an acumen outcome never changes the gate’s allowed verdict. Paths are resolved from the build’s Workroom change-impact contract (falling back to the plan document; the realized diff wins at ship) and resolution is fail-open — if it yields nothing, the gate behaves exactly as it did before consults existed apps/web/lib/decision-perspective/build-studio-gate.ts, planned-file-paths.ts
Scheduled outbound action dispatcher (saga-ish) ScheduledOutboundAction (pending → fired/failed) + marketing tickScheduler() apps/web/lib/marketing/scheduler.ts
Multi-branch parallel agent work + resume DeliberationRun / TaskNode DAG (skips completed branches on re-run) apps/web/lib/queue/functions/deliberation-run.ts
Autonomous agent loop + tool grants resolveAgent → resolveTools → executeAgenticLoop; grant implication graph autonomous-work-run.ts, apps/web/lib/tak/agent-grants.ts
Renderer-neutral product-surface context Principal-bound Authorized Surface sessions expose semantic state, validation, and governed actions to browser, mobile, workroom/headless, scheduled, background, and external execution authorized-surface-runtime.ts, surface-pack.ts
Deploy-without-killing-in-flight (rainbow-ish) Quiescence protocol drains activity before container swap quiescence-run.ts
Cross-ring observation envelope GearInterface (additive evidence at ring boundaries) dpf-patterns.md §1.1
Operator progress visibility (truth-source-honest) Build Studio operational panel — N/M task counter, TruthSourceBadge, stale-conflict detection, quiet-agent watchdog with a “view last action” deep-link BuildProgressOperationalPanel.tsx, progress-visibility.ts
Root-cause-first failure rendering Dispatch history card — classified BuildFailureAxis shown above raw stdout/stderr BuildDispatchHistoryCard.tsx
Visible state machine as product surface ProcessGraph — phase/task/fork-join nodes with inspectors; PhaseIndicator / PhaseMiniRail / QueueStateBadge ProcessGraph.tsx, WorkflowStageInspector.tsx
Operator recovery affordances StalledTaskRecoveryActions (retry / abandon / escalate) + StallEventHistoryStrip + a stall-thresholds admin page StalledTaskRecoveryActions.tsx, StallEventHistoryStrip.tsx
HITL gate as a product surface DecisionPerspectiveGatePanel + ActionBanner capture escalate/defer outcomes; a decline outcome renders as a settled answer rather than a pending capture DecisionPerspectiveGatePanel.tsx
Unified evidence / activity timeline UnifiedEvidenceTimeline over the agent-event-bus stream UnifiedEvidenceTimeline.tsx
Notification transport notifications API + adapter (currently wired for self-upgrade only — not for build / gate / completion events) app/api/v1/notifications, notification-adapter.ts

The marketing scheduler and deliberation runner are already built the right way — as Inngest functions over generic state-tracked rows. Build Studio predates this discipline and never moved onto it.

3.1 Why this actually survives a crash: Inngest is out-of-process

The whole thesis hinges on one deployment fact that must be stated explicitly, because the obvious reviewer objection is “if the portal dies, doesn’t Inngest die with it?” It does not. Inngest runs as a separate self-hosted container (inngest/inngest:latest) with its own Postgres database and Redis (docker-compose.yml:666). The portal is a stateless function-server to Inngest: on boot it PUTs its function catalog to /api/inngest (self-hosted Inngest does not auto-discover apps — registration is explicit, DPF_INNGEST_SELF_SYNC_ON_BOOT_ENABLED), and thereafter Inngest invokes the portal one step at a time, persisting each step’s result in its own store.

The consequence is exactly what the in-process loop cannot give us: when the portal recycles mid-run, the durable run does not. Inngest holds the journal, notices the invocation failed or timed out, and re-invokes the step against the new portal instance — completed steps return memoized results, execution continues from the first un-journaled step. This is the mechanism that deletes the boot-reconciliation hooks: crash recovery moves from “the portal re-scans the DB on boot for orphans it left behind” to “the engine that never died retries the step.” It is also the precise reason the marketing dispatcher and deliberation runner have never needed stranded-row recovery hooks, while Build Studio needs three.

3.2 The experience layer exists too — and has the same Build-Studio-only problem

The durability story has an exact UX twin. Over the 2026-05 progress-visibility overhaul, DPF built a genuinely good operator-facing layer for watching, steering, and recovering a long-running process (the bottom rows of the table above). It even includes a DPF-novel pattern the industry references don’t name: truth-source honesty — when the agent’s chat self-report disagrees with newer DB / sandbox / verification state, TruthSourceBadge shows which signal and how old, and hasStaleTruthConflict surfaces the disagreement rather than silently trusting one. That is awaiting-input / stale-as-product-surface done right, and it is worth elevating to a named pattern (§5, criterion 14).

But every one of those components lives under components/build/* and is wired to FeatureBuild / buildExecState, not to a generic run. So the experience layer is bespoke to Build Studio in exactly the way the execution layer is. A kind="campaign" or kind="month-close" DAP would today re-invent the process graph, the progress panel, the gate panel, the recovery actions, and the timeline from scratch — and would very likely re-introduce the silent-failure UX the build layer already fixed. Generalizing the experience layer is therefore a first-class part of the DAP convergence, not a follow-on polish task.

Two real gaps remain even within Build Studio, and they are the operator’s sharpest pain points:


4. The gap, named

Build Studio’s orchestrator runs the agent loop inside the portal’s request/process lifetime, not inside Inngest’s durable step model. Every resilience problem follows from that one decision.

Concretely:

This is not a criticism of the people who built it — build-pipeline.ts is a good response to the symptom. It is the textbook signature of a long-running process implemented as an in-process loop: you end up re-deriving the durable-execution engine by hand, one fix at a time. The entire durable-execution industry exists because this exact pattern doesn’t survive contact with crashes and deploys. DPF already bought the cure (Inngest) and applied it everywhere except its most important long-running process — so the work is convergence onto the engine, deleting the hand-rolled layer, not building something new.


5. Design principles (the bar any long-running process must clear)

Distilled from §2–§4 and the founder kernel. These are the acceptance criteria for the target architecture.

  1. Durability is a property of execution, not a retrofit. Journal each step; resume from the last completed step. Never re-implement resume at the application layer when the engine provides it. (Inngest step.run.)
  2. The process is a durable object with stable identity independent of any portal process, container, or VM. (Cursor Temporal workflow / Linear AgentSession → DPF TaskRun/ProcessRun.)
  3. One explicit, user-visible state machine per process, with awaiting-input, error, and stale as first-class states. State is a product surface. (Linear six-state model.)
  4. Workflows where you can, agents where you must. The deterministic engine owns what comes next and where artifacts live; the model makes bounded decisions inside that frame. (Anthropic; Praetorian/AWS consensus.)
  5. Side effects are idempotent, individually retryable steps. A step that re-runs must not double-execute (no silent dropped writes, no double-published campaign asset, no double-posted journal entry).
  6. Accountability and current authority at consequential boundaries. Apply the existing policy for publishing, financial effects and other consequential actions. Reuse valid standing authorization; obtain a human decision only when scope, authority or unresolved judgment requires one. An internal retry or executor replacement alone is not a new approval boundary. Governance approves evidence, not provenance.
  7. Liveness contracts, not silent hangs. Heartbeats + staleness thresholds + a watchdog that retries/abandons/escalates. (Linear stale; DPF StallEvent.)
  8. Bounded autonomy via external circuit breakers. Per-step timeouts, iteration caps, and spend ceilings enforced outside the model so a runaway run “fails small, not crashes big.”
  9. Plan as an editable, durable artifact, with revert-and-re-plan preferred over patching an in-flight run. (Cursor Plan Mode.)
  10. Deploy without killing in-flight work. Drain (quiescence) and/or run versions side-by-side (rainbow); pin a run to its workflow version. (Anthropic rainbow deploys; DPF quiescence.)
  11. Saga compensation for partial failure. When a multi-step business process fails midway, run idempotent compensations to restore a consistent state rather than leaving half-done work.
  12. Observability + per-gate eval. Replayable step traces (the journal is the trace) and automated evaluation at each gate, aligned to OpenTelemetry GenAI conventions where practical.

Experience criteria (a durable process the operator cannot see or steer is not done — Nielsen heuristic #1, visibility of system status):

  1. Continuous visibility of system status. Every DAP renders its phase and status at all times; “is it still working?” is answered on-screen, not by asking. Quiet ≠ dead must be visible — the quiet-agent watchdog already does this for builds; it generalizes. (NN/g heuristic #1.)
  2. Truth-source honesty over false certainty. When signals disagree (chat self-report vs. journal vs. sandbox vs. verification), show the source and its age and surface the conflict; never silently pick one. (DPF TruthSourceBadge — elevate to a named pattern.)
  3. Notify, don’t poll. awaiting-input and terminal outcomes push to the operator (notification + a cross-process inbox), because a multi-hour / multi-day process cannot assume anyone is watching the tab. Frame the decision in the operator’s terms — a non-technical operator decides, the agent runs the system — never in buildExecState internals.
  4. Recovery affordances at the operator’s altitude. Every error / stale state offers a bounded next action — retry / abandon / escalate / edit-plan — in the UI, at the altitude the operator reasons about (the process), not only the engine’s internal step. A silent boot-time self-heal must still leave a visible audit entry.
  5. The plan is an editable surface, not a transcript. Criterion 9’s editable plan is editable in the UI; revert-and-re-plan is a visible affordance, not “re-prompt a coworker and hope.” (Cursor Plan Mode.)

5.1 Common recovery contract

This contract applies to every agentic process, including a short tool action that can outlive its caller. It extends the existing TaskRun, Workroom, AsyncInferenceOp and durable queue contracts. It introduces no parallel engine, ledger, universal approval step or client-owned recovery loop.

  1. Work has stable identity. Persist the immutable request, owning process, version and relevant evidence references before dispatch. Keep business work identity separate from execution attempts, sessions and credentials. Resume the same operation for the same authorized request; an intentional successor records its predecessor and reason. Changed intent is a new operation.
  2. Progress survives interruption. Persist checkpoints, pending dispatch, provider handles and committed results in their existing authoritative stores. Reconcile a lost response against committed state before invoking inference or publishing again. A successful tool call, a saved receipt and a satisfied gate are distinct facts.
  3. Effects are reconciled before replay. Use stable effect keys and atomic state transitions where available. An ambiguous remote outcome requires provider reconciliation or explicit repair, not a blind repeat. Compensation is domain-specific and must not claim to undo irreversible effects.
  4. Authority is current; ownership is durable. Validate the current human, organization, coworker, consent and resource scope on resume or reassignment. Credential refresh within a valid authority family preserves task identity; revoked authority pauses access. An authorized takeover records the prior and new executor without discarding work or history. Executor replacement alone preserves accountability; an authorized accountable-owner transfer records its own before/after identity and authority.
  5. The platform drives bounded recovery. Queue delivery and reconciliation operate without the original client. Classify transient dependency failures, exhausted capacity, missing decisions, denied authority and actual defects. Retain retry budgets across restarts and replacements. At exhaustion, persist a specific next action and responsible owner, or a justified terminal disposition. Neither an infinite loop nor silent abandonment is recovery.
  6. Recovery is visible and verifiable. Show the last committed progress, current attempt, uncertainty, responsible owner and next action in the existing work surface. Notifications wake a reader; canonical state decides what happened. A replacement human or agent can continue from those records without reconstructing a private conversation.

Interruption acceptance matrix

Apply relevant rows through existing failure-analysis and verification evidence; record why a row does not apply. This is one shared contract, not a fresh checklist approval on every retry.

Interruption or boundary Required observation
Caller disconnects or is killed after dispatch Server-owned work progresses or exposes a bounded wait; reconnect retrieves the same operation.
Worker restarts or an upgrade interrupts a step Completed effects are retained; the unfinished step resumes under its recorded version or an explicit migration.
Response is lost after an effect or receipt commits Reconciliation returns the committed result and advances once; no duplicate effect, inference or approval.
Duplicate delivery or competing executors A fenced claim or compare-and-set admits one valid transition; a stale worker cannot overwrite its successor.
Credential rotates or an executor is reassigned Authorized recovery retains work identity and evidence; another human, tenant, consent or resource cannot inherit access.
Provider, source reader or budget becomes unavailable Progress and real continuation cursors survive; retry is bounded and wakes on a relevant change. Exhaustion names a repair or terminal disposition.
Authority is revoked or the request/artifact is superseded Resume refuses the stale authority or evidence and supplies the supported repair; it never invents approval.
No eligible replacement or a nonrepeatable effect is uncertain Work remains visible with an accountable owner and reconciliation/compensation path; it is not silently marked successful.

Coverage belongs to the owning adapter’s tests and runtime evidence. Updating this architecture does not establish fleet-wide conformance. The September 2026 review incident proved the difference: receipt validation and completion were repaired, while access-token-based task identity still required its own OAuth repair. Existing async-operation recovery and provider-outage work likewise retain their own acceptance obligations.

6. Target architecture — the Durable Agentic Process (DAP)

A single pattern that every long-horizon process is an instance of. It is mostly assembly of existing DPF substrate under one name.

                        ┌─────────────────────────────────────────────┐
                        │  ProcessRun  (durable object, stable id)      │
                        │  - kind: build | campaign | month-close | …   │
                        │  - phase: domain state machine (explicit)     │
                        │  - status: working|awaiting-input|error|stale │
                        │  - plan: durable editable artifact            │
                        │  - heartbeat, version, evidence, audit        │
                        └───────────────┬─────────────────────────────┘
                                        │ drives
        ┌───────────────────────────────┼───────────────────────────────┐
        ▼                                ▼                                ▼
  Inngest workflow fn            Scope-appropriate gates         Watchdog (liveness)
  step.run per phase /           WWMD / WWWD / profession        StallEvent +
  per task → journaled,          + current authority             taskrun-watchdog →
  retried, resumable             + required decisions            retry/abandon/escalate
        │
        ▼
  Bounded sub-agent steps (orchestrator-workers)
  - each step: idempotent, per-step timeout, isolated context
  - emits semantic activity (thought/action/response/error) to agent-event-bus
  - circuit breaker: iteration cap + spend ceiling
        │
        ▼  (all of the above is rendered to, and steered by, the operator)
  Experience layer  (kind-agnostic; generalized from components/build/*)
  - process graph + phase/status        - truth-source-honest progress
  - awaiting-input → notify + inbox     - recovery affordances (retry/abandon/escalate)
  - editable plan artifact              - per-process evidence timeline

The six composable layers, all already present in DPF:

  1. Durable object — generalize TaskRun into (or alias it as) ProcessRun with a kind discriminator and a domain phase (§7.4). One row, stable id, owns the state machine, plan artifact, evidence, heartbeat, and version.
  2. Durable execution — the orchestration is an Inngest function with one step.run per phase (and per task within a phase). The journal gives resume-from-last-step for free, deleting the title-match resume logic and the boot-reconciliation hooks.
  3. Explicit state machine — the domain phase enum (Build Studio’s already exists; marketing/close get their own) plus the cross-domain status (working/awaiting-input/error/stale) borrowed from TaskRun. Both rendered in the UI.
  4. Decision and authority gates — existing policy and settled direction in the owning scope determine whether a decision is needed: WWMD for platform development, WWWD for the organization’s business, profession authority for craft. Where human input is required, persist the wait and resume on its recorded response rather than blocking a process. Reuse unchanged authorization and evidence during recovery.
  5. Liveness + circuit breakers — lastHeartbeatAt + StallEvent watchdog + per-step timeout + iteration/spend caps.
  6. Experience layer — the operator-facing surface that renders layers 1–5 and feeds the gate in layer 4: a kind-agnostic process graph, truth-source-honest progress, a cross-process inbox + notification transport for awaiting-input / terminal states, recovery affordances, and an editable plan. Generalized from the Build Studio components that already exist (§3.2), not built new. Designed in depth — trust calibration, attention/notification policy, situational-awareness gates, progressive disclosure, the cross-process inbox, and UX acceptance metrics — in the companion 2026-06-09-dap-experience-layer-design.md.

Implemented external Task slice (2026-08-31): ordinary MCP TaskRun submissions now use the existing self-hosted Inngest substrate as a durable execution outbox. Submission commits the task and immutable request/authentication binding before enqueue; the worker claims it with the canonical heartbeat-aware compare-and-set transition, runs the unchanged governed executor, and emits status only after committed transitions. A scheduled two-minute reconciliation job re-enqueues bounded pending work after a portal restart or lost delivery. Streamable HTTP GET/SSE pushes MCP notifications/tasks/status; tasks/get and tasks/list remain the recovery and compatibility path. This realizes “notify, don’t poll” as push first, bounded polling second—not push-only—and deliberately avoids caller-selected webhooks or a second task ledger.

Implemented upgrade drain that waits for work (2026-10-01, BI-F9EE05E5 slice A): the QuiescenceRun coordinator (queue/functions/quiescence-run.ts) no longer stops in-flight work when a self-upgrade drains. Admission closes at drain start. TaskRuns are flipped to quiescing only once hard blockers clear (or on operator force), so live build phases run to completion and are never reaped while they still heartbeat. The wait checks once per tick and sleeps on an operator-control event between checks. It pauses in the non-terminal awaiting-operator status at a configurable bound (default 60 minutes), and an operator abort is recorded durably (QuiescenceRun.abortRequestedAt/By). The self-upgrade job runs as separate pre-drain, wait and swap steps (self-upgrade-steps.ts, self-upgrade-swap.ts) rather than one long request. The wait helpers live in self-upgrade/drain-wait.ts, admission re-assertion and the swap guard in drain-admission.ts, and the import-free status contract in quiescence-contract.ts. Reconcilers and the stuck-coordinator watchdog key on heartbeat and swap state, never on run age. Design: Activity Quiescence Protocol §11a.

Implemented guard-finding repair (2026-09-30, BI-B2EEA6DE): when a Build Studio build’s finalize gauntlet fails on its own change, the review job hands the build back to its build phase and sends build/gauntlet.repair. That event-only function, registered in queue/functions/index.ts with per-build concurrency 1, dispatches one repair task through the orchestrator’s specialist dispatch (runGauntletRepairTask) and returns the build to review. The bound (two hand-backs, then one operator escalation) and the state (verificationOut.gauntletRepair, pointing at the evidence record) live in lib/build/gauntlet-repair.ts.

Implemented async delivery projection (2026-09-04): the Delivery task hub reads authorized durable async-operation handles through their server-owned TaskRun or Workroom binding. One event-only function registered in queue/functions/index.ts consumes committed async transition locators, re-reads canonical state, writes a deterministic status-changed activity to the existing Workroom ledger, and wakes the bounded list stream. Recent terminal transitions may also create one deduplicated semantic notification. The event payload never supplies status, Workroom, recipient, result, or notification authority, and this projection adds neither a scheduler nor a second execution or task ledger.

Deliberation branches execute (2026-09-15): the runner was durable and resumable from the start, but it only ever routed a branch — routeEndpointV2 chose a provider and model, the choice was recorded, and the node was marked complete without a call. Every branch finished in zero seconds, so the synthesizer found no positions to merge and every run on every pattern returned “Insufficient evidence to produce a recommendation” (measured: review 1207/1210, debate 198/198, governance-triage 263/275 — one distinct outcome between them). Branches now dispatch through routeAndCall, the canonical route-and-execute entry point, carrying the role’s persona and the run’s brief; the reply is persisted to TaskNode.outputSnapshot and parsed into the BranchArtifact fields the synthesizer already consumed. OrchestrateDeliberationInput gained brief / subject, because the engine previously had no channel for what it was deliberating about. Budget accounting moved from a += 0 placeholder to observed token usage, so budgetUsd now actually halts a panel. The durable execution properties described above are unchanged — this fills the dispatch step inside them.

What is deliberately not adopted: Temporal/Restate/DBOS as a new engine. DPF already runs Inngest, which provides the same journaling/retry/resume semantics. Adding a second durable engine would violate single-source-of-truth and the simplicity principle. The lesson from Cursor is “put the agent loop on durable execution,” not “use Temporal specifically.”


7. Applying DAP

7.1 Build Studio (the first and proving instance)

The migration is largely mechanical and removes more code than it adds in steady state — but it is not free, and the constraints that make it non-trivial are named in §7.5. The moves:

Net: Build Studio gets simpler (less bespoke durability code) and more resilient (durability from the engine). That is the resolution of the “resilience vs. simplification” tension we have been fighting.

7.2 Marketing campaigns

Already two-thirds there (ScheduledOutboundAction + tickScheduler). To make a campaign a first-class DAP:

Cadence is now posture-driven (BI-C26FE785, 2026-08-26). planUpcomingForAssetTasks() previously scheduled every drafter run at a fixed ADVANCE_DAYS_FOR_DUE_WINDOW = 3. It now takes the resolved marketing-campaign proactivity posture and varies the lead time with it: quiet suppresses planning outright (planning-then-staying-silent would still spend model budget and fill the outbound queue behind the operator’s back), balanced keeps the standard lead time, and assertive starts creative earlier — falling back to the standard lead time rather than skipping when the longer one would land in the past, so a more assertive posture can never schedule less work than a calmer one.

This matters for the DAP framing above because it makes the brief → draft transition a governed decision rather than a constant. The HITL gate is unchanged and now doubly enforced: the resolver caps marketing-campaign at an actionBoundary of propose for every level, and because the posture ladder only ever tightens, no room declaration or operator preference downstream can loosen it to preauthorized. Publishing therefore remains reachable only through the approval queue, which is the non-bypassable boundary this section already required.

7.3 Monthly financial close / “balancing the books”

The highest-stakes instance, and the one where durability + auditability + idempotency matter most. The finance skills already exist (finance:close-management, finance:reconciliation, finance:journal-entry, finance:financial-statements).

7.4 The one new substrate, justified

The only genuinely new thing proposed is a thin generalization of TaskRun into ProcessRun (or, cheaper, *using TaskRun as-is with a richer kind/source and a phase field). Justification against verify-substrate-before-proposing-new:

This must be confirmed by a substrate grep + live-backlog sweep at BI-filing time, not taken on this doc’s word.

7.5 Migration risks and Inngest’s own constraints

“Put the loop on the engine” is the right direction, but durable execution swaps one set of constraints for another. These must be designed for in Phase 0, not discovered in production. None is a blocker; each is a known cost that the “mechanical” framing of §7.1 should not hide.

7.6 Inngest as a concentrated platform dependency, and the versioning crux

§7.5 covers Inngest’s per-function constraints; this covers the system consequence of putting build, campaign, and month-close on one self-hosted engine — and resolves the one place the doc still hand-waves: workflow versioning across self-upgrade. (Self-hosting facts independently verified 2026-06-09; cites §10.)

Open design question #1 — self-upgrade × determinism (the crux of Phase 2, currently unresolved). Principle 10 wants “deploy without killing in-flight work,” but self-upgrade recycles the portal with new code for all functions at once, and §7.5’s “recovery = step granularity” already shows that a step in flight at swap time restarts. Two honest options: (a) quiescence drains all in-flight DAP runs before the swap — clean, but a multi-hour build blocks or is sacrificed by the upgrade; (b) the side-by-side pattern above — self-upgrade registers new function ids and lets old runs finish on the old code paths — true rainbow, but every DAP function carries version-routing and the post-swap portal must keep old code resolvable. Phase 2 must pick one deliberately; it is not a one-liner. Recommend a Phase-0 spike that kills the portal mid-build and mid-self-upgrade and observes resume empirically (per structural-verification-is-not-functional).


Each step below is a backlog item promoted through Build Studio in the normal way — this doc does not authorize code.

  1. Phase 0 — Spec + BI. File a BI: “Move Build Studio orchestration onto Inngest durable execution.” Filed as BI-89030C9B (epic EP-BUILD-STUDIO, status: triaging); it blocks the experience-layer item (BI-BC8F667E). Substrate-verify TaskRun extension (§7.4) and the Inngest constraints in §7.5 (step-output size limits, replay determinism of the current orchestrator control flow, stable step ids) — confirm the pinned Inngest version’s actual limits before committing to the design. Smallest provable slice: wrap one phase (the build phase) as an Inngest function with per-task step.run, returning artifacts by reference, behind a flag, on a contained build. Prove resume-across-restart functionally on the live install (per structural-verification-is-not-functional).
  2. Phase 1 — Retire the retrofits. Once durable execution is proven for the build phase, delete title-match resume and the stranded/contradictory boot hooks; fix the silent dropped taskResults write as an idempotent step; add per-specialist timeout + circuit breakers.
  3. Phase 2 — Self-upgrade safety. Resolve open design question #1 (§7.6): choose drain-all-before-swap vs. side-by-side function versioning, then implement it — there is no native “pin the version” toggle. Confirm a build survives a self-upgrade swap end-to-end, including a step in flight at swap time. Closes the self-upgrade-kills-in-session class.
  4. Phase 3 — Name the pattern. Promote the assembled pattern to a dpf-patterns.md entry (“Durable Agentic Process”) and a kernel principle (“long-running work runs on durable execution, never an in-process loop”). Extend TaskRun per §7.4.
  5. Phase 4 — Second instance: marketing campaign as a DAP, reusing the now-named pattern. This is the real test of generality.
  6. Phase 5 — Third instance: monthly close as a DAP, with idempotent posting + sign-off gate + compensation. Highest stakes, done last, on a proven pattern.
  7. Cross-cutting — observability. Align the step journal/agent-event-bus emissions to OpenTelemetry GenAI conventions so every DAP is replayable and eval-able at each gate.
  8. Cross-cutting — experience layer. Generalize the Build Studio progress-visibility components (process graph, operational panel, gate panel, stall-recovery, TruthSourceBadge, evidence timeline) into a kind-agnostic experience layer, and add a cross-process inbox + notification transport so awaiting-input and terminal states reach the operator without polling. The notification contract is already designed — the realtime-HITL-mobile-companion spec defines a canonical HitlNotificationEvent (eventId/taskRunId/riskClass/title/summary/deepLink) projected into Notification rows, a Paused-Work surface, push via PushDeviceRegistration, and authenticated approve/reject/request-changes endpoints (the audit confirms TaskRun/Notification/agent-event-bus/SSE already exist; only the canonical event, channel policy, and push delivery are unbuilt). A generalized DAP awaiting-input state should emit that same event, not invent a second path. Add in-UI plan editing (criterion 17). Sequence this alongside Phase 4 — the marketing instance is what proves the experience layer generalizes, not just the engine. The full UX design for this item — the notification policy that makes riskClass an alarm-fatigue-safe channel selector, situational-awareness gates, the cross-process inbox verb-set, re-entry digests, and the UX acceptance metrics to ship against — is in the companion 2026-06-09-dap-experience-layer-design.md. Filed as BI-BC8F667E (epic EP-COWORKER-INTERACTIVITY, status: triaging), with the Phase 1–2 notification slice (notification policy + elicitation activity type + situational-awareness gate) called out to land with the durable-execution migration rather than waiting for the marketing instance.

9. Decision B — execution-agent ownership and model economics

§1–§8 are Decision A: how the process is orchestrated (resilience + simplification). This section is Decision B, the orthogonal question raised in review: do we own the development work, and can our own LLM cut cost? The two are deliberately decoupled — and the central finding is that Decision A is the precondition that makes Decision B a safe, reversible, per-task dial rather than an architecture bet. Doing A does not commit us to any model choice; it gives us the seam to change model per step without re-plumbing.

9.1 Two sub-decisions, kept separate

9.2 What the code does today (verified 2026-06-09)

9.3 The pricing premise is shifting under us (the decision-changer)

The “CLI on a flat subscription is ~5–20× cheaper than per-token API” advantage is real for interactive coding — but Anthropic has moved programmatic/headless use (claude -p, the Agent SDK, CI actions) onto per-token billing (reported cutover 2026-06-15; the mechanism is confirmed from the primary source, the exact date is not — §9.7); the subscription contributes only a fixed monthly dollar credit (~$200 on Max 20×) that drains at API rates and then stops (Anthropic Agent SDK billing, observed 2026-06-09). Build Studio’s dispatch is automated, headless CLI-in-a-sandbox — squarely the programmatic bucket. So for an automated build agent, a $200 Max seat is economically a $200 spend cap, not all-you-can-eat.

The implication is the opposite of “lock in the subscription”: the flat-rate advantage for automated Build Studio has a shelf life, which argues for making the model a routable dial (what A enables) rather than betting the architecture on subscription economics staying cheap. This is the platform’s own no-provider-pinning principle meeting a market reason to honor it.

9.4 Can our own LLM cut costs significantly? Honest answer: partially.

9.5 Cost bands (order of magnitude, mid-2026; full cites §10)

Lever Number that matters
Frontier agentic task, uncached ~$3–$6/task (Sonnet/Opus/GPT-5.5 tier); median ≈ 1M input + 40K output
Prompt caching (the #1 lever) cache reads = 0.1× input (90% off) → ~3–5× cheaper/task; input dominates 25:1
Automated 50–200 tasks/day on frontier API ~$2K–$26K/mo depending on tier + caching
Cheap open-model API (DeepSeek-class) ~10× cheaper than Sonnet per token — the leaf-task target
Self-hosted GPU (big-MoE coding) ~$35K–$50K/mo at 24/7 for V4-Pro on 8×B200 — only sane >100–200M tok/day
Token variance per task budget for ~30× spread; more tokens ≠ better

9.6 Recommendation

Net: A and B compose. A is the seam; B is a measured tuning exercise on top of it — reversible per task, and hedged against a vendor pricing model that is already moving against flat-rate automation. Nothing in B requires “owning the entire process” or mirroring the CLI’s work in our code.

9.7 Epistemic status of the §9 numbers (independent verification, 2026-06-09)

§9’s market figures were spot-checked against live sources. They are directionally sound and the §9.6 recommendation is unchanged — but three are overstated or unsupported and must be softened before any spend decision leans on them. Recorded here so the BI inherits the caveats, not just the headline numbers.


10. References (primary sources, dated)

Cursor

Linear

Anthropic

Durable execution & orchestration

Model economics (Decision B) — all observed 2026-06-09, fast-moving

Experience / HCI


This is an assessment. Every recommendation in §7–§8 enters the backlog and is promoted through Build Studio; nothing here is authorization to write feature code directly.

Merge-triggered subscribers (BI-A6E4D205, BI-848360EF)

build/pr-merged.received is emitted by the signed GitHub webhook receiver when a pull_request closes as merged. It carries repository, number, head branch, head sha, merge commit and merge time.

Two subscribers consume it, both registered in eventFunctions — they have no cron and must never appear in scheduledFunctions, which is the registry the admin Scheduled Jobs surface renders and whose parity guard requires a SCHEDULED_JOB_CATALOG row:

Both are idempotent, because GitHub re-delivers webhooks. Neither asserts success when it cannot reach its subject.