Skip to content

feat(workhub): persist delegation linkage - #3935

Open
ARE404 wants to merge 5 commits into
apache:mainfrom
ARE404:feat/workhub-durable-delegation
Open

feat(workhub): persist delegation linkage#3935
ARE404 wants to merge 5 commits into
apache:mainfrom
ARE404:feat/workhub-durable-delegation

Conversation

@ARE404

@ARE404 ARE404 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Persist the first Slice 5 delegation authority in the existing WorkHub Coordination Session:

  • append closed, versioned delegation_intent and delegation_committed records around create_new and delegate_existing
  • retain exact user text and the original create_new title/workspace in the immutable intent
  • scope the renderer retry lease to one Runtime Host Coordination Session and preserve the same action identity across reload, waiting, and summary-repair retries
  • replay exact committed results after Runtime Host restart and reject conflicting action-id reuse
  • recover accepted target Turns from durable root admission, pending message admission, and immutable steering-message proof before attempting another submit
  • persist the first generated Coordination summary so retries remain byte-identical even if the target projection changes
  • keep waiting_for_user as a local, retryable result so it cannot consume the immutable acceptance summary
  • retire a newly created empty Session after a definitive submit rejection using revision-guarded session.remove; stable-create replay retains the guarded cleanup revision while the Session remains pristine so an unknown first discard can be retried safely

This supersedes #3930, which GitHub automatically closed when its non-conforming fork branch was renamed. It intentionally does not add replace, Stop ownership, target lifecycle state, supersession, or a general workflow phase machine.

Refs #3492
Proposal: #3286

Design choice: retry-driven immutable intent

A committed-only journal is insufficient because candidateRef is opaque and scoped to one candidate snapshot. If the target effect succeeds but the committed link is not appended before a Host restart, a later candidate set cannot reconstruct the selected Session.

The Action Gate therefore appends one self-contained immutable intent before the target effect, followed by one immutable commit containing the accepted target Turn. Its fingerprint covers stable user intent rather than ephemeral candidate IDs; once prepared, the intent owns the resolved target and creation context.

Recovery is driven by an explicit retry carrying the same production action identity. The renderer persists that identity with the retained Composer draft and scopes it to the active Runtime Host Coordination Session. It retires the identity only after target admission and the Coordination summary have both settled.

Before a retry can perform another target effect, the gate first checks durable admission evidence. An existing root receipt, pending message admission, or immutable steering-message proof is treated as accepted and is committed instead of re-submitted. A temporary waiting_for_user result stays local and does not reserve the immutable summary turn; the same action identity can later record the actual acceptance.

For create_new, cleanup remains deliberately narrow. The catalog returns a discard revision only for the newly created or exact stable-replay Session while it is still pristine. This lets a retry repeat revision-guarded cleanup after an unknown discard outcome, but any metadata or execution mutation revokes cleanup authority.

This deliberately avoids an autonomous startup executor or mutable workflow phases while closing the following failure cuts:

  • steering accepted, followed by delegation commit failure or Host restart
  • delegation committed, followed by Coordination summary failure
  • retry while the target is temporarily waiting_for_user
  • switching between Runtime Hosts with a retained failed draft
  • target projection changing between summary attempts
  • create_new succeeding before a definitive target-submit rejection, including an unknown first cleanup outcome

Unknown submit outcomes remain recoverable and are never compensated destructively.

UI behavior evidence

Before

image

After

image

Failure → reload → same-identity retry

pr3935-workhub-failure-reload-retry.mp4

The screenshots and recording come from the Electron WorkHub E2E flow. The recording shows a post-admission Coordination-summary failure retaining the draft, a renderer reload retaining the same action identity, and a successful retry producing one accepted request rather than a duplicate delegation.

Verification

  • npm run lint — PASS
  • npm run format:check — PASS
  • npm run build — PASS
  • npm run typecheck — PASS
  • npx knip --workspace apps/desktop — PASS
  • npx knip --workspace packages/ui — PASS
  • Core full test suite — PASS (659/659)
  • Runtime Host full test suite — PASS
  • Desktop main-process full test suite — PASS (1566/1566)
  • focused Runtime Host Action Gate and catalog regressions — PASS (47/47)
  • focused Desktop controller and surface regressions — PASS (92/92)
  • Electron WorkHub reconstruction E2E — PASS (3/3)
  • changed-file Biome checks — PASS
  • ASF header audit — PASS
  • git diff --check — PASS

Verified against exact head 04b17f4caf35f0f358f8ce4de62d28e654e861e2.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Codex implemented the typed delegation journal, production retry identity and recovery wiring, immutable steering-proof recovery, per-Host send leases, stable summary replay, revision-guarded create_new compensation, regression tests, and ADR updates. It also ran the verification listed above. All commits include the required Generated-by: Codex trailer.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, build, typecheck, affected test suites, and both required knip checks pass locally
  • UI behavior evidence is included above

Does this PR entail a change in behavior?

  • Yes — described under Summary and UI behavior evidence above
  • No

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What this PR does

Slice 5's first cut: it makes "this delegation happened" a durable fact. On top of #3818's non-destructive typed Action Gate, it appends an immutable delegation_intent before the target effect and a delegation_committed after it, both into the existing WorkHub Coordination Session. Around that it adds a renderer send lease binding an action identity to the retained Composer draft per Runtime Host, three-source recovery of the accepted target Turn after a Host restart, a persisted first Coordination summary so retries stay byte-identical, and revision-guarded session.remove compensation for a create_new Session whose submit was definitively rejected. It deliberately excludes replace, Stop ownership, target lifecycle state, supersession, and a general workflow phase machine.

Reviewed at exact head 1b352a04f9944fbd3acc0a21039f6aa300ab1e79. test and windows_recovery are green.

Verdict

Requesting changes: 4 × P1, 5 × P2, 4 × P3, all inline.

The design choice in the PR description is right, and I want to say so before the findings. A self-contained intent written before the effect is genuinely stronger than a committed-only journal, candidateRef really is the wrong thing to key recovery on, and the lock order [Coordination lease] → target effect → [Coordination lease] is clean with no nesting and no deadlock. Crash-after-effect/before-commit is correctly idempotent through #queryDurableSubmitProof. The three recovery sources are not redundant — each covers a disposition the others cannot prove.

Nothing is P0: workHub.enabled defaults to false (packages/core/src/settings.ts:560), so none of this blocks a release. The four P1s must be fixed before merge.

Root causes

Most of the findings are not independent. Four choices produce almost all of them, and fixing the first and last collapses most of the list.

1. The action identity was built on the wrong component. WorkHubSendLease is simultaneously the ComposerDraftPersistence implementation (read/write on key 'workhub'), the owner of the action identity, and the store for the frozen summary — all in one blob whose identity is inferred from draft-text equality. Those two things have opposite lifecycles. A draft changes on every keystroke and is cleared on send; an action identity must survive edits and retire only on an explicit protocol event. Hosting the identity inside the draft component makes every keystroke an identity event, and makes any non-Composer action (a clarification click) unable to own an identity at all.

This one choice produces P1 #1, P1 #2, P2 #8, and P3 #11, and it is why settle/complete/acquire never settled into a clean shape. The smaller design: keep requestId under its own key scoped only by Host, retire it with an explicit release(requestId), and leave the draft on the existing ComposerDraftPersistence seam. preservesIdentity, summary's owner check, and the whole read/write surface on this class then disappear.

2. The record state machine has no terminal state. The journal models intent → committed and nothing else, so "this intent can never succeed" is represented as "stay in intent forever". Every path where the effect becomes permanently impossible therefore becomes a permanent trap rather than a clean failure: the tombstoned create_new id (P1 #3), a fingerprint mismatch on a durable identity (P2 #6), and an edited target message whose digest no longer matches. A third terminal record — delegation_abandoned, meaning "this actionId is spent, mint a new one" — closes all three at once and is the missing piece rather than three separate patches.

3. A parallel path instead of the closest existing seam. workhub-delegation-journal.ts reimplements workhub-coordination-coordinator.ts#record point for point: header validation, derived message ids, high-water + messageIds snapshot read, content-equality idempotency, admission.run + refreshCanonical, drain-on-error. AGENTS.md's first principle applies. Beyond the duplication (P3 #12), the two copies independently decided what an unexpected error means — which is where the drain escalation in P2 #5 comes from.

4. Typed failures still have no end-to-end channel. Server-side codes exist (WorkHubActionGateFailureCode, WorkHubActionEffectFailureCode), are collapsed at the coordinator, dropped by ipcMain.handle, and reconstructed in the renderer by regex over English prose. This PR adds a fifth message that matches no pattern. It is not just cosmetic: it is precisely why P1 #1 and P1 #2 reach the user as "delivery failed, please retry" — an invitation to duplicate the delegation — instead of "already delivered" or "this can never succeed". Carried from #3818 and extended here.

One smaller point that is not a defect but shapes the design: the durable journal's primary key is minted and stored only by the renderer (sessionStorage), so the Host owns the authority but cannot enumerate, repair, or GC it, and every recovery must be client-driven (P2 #9). The PR rejected "an autonomous startup executor", which is reasonable — but Host-minted identity is not the same thing as a startup executor, and would remove the dependency on renderer storage surviving.


AI use: Claude Code assisted with source investigation and ran adversarial passes over the record layer, Action Gate, recovery, renderer, and architecture slices; every graded finding was re-verified against the exact head by me, and the analysis, grading, and conclusions are my own.

简体中文

这个 PR 在做什么:Slice 5 的第一刀,把"这次委派发生过"变成持久事实。在 #3818 的 typed Action Gate 之上,于同一个 WorkHub Coordination Session 内在目标副作用前后各追加一条不可变记录(delegation_intent / delegation_committed),配套渲染进程按 Host 作用域的 send lease、Host 重启后的三源恢复、首次 Coordination summary 持久化,以及 create_new 提交被定性拒绝后用 revision-guarded session.remove 回收空 Session。

结论:request changes,4 × P1 / 5 × P2 / 4 × P3,全部在行内。

先说对的部分:副作用前写自包含 intent 确实强于只写 committed,candidateRef 也确实不该作为恢复的键;锁顺序 [协调会话租约] → 目标副作用 → [协调会话租约] 干净、无嵌套、无死锁;副作用后崩溃前提交这一刀经 #queryDurableSubmitProof 是幂等的;三源恢复不冗余,各自覆盖别的源证明不了的 disposition。

没有 P0:workHub.enabled 默认 false,不阻塞发布。四条 P1 是合并前必须修的。

根因(多数发现并不独立,四个选择产生了几乎全部问题,修掉第 1 和第 2 条能一次收掉大半):

  1. action identity 建在了错误的组件上。 WorkHubSendLease 同时是 ComposerDraftPersistence 实现、action identity 的持有者、以及冻结 summary 的存储,全部塞在同一个 blob 里,且身份由草稿文本相等性推断。这两者生命周期正好相反:草稿每次按键都变、发送后清空;action identity 必须跨编辑存活、只能由显式协议事件退休。把身份寄生在草稿组件里,就等于让每一次按键都成为身份事件,也让任何非 Composer 发起的动作(点击澄清选项)根本无法拥有身份。这一个选择直接产生 P1#1、P1#2、P2#8、P3#11。更小的设计:requestId 单独一格、只按 Host 作用域、由显式 release() 退休,草稿继续走既有 seam;preservesIdentitysummary 的 owner 校验、这个类上的 read/write 全都可以删。
  2. 记录状态机没有终态。 只有 intent → committed,于是"这条 intent 永远不可能成功"被表示成"永远停在 intent"。所有效果已永久不可能的路径都变成死锁而不是干净失败:墓碑化的 create_new id(P1#3)、fingerprint 不匹配(P2#6)、目标消息被编辑后 digest 对不上。加第三条终态记录 delegation_abandoned(含义:此 actionId 已作废,请重新取号)能一次关掉三处,而不是打三个补丁。
  3. 建了平行路径而没有扩展最近的 seam。 journal 逐点复刻了 workhub-coordination-coordinator.ts#record。除重复本身(P3#12)外,两份副本各自独立决定"未预期错误意味着什么",这正是 P2#5 里 drain 升级的来源。
  4. typed failure 仍然没有端到端通道。 服务端有类型码,在 coordinator 被压平、被 ipcMain.handle 丢弃、再由渲染进程用英文正则重建;本 PR 又新增了一条不匹配任何模式的消息。这不只是观感问题:它正是 P1#1 和 P1#2 到用户面前变成"投递失败,请重试"——一句诱导重复委派的提示——而不是"已投递"或"这条永远不会成功"的原因。

另有一条不算缺陷但塑造了整个设计:持久 journal 的主键由渲染进程生成并只存在 sessionStorage,于是 Host 拥有权威却无法枚举、修复或回收它,所有恢复都必须由客户端驱动(P2#9)。PR 拒绝"自主启动执行器"是合理的,但由 Host 分配并持久化身份并不等于启动执行器,那样就不必依赖渲染进程存储存活。

Comment thread apps/desktop/src/renderer/workhub-surface.tsx
Comment thread apps/desktop/src/renderer/workhub-send-lease.ts Outdated
Comment thread packages/runtime-host/src/server/workhub-coordination-action-gate.ts Outdated
Comment thread packages/core/src/session.ts
Comment thread packages/runtime-host/src/server/execution-composition.ts Outdated
Comment thread packages/runtime-host/src/server/workhub-delegation-journal.ts
Comment thread packages/runtime-host/src/server/workhub-delegation-journal.ts
Comment thread apps/desktop/src/renderer/workhub-controller.ts
Comment thread docs/architecture/workhub-coordination-session-adr.md
@Astro-Han
Astro-Han dismissed their stale review August 26, 2026 17:48

Downgrading to a non-blocking comment review. The findings and inline comments stand as advisory; they are not a merge block.

@github-actions github-actions Bot added the effort/XL Over 1000 readable lines label Aug 27, 2026
ARE404 added 5 commits August 27, 2026 14:26
Recover accepted submissions before retrying, retry pristine create cleanup after unknown outcomes, and keep waiting responses from consuming the final coordination summary.

Add focused coverage and a renderer reload E2E for the durable action identity flow.

Generated-by: Codex
Persist definitive abandonment and deterministic cleanup state, preserve retry identity across renderer drafts and Host scopes, and recover accepted steering from durable proof.

Carry typed failures across Desktop IPC, avoid unnecessary Host drains, and extend regression coverage for crash and retry seams.

Generated-by: Codex
@ARE404
ARE404 force-pushed the feat/workhub-durable-delegation branch from 04b17f4 to dc25b43 Compare August 27, 2026 06:38

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes "a delegation happened" durable so one piece of user work reaches a target Session at most once. Problem correctly defined, and delegation_abandoned closes the biggest gap in the original design. Net improvement over main — not blocking.

Nine findings last round: five fixed, three partial, one unchanged, seven new. That ratio is the review.

Root cause (architecture)

"This send" has three state machines with different values, transitions, and non-equivalent terminals, and nothing binds them:

Authority Values Terminal
Renderer lease (localStorage) active | settledworkhub-send-lease.ts:35 the 4 codes in workhub-surface.tsx:454
Renderer turn list (React state) routing | settled | failedworkhub-surface.tsx:47
Host journal intent | committed | abandoned delegation_abandoned

P1 — a non-terminal failure makes the next, rewritten message send the old text. workhub-send-lease.ts:84-90 returns existing.action.text unconditionally while the action is active; the guard at workhub-surface.tsx:188 only runs under preserveDraft, which send (:335-354) does not pass. Send "check the payment regression" → route throws persistence_failed → turn becomes failed, lease stays active → the user clears the box and types something else → the old text is delivered, and the turn list shows it. A renderer reload mid-send leaves the same permanently-active state. This is the first two rows disagreeing about one event: failed exists in one machine and not the other.

P2 — operation_conflict is too coarse to be a renderer-side terminal. workhub-coordination-coordinator.ts:209 collapses candidate_set_stale / candidate_unavailable / self_route / action_conflict into one code, and the renderer abandons the identity on sight. After a successful submit followed by commit_outcome_unknown, a retry whose candidate set has changed raises action_conflict → identity discarded → the user sends again → the target receives the same work twice. The distinction that matters — "this identity is empty, remint it" vs "this identity may already have delivered" — exists only in the Host and never crosses IPC.

The convergent fix is to delete state, not add a check: make the journal the single authority and reduce the lease to requestId only. Dropping text/draft/state removes both of the above, plus preserveDraft and the last P3 below, and it stays inside workhub-send-lease.ts and workhub-surface.tsx — the Host side does not move. A text-equality guard would add a fourth cell to those tables, and this is already the second round.

Local — worth fixing in this PR

P1 — actionFingerprint omits the target, so a retry can be delivered to the wrong Session with nobody detecting it. workhub-coordination-action-gate.ts:564-572 covers {userText, disposition, assistantText?} but not targetSessionId / create.workspace. First delivery to A fails with session_busy (non-terminal, identity retained) → the user retries the same text → policy now routes to B → the fingerprint matches → #act short-circuits into #executeDelegation(durable) and still delivers to A, discarding the freshly resolved candidate. No party considers this an error, so delegation_abandoned does not cover it.

P2 — the epoch bump does not close the downgrade path it was raised for. RUNTIME_HOST_COMPATIBILITY_EPOCH 53 → 54 guards the Client↔Host handshake, but decodeMessage still throws on an unknown type and SQLITE_SESSION_METADATA_SCHEMA_VERSION is still 32. An older build opening a profile that already holds delegation_* records runs the same build on both sides, so the handshake never fails and recoverInterruptedSessionsStrict fails at startup instead. Either tolerate unknown message types on read, or reject the profile explicitly.

Ungraded: epoch 54 is also claimed by #3751 and #3390 against main = 53. scripts/protocol-epoch-check.mjs only compares head against origin/main, so it structurally cannot see sibling branches. Whichever lands first keeps 54; the other two need a rebase.

P3

  • session-catalog-coordinator.ts:151 STABLE_SESSION_INITIAL_REVISION = 1 is a second authority for "initial revision is 1" (sqlite-session-metadata-store.ts:917 is the first), and #create grows three WorkHub-specific callbacks, pushing a WorkHub policy into the generic catalog. Related: could you state where revision === 1 ⇒ pristine comes from? If a header is not written between message admission and Turn start, a qualified submit failure during recovery would remove a Session holding a pending admission.
  • 'unauthorized' in isTerminalWorkHubSurfaceFailure is dead — :209 already maps it to operation_unavailable.
  • workHubActError (runtime-host-workhub-ipc-main.ts:88-122) enumerates the codes a third time and silently degrades unknown ones to internal_failure, with no compile-time signal when a code is added.
  • acquireAttempt:106 writes already-sent text back into the persisted draft under preserveDraft, so a failure or reload makes a sent message reappear in the composer.

Simplification

workhub-delegation-journal.ts still reproduces workhub-coordination-coordinator.ts#record point for point and grew another 104 lines this round; one shared CoordinationRecordWriter covers both, and the split already showed as the read-path drain being fixed while the three write-path drains were not. Once delegation_abandoned exists, the whole create_new compensation chain guards a cosmetic leak rather than a correctness problem, and it is the heaviest runtime mechanism this PR adds. workhub-target-submission-recovery.ts reinterprets three kinds of evidence message-coordinator already owns; the minimal form is for turn.message.submit to be idempotent per messageId and self-describing about disposition.


AI-assisted review: a Claude Code subagent did adversarial analysis of the 1b352a04fdc25b4320 delta; I verified every graded finding against the new head myself — read acquireAttempt / send / isTerminalWorkHubSurfaceFailure for the first P1, enumerated the three state machines from source, and confirmed the epoch value and the sibling-branch collision. No tests, typecheck, or format:check run; revision === 1 ⇒ pristine unverified. AI review is not independent human review.

简体中文

问题定义正确,相对 main 是净改善,不阻塞。上一轮九条:五修、三部分修、一未动,新增七条——修复在一张张状态表上分别进行,这是根因的症状。

根因:"这次发送"有三个状态机(渲染 lease、turn 列表、Host journal),取值与终态定义都不等价且互不绑定。P1-1(旧文本复发)是 failed 只存在于其中一个状态机;P2-1(operation_conflict 过宽)是两侧终态定义不等价。收敛做法是删状态——让 journal 成为唯一权威,lease 只留 requestId——而不是补一处文本校验。

本 PR 内值得修:actionFingerprinttargetSessionId(会静默错投且无人察觉);epoch 54 没关上它想关的降级路径,且与 #3751#3390 撞号。

@likun666661

Copy link
Copy Markdown
Member

Architecture question: can Slice 5 be one atomic assignment followed by asynchronous consumption?

After tracing the current storage boundaries, I think this PR may be solving a local transactional problem as a distributed saga.

The important boundary does not appear to be "Coordination Session versus target Session". Within one Runtime Host, Session metadata/transcripts, message admissions, AgentRun admissions, and runtime-event authority are all persisted in the same operational-state database (runtime.sqlite) through leases of the shared OperationalStateDatabaseOwner. Cross-Host coordination is explicitly deferred.

The genuinely non-transactional effect is waking or continuing the in-memory executor, not recording facts for two Sessions.

That suggests a smaller causal spine:

resolve a bounded target
-> atomically persist target admission + Coordination linkage
-> commit
-> asynchronously wake/continue the target executor
-> recover the durable admission after a crash

Proposed atomic primitive

Instead of:

append delegation_intent
-> call turn.message.submit
-> recover three possible proof forms
-> append delegation_committed
-> ask the renderer to append a visible summary

could the Host expose one storage-level operation such as:

assignWorkHubMessage({
  actionId,
  coordinationSessionId,
  targetSessionId,
  userText,
  disposition,
  createContext?,
})

Under the target/Coordination admission authorities, its single SQLite transaction would:

  1. Revalidate the target and relevant target Turn state.
  2. For create_new, create the Session metadata in the same transaction.
  3. Insert an idempotent target pending-message/root-Turn admission using IDs derived from actionId.
  4. Append one immutable typed Coordination record, for example delegation_assigned, containing:
    • actionId
    • exact userText
    • disposition
    • targetSessionId
    • targetTurnId
    • targetMessageId
    • create context when applicable
    • any stable display fields needed by the WorkHub timeline
  5. Commit all of those facts together.

Only after commit would MessageCoordinator wake the target Session. If the Host dies after commit and before wake, normal recovery sees the durable pending admission and wakes it later. If it dies before commit, neither Session observes the assignment and retry is safe.

The resulting authoritative state machine is much smaller:

no delegation_assigned record
  = no target admission happened

delegation_assigned exists
  = target admission and Coordination linkage both happened

Target execution lifecycle remains owned by the ordinary Session and is projected into WorkHub; WorkHub does not create another active/settled/failed state machine for it.

Crash cuts

Failure cut Result
Before transaction commit Everything rolls back; same actionId can retry
After commit, before executor wake Durable admission exists; recovery wakes it
After executor wake Existing root-Turn/message recovery applies
IPC response lost Same actionId reads and returns the existing assignment
Renderer reloads Timeline projects delegation_assigned; no second summary write is required

There is still a need for a small client idempotency key until the Host acknowledges the request, but it can be only { hostScope, actionId }. Composer draft persistence remains on its existing seam. The renderer should not need to persist action text, active/settled state, or a frozen summary in the same blob.

Coordination transcript projection

The typed delegation_assigned record can itself project as the visible WorkHub turn:

User: Continue the payment regression
Assistant: Assigned to Payments

If the transcript contract requires ordinary user/assistant/state messages, those messages can instead be appended in the same transaction. Either form avoids the current post-commit renderer write and therefore removes the "target committed but Coordination summary failed" cut.

This also avoids paying five Coordination messages per successful delegation (intent + committed + user + assistant + state) unless all five have independently justified semantics.

create_new compensation and retirement

The current synchronous compensation chain appears unnecessary under this boundary.

Ideally, Session metadata creation and target admission are in the same transaction, so a failed assignment rolls the new Session back and there is no empty Session to discard.

If database-external resources have already been materialized, logical retirement should still be synchronous while physical cleanup is asynchronous:

one transaction:
  mark Session retirement_pending/tombstoned
  append an idempotent retirement outbox job
  exclude it from candidate/projection visibility
commit

background retirement worker:
  stop/verify execution
  delete runtime/artifact/workspace state
  mark cleanup complete

From the coordinator's perspective, committing the tombstone/outbox notification finishes the operation. It should not synchronously own discardRevision, retry session.remove, interpret an unknown cleanup outcome, or drain the Host because physical cleanup did not finish inline.

For this PR specifically, that would remove or sharply reduce:

  • delegation_intent -> delegation_committed/abandoned orchestration
  • WorkHub-specific three-source target-submission recovery
  • renderer text/summary/action-state coupling
  • the post-commit summary repair path
  • STABLE_SESSION_INITIAL_REVISION as a WorkHub cleanup authority
  • discardRevision / discardCreated
  • synchronous revision-guarded session.remove compensation

Why this may be the more Occam-shaped boundary

This is not necessarily the smallest diff: it requires a real transactional admission API below the current coordinator methods. But it has fewer independent authorities and fewer non-equivalent state machines.

The current design has renderer lease state, local React turn state, a Host intent/commit/abandoned journal, target admission state, and a separately persisted Coordination summary. The proposed design has one atomic assignment fact plus the target Session's existing execution state.

The questions I would like to resolve before adding more retry states are:

  1. Is any durable fact required for target admission or Coordination linkage actually outside runtime.sqlite or outside the shared transaction owner?
  2. Can turn.message.submit be split into "durably admit" and "wake/consume after commit"?
  3. Given that cross-Runtime-Host coordination is explicitly deferred, what failure requires a saga here rather than a local atomic assignment plus the existing recovery executor?
  4. Can logical retirement be the user-visible commit boundary, with physical deletion handled by an idempotent background consumer?

I am not suggesting that runtime execution itself belongs inside a SQLite transaction. The proposal is specifically to atomically commit the assignment and durable admission, then let execution consume that committed fact asynchronously.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/XL Over 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants