Skip to content

feat(workhub): resume delegated work from the coordination conversation - #4713

Merged
Astro-Han merged 13 commits into
apache:mainfrom
ARE404:feat/workhub-resume
Sep 6, 2026
Merged

feat(workhub): resume delegated work from the coordination conversation#4713
Astro-Han merged 13 commits into
apache:mainfrom
ARE404:feat/workhub-resume

Conversation

@ARE404

@ARE404 ARE404 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Summary

WorkHub can now resume explicitly named interrupted work through the existing Host resume operations. Resume remains a control action over the target Session: Coordination does not acquire a durable copy of its execution outcome.

Refs #3492

Behavior and boundary

  • Named resume uses the shared resolver and deterministic Host Action Gate. The proposal carries the assignment reference discovered by the existing candidate API; the Gate revalidates it against the target Session and trusted text.
  • Host execution uses turn.resume.query / turn.resume.start. Safe-boundary resume still requires MAKA_RUNTIME_SAFE_BOUNDARY_RESUME=1.
  • No resume action claim, metadata migration 39, new coordination record kind, or durable result card remains. The conversation uses its ordinary request acknowledgement; current execution feedback comes from the target Session.
  • A request-derived continuation Turn identity lets Host admission reject an old request attempting to resume a later interruption after restart. The PR no longer promises a durable coordination-level resume receipt.
  • Host lineage lookup keeps Stop attached to the continuation. The existing message execution query also follows that lineage, so the original WorkHub card displays the continuation's state rather than the interrupted Turn's state.

Compatibility

Compatibility epoch 120 covers the named resume proposal and its required assignment reference. The Host Gate remains the admission boundary, so the wire entry point is retained. The execution ledger's non-unique continuation index remains for bounded lineage lookup; the separate resume claim migration has been removed.

Verification

After merging main at 93224038a: full workspace build, typecheck, lint, format and protocol epoch checks pass; 291 affected tests pass.

  • Core, Storage, Runtime Host and Desktop production builds pass.
  • Workspace typecheck, lint, format, renderer architecture and protocol epoch checks pass.
  • Affected matrix: 298 passed. After the final message projection change, 177 affected owner/composition tests passed; these sets overlap.
  • Production composition covers interrupt, resume, restart with a fresh state-root owner, rejected old-request replay, a fresh resume, continuation feedback and Stop cancelling that continuation.
  • No full Desktop interactive recording or OS-level Host kill was performed. New remote CI must run after publication.

AI use

Generative tooling made a substantive contribution. Claude Code contributed the original implementation; Codex implemented the review fixes and this reduction under human direction and ran the stated verification.

@github-actions github-actions Bot added the effort/L Under 1000 readable lines label Sep 4, 2026

@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.

Reviewed at cbfb198. The problem is real (Slice 5's exit sentence names resume), and the core of the design holds: the resume goes through the Action Gate, the Gate revalidates the active set and the named Session on the Host, and planResume/resumeDelegation call the same turn.resume.query / turn.resume.start handlers the interrupted-turn banner uses. Deriving the continuation turnId from the delegation and source run is fine too; that id has always been caller-supplied, so this is a stronger caller, not a second authority. One P1 and four P2s before it can merge, plus an ordering constraint with #4699.

P1: after a resume, Stop Payments cannot stop the work and reports it terminated. The continuation Turn is admitted with content: null and no source message (root-turn-coordinator.ts:2873-2879), so readMessageExecutionDisposition keeps resolving the delegation to the original root turn/run. retireDelegation builds its identity from that disposition (execution-composition.ts:1503-1512) and stopOwnedWorkHubRoot compares it against the active registry (:2130), where the continuation lives under wht_…. Nothing is stopped; coordinator.read(original) is failed/cancelled (that is why the resume was allowed), so the outcome is already_terminal, the fact is committed, and the delegation leaves the active set while the continuation keeps running in the target Session. replace and readDelegationRetirement take the same path, so "observe" is wrong too. Fix at the owner: the Host resolves "latest run on this root Turn's lineage" (the continuation admission already carries sourceRunId/sourceTurnId), and both retireDelegation and planResume use it. That also deletes the next item.

P2: lineage is recovered by scanning the coordination transcript, which is exactly what #4699 removes. workhub-coordination-coordinator.ts:424-432 does readMessagesSnapshot(WORKHUB_COORDINATION_SESSION_ID) to find the previous delegation_resume_resolved. That (turnId, runId) is Host-owned state mirrored into the transcript. #4699 deletes readMessagesSnapshot from CoordinationStores and replaces the unbounded scan with a paged, incremental active-assignment projection, so this hunk will not typecheck after it lands; both branches also claim epoch 113. #4699 has to go first, and this PR should rebase onto it and take the Host-side lineage from the P1 fix instead of the scan.

P2: already_running is decided from the persisted run snapshot without asking the Host, and every failure becomes parked. execution-composition.ts:1425-1431 returns already_running for admitted/created/running, which is the state a Host crash leaves behind; the stop path has a recovering outcome for exactly this window and its comment explains why. Then :1418-1422, :1446-1452 and :1473-1476 collapse a bare catch, turn.resume.query park reasons and every !ok (host_draining, internal_failure, session_archived, …) into parked and commit it as an immutable delegation_resume_resolved. Forward the Host's park reason, let operation failures surface as errors rather than facts, and give resume a recovering exit.

P2: the Host execution side has no coverage. planResume / resumeDelegation (execution-composition.ts:1396-1483) are stubbed in the coordinator test (:1636-1648) and untouched by the gate test, so lineage selection, the already_running decision, the error-to-parked mapping and the P1 path are all outside the suite. The resume_work branches in protocol/workhub-coordination.ts:541-570, 681-691 are not in the protocol test either. The P1 scenario is the natural regression.

P2: continue / 继续 / restart / 重新开始 should not be resume verbs. Continue Payments and 继续支付任务 are how people hand the next instruction to a Session; with a Session of that name the text is consumed as a resume (workhub-controller.ts:505 returns before ordinary routing) and, on already_running, the user reads "no need to resume" and their message was never sent. Restart Payments maps to the same derived continuation Turn (workhub-coordination-action-gate.ts:1163), which is the opposite of what the word asks. The PR's own comment at workhub-creation-intent.ts:113-117 names the ambiguity; the name-match guard does not help when the whole message is the verb plus the name. Keep resume / 恢复 / 接着跑, drop the other four.

Smaller, in one pass with the above:

  • workhub-controller.ts:505-556 is :562-616 with four tokens changed; route-policy already extracted resolveNamedDelegationAction for the same reason. One submitNamedDelegationAction helper takes both to ~6 lines. Same in core: directWorkHubResumeCue/Target copy the stop pair line for line, and reuse UNSAFE_STOP_TARGET / stripMatchingStopQuotes under stop's name.
  • WorkHubRequestIntent.resume.cue has no reader that imperative does not already answer (workhub-route-policy.ts:190 vs :255).
  • whr_ + sha256(actionId) is already the runId prefix for assign; here it names a message id.
  • workhub-surface.tsx:586-588: the frame state reads stop?.outcome but not resume?.outcome, so a parked resume renders as a completed turn.
  • The body is out of date on three facts: epoch is 112 → 113, migration 39 rebuilds workhub_action_claims (SQLite cannot widen a CHECK in place; the migration itself is written correctly), and the Host-side stop does not use workHubNamedDelegationActionTargetsSession. Please rewrite the compatibility section before squash.

This is a user-visible surface (three new outcome states, two clarification prompts, three locales) with no before/after evidence in the body. Manual checks that remain after the fixes: delegate → interrupt → Resume XStop X, confirm the continuation actually stops and the card reflects it; kill the Host mid-run and resume after restart, confirm it does not answer already_running; a parked resume's frame state.

Evidence boundary: static read at cbfb198 against c180a2bac3 plus the #4699 branch; git merge-tree for the conflict; no build, no tests, no Desktop launched. The P1 chain is derived from the admission shape and isActiveWorkHubRoot, not observed.

AI-assisted review: drafted with Maka; I verified the retirement identity path, the transcript scan and the epoch/migration facts myself.

@github-actions github-actions Bot added effort/XL Under 2500 readable lines and removed effort/L Under 1000 readable lines labels Sep 5, 2026
@ARE404
ARE404 force-pushed the feat/workhub-resume branch from 4477263 to 6c62c31 Compare September 5, 2026 07:34

@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.

Re-reviewed at 6c62c31, reading the PR's own delta over the #4699 commit it sits on (28 files, +2159/−157). Every item from the last round is closed in code:

  • P1: readLatestRootTurnLineage (root-turn-coordinator.ts:523) walks safe_boundary_continuation admissions forward from the Host's own ledger, and readDelegationRetirement, retireDelegation and planResume all use it. The delegate → interrupt → ResumeStop regression goes through the production composition and asserts the continuation is cancelled.
  • Transcript scan: gone. Lineage comes from the Host; the two new coordination messages carry the plan and its outcome, and sourceRuntimeEventHighWater gives the persisted plan a real job on replay. Migration 39 rebuilds workhub_action_claims inside one BEGIN IMMEDIATE, no index or FK on that table, so it is correct.
  • already_running now asks the Host; recovering throws operation_unavailable without writing a fact; park reasons pass through; !ok throws.
  • planResume / resumeDelegation, the Gate and the resume_work protocol branches are under test through their owners.
  • Verbs are resume / 恢复 / 接着跑 only, with the four rejected forms asserted negative.
  • The duplicated controller blocks, cue, the whr_ reuse, resume?.outcome on the surface and the three stale body facts are all fixed.

One new P1, then three P2s, then ordering.

P1: in a default build, resume always fails and records the failure as a durable fact. safeBoundaryResumeEnabled is process.env.MAKA_RUNTIME_SAFE_BOUNDARY_RESUME === '1' (execution-composition.ts:1024), off unless set. With it off the kernel skips continuation inspection (runtime-kernel.ts:647) and turn.resume.query parks with resume_feature_disabled; planResume turns that into { kind: 'parked' } (execution-composition.ts:1432), and the coordinator commits delegation_resume_requested + delegation_resume_resolved for it, then the surface says "Could not carry this work on. Open its Session to see why". So on the shipped configuration the new command fails every time, sends the user to a Session that has nothing wrong with it, and leaves two immutable records per attempt. The one useful fact, the park reason, reaches WorkHubSubmission.parkReason (workhub-controller.ts:413) and nothing reads it. Treat resume_feature_disabled like recovering: no persisted fact, operation_unavailable, and a clarification that says the runtime has resume off (the CLI already has the wording at pi-tui-runner.ts:348). The body's Behavior section should state the flag.

P2: CI is red on formatting. Biome wants packages/core/src/session.ts:1729-1730 on one line; npm run format. The body says lint passed, which is a different check.

P2: resume and stop disagree on what "one delegation on this Session" means. #resumeSource (workhub-coordination-action-gate.ts:618-627) rejects whenever onTarget.length !== 1. #stopSource (:689-700) first drops delegations whose work is retired and accepts if one is still holding work. Delegate to Payments twice, let the first finish, interrupt the second: Stop Payments resolves, Resume Payments answers "this name matches several pieces of work". That is the rule the PR's own route-policy comment says must not happen, broken on the Host side. Reuse stop's retirement filter in one helper.

P2: lineage resolution reads the Session's whole root-admission ledger. readLatestRootTurnLineage calls listRootTurnAdmissionsForRecovery(sessionId), which materialises and chain-validates every root admission (up to 1 MB each) for the Session, and it now sits under readDelegationRetirement, which the stop path calls once per active delegation on the target. A long-lived Session pays an unbounded read on every stop, replace or resume; that is the shape #4699 just removed from the transcript, moved to a different ledger. Add a store query that returns only safe_boundary_continuation admissions for the Session (or walks forward from a given turnId) instead of the full chain.

Smaller:

  • parkReason on WorkHubSubmission has no reader outside tests; either it lands on screen with the P1 fix or it goes, same as cue last time.
  • isWorkHubCoordinationFailure replaces the deleted re-export with a new export; workhub-surface.tsx:30-38 already imports from the controller, so a direct value import of the class is one fewer concept. #4822's ask was three lines deleted, not one deleted and one added.
  • In resolveNamedDelegationAction the unnamedReason branch is unreachable for resume (imperative is only true with a target), and the two kind === 'resume' ? … : … return blocks in submitNamedDelegationAction have identical structure.

Ordering with #4699: unchanged, #4699 first, then rebase; the head is on an older #4699 commit and a5e1832283 has landed on that branch since. git merge-tree reports five conflicts, four mechanical (epoch table in protocol/index.ts, adjacent hunks in execution-composition.ts, two test files). The one that needs care: a5e1832283 changes sessionActions.assign from returning committedAssignment to taking a publishCommittedAssignment callback, in the same lines where this PR extracts the CoordinationSessionActions type (workhub-coordination-coordinator.ts:139-156); redo the extraction on the new signature. Keep #resume's beforeAppend calling the internal #listActiveAssignments as stop does, not the #admission.run-wrapped effect, or it re-enters the lease.

Manual acceptance after the fixes (three new outcome strings, two clarifications, three locales, no screenshots in the body): default build with the flag unset, Resume X should clarify rather than record; flag on, delegate → interrupt → Resume XStop X and watch the card; kill the Host mid-run, restart, Resume X should be retryable, not already_running; the two-delegations case above; parked frame state, light and dark, before/after.

Evidence boundary: static read of 6c62c3147e against 29367f7b1c and a5e1832283, plus one read-only biome format check; no build, no suites, no Desktop.

AI-assisted review: drafted with Maka; I verified the flag path, the resume/stop asymmetry and the conflict set myself.

@Astro-Han

Copy link
Copy Markdown
Contributor

Following up on the findings above with the thing that should come first: please reshape this PR to the smaller form below before addressing the P1/P2s individually, because most of them disappear with it. The delta over #4699 is +2159/−157 for a command whose whole job is "find the interrupted delegation on X, call the same turn.resume.query / turn.resume.start the banner calls, tell the card". I ablated it against stop, which is the reference shape on this branch (two facts, one side effect, no lineage walk), and about 58% does not survive. Two choices drive almost all of it.

1. Resume copies stop's two-phase "write the intent, then execute, then write the result" for an action that is not destructive. That is what forces delegation_resume_requested with an embedded plan (sourceTurnId / sourceRunId / sourceRuntimeEventHighWater / targetTurnId), the planResume / resumeDelegation split, the replay branch in the Gate, the ready/parked cross-field validation in core/session.ts, and the restart-replay test. Your own comment at workhub-coordination-action-gate.ts:612-614 makes the case for dropping it: "Unlike a stop this needs no claim to find its way back: resume changes no durable link". Replay safety is already carried by the action claim, the deterministic workHubResumedTurnId, and turn.resume.start's idempotency on an existing admission (root-turn-coordinator.ts:1783-1832). Recomputing the plan after a crash can only land on the continuation itself, already_running or recovering; none of those is a second execution. The banner and CLI path (runtime-host-session-driver.ts:487-502) is query → start in 15 lines with no persisted plan, and it has the same restart story.

2. The kernel's park reason is treated as a domain fact that must be persisted and transported. It moves from protocol/turn.ts into @maka/core/runtime-invocation, gets validated in two message kinds, decoded in the protocol, and lands on WorkHubSubmission.parkReason, where nothing reads it. The protocol comment at workhub-coordination.ts:235 says the reason "is not restated here", and line 242 restates it. already_running is the same pattern: computed up front from isActiveWorkHubRoot when the Host already parks a busy Session as session_busy.

What survives, and why: the intent verbs and directWorkHubNamedAction sharing; resolveNamedDelegationAction / resolveResume and the two clarifications; #resumeSource + workHubResumedTurnId + fingerprint + claimAction('resume') (this is where "not twice after restart" actually lives); migration 39 (one identity, one operation); readLatestRootTurnLineage or an equivalent, because without it Stop after Resume cannot find the continuation, though carrying the originating message identity on the continuation admission would let readMessageExecutionDisposition answer directly and skip the ledger walk (the third P2 above); one card branch with a two-entry outcome → copy table.

The shape I am asking for, concretely: one #resume in the coordinator that, under the claim, calls one resumeDelegation (disposition → lineage → terminal check → turn.resume.queryturn.resume.start) and commits one delegation_resume fact with the outcome, same field set as stop's resolved message plus userText / targetSessionName. Protocol: resume_work reuses stop's expects, result has two outcomes and no reason. Core: one message interface and validator. Tests that exist only for the removed mechanisms (persists a resume plan and result before replaying after Host restart, the Gate replay case, the requested half of the core decode test, the parked-reason controller test) go with them. My estimate is ~470 production and ~420 test lines against 1078 / 1080 now, with all four acceptance points intact.

This also folds the P1 in naturally: with no persisted request, resume_feature_disabled is just a clarification like recovering, and there is nothing to record. The resume/stop asymmetry (second P2) closes by reusing stop's retirement filter in #resumeSource, and the ledger walk (third P2) either becomes a targeted query or goes away if the continuation admission carries the originating message identity. Rebase onto #4699 once it lands, then push the reshaped version and I will re-read it as a whole.

Evidence boundary: static read of the delta plus the turn.resume.* implementation and the CLI driver; line counts are git diff --numstat split by hand, ±10%.

@ARE404
ARE404 force-pushed the feat/workhub-resume branch from 87b4c0b to db17b3f Compare September 5, 2026 09:47

@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.

Reviewed current head b2289fba53905374c717758b84a513d28bf20a5d (OPEN). One P1 and one P3 below. This is a feature; merging is a human decision regardless. Scope note: schema migration and protocol boundaries were reviewed line by line; the WorkHub controller/route-policy/surface files (~10 files) were not individually reviewed — flagged below, not covered.

P1 — the new unique index can break opening existing databases

sqlite-core-execution-schema.ts bumps the version 7 → 8 with a unique partial index on (session_id, sourceTurnId, sourceRunId) for safe_boundary_continuation rows. But that kind is not new — it already exists on main (runtime-invocation.ts:263) — and today's admitRootTurn dedupes only on turn_id, never checking the source triple. So two continuation records from the same boundary with different turn_ids can legally coexist today — exactly what the index wants to forbid.

If any installed database already holds such a pair, CREATE UNIQUE INDEX throws inside a bare db.exec that runs at database open (operational-state-store.ts:460, operational-target-schema.ts:71) — the outcome is not "feature unavailable" but "database won't open". There is no dedup or backfill step, and the new test only exercises the happy path on a fresh empty database. Whether such duplicates exist in the wild depends on whether historical flows could produce two turns from one boundary — that is for the author/maintainers to confirm, and the severity hinges entirely on the answer: impossible → P3 (missing migration test); possible → release-blocking.

Suggested fix: dedupe first (keep earliest or latest), or build a non-unique index and enforce uniqueness on the write path; either way, add a migration test with pre-existing duplicates.

P3 — epoch 113 is claimed by two open PRs; merge order matters

Main is at epoch 112. This PR writes 113 ("WorkHub exposes current active delegation linkage") plus 114 for itself — i.e. it stacks on a not-yet-merged sibling. Meanwhile PR #3299 independently claims 112 → 113 for a different reason. Whoever merges first takes 113; the other must renumber, or the epoch comments (the only trail for future cross-version handshake debugging) will mismatch real history. This is a merge-ordering dependency for planning, not a defect of this PR.

Verified good (worth stating)

The session-metadata migration 39 is exemplary: the comment explains why the table must be rebuilt (SQLite cannot relax a CHECK in place); rename-then-rebuild loses nothing (the v38 table deliberately had no indexes or foreign keys); explicit column lists on INSERT and SELECT; runs in a transaction (BEGIN IMMEDIATE); and a version-vs-entries-count assertion catches forgotten bumps. Likewise, readRootTurnContinuationAdmission re-validates the three fields after the index lookup instead of trusting json_extract blindly.

What I could not judge

The app was not run; resume-flow interaction unobserved. Whether duplicate boundary records exist in the wild (see P1) needs author/maintainer confirmation.


Automated review notice: This comment was posted by an automated review agent operated by Astro-Han. It is not an independent human review and does not replace one.

简体中文

本条结论全部来自 @Opus-Qronos-AstroHan 的审查。我自己没有读这份 diff;我核的是当前 head 有没有漂移。当前 head 是 b2289fb,未关闭。P1 是新唯一索引可能让已有数据库打不开,要作者确认历史数据形态。P3 是 113 纪元被两个 PR 同时占用,合并顺序决定谁改号。功能类合并由人类拍板。

@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.

Follow-up to my review above, same head b2289fba53905374c717758b84a513d28bf20a5d: this covers the controller/UI portion that the first review explicitly left out (supplemental review of workhub-coordination-port.ts, workhub-controller.ts, workhub-route-policy.ts, workhub-surface.tsx, preload and related copy). One P2 and two P3s; still no P0–P1 from either part. Feature — merging remains a human decision.

The biggest win here is deleted code: the renderer used to replay the whole coordination history (rebuildCompleteHistory: loop loadBefore until no older, then loadAround) on every reset — linear in conversation length — just to derive current state. Now it asks the host (deps.candidates()), which owns the authoritative data. Direction is right, which is exactly why the P2 below is worth fixing.

P2 — one Host IPC per transcript batch, with no coalescing

At the end of the batch callback, if (handle && (reset || changed || batch.ready)) scheduleRefresh(refreshGeneration) queues one deps.candidates() onto the serial refreshLane — per batch. Bursty transcript batches therefore produce one Host round-trip each. Please coalesce (e.g. trailing-edge or generation check) so a burst resolves to a single refresh.

P3 — resume with a verb but no clear object gets no feedback at all

Entry handling is asymmetric: stop (workhub-route-policy.ts) with a cue but no usable target falls into a clarification branch that asks the user back; resume with if (!action.imperative || !action.target) return { kind: 'not_requested' } silently treats "keep working on that thing" as never requested — the message lands as ordinary chat and the user likely believes an instruction was issued. Please clarify or execute.

P3 — resume_target_unavailable merges a transient failure with a permanent state

workhub-controller.ts:425-429 maps both operation_unavailable and host_not_ready to the same clarification. But host_not_ready is transient (host still starting; retry helps) while operation_unavailable is capability-based (this host never enabled safe-boundary resume; retry never helps). Please distinguish them so the user knows whether to wait or stop trying.


Automated review notice: This comment was posted by an automated review agent operated by Astro-Han. It is not an independent human review and does not replace one.

简体中文

本条是上一条的补充,同样来自 @Opus-Qronos-AstroHan 的审查,覆盖之前声明没审的控制器界面部分。P2 是每批转录都发一次 Host 请求没合并,另两条 P3 是无目标恢复没反馈、临时和永久失败混为一谈。功能类合并由人类拍板。

ARE404 and others added 10 commits September 6, 2026 08:43
Resume asks the Host to carry on work an interruption left unfinished.
It is admitted on the same terms as a stop — a direct speech act naming
one existing Session — so it reuses the stop reader's rules: a question
is not a command, a malformed literal is refused, and an anaphoric target
carries the cue without claiming a target so the surface can ask which
work rather than guess.

English covers resume / continue / restart and Chinese 继续 / 恢复 /
接着跑 / 重新开始, the same colloquial range each language already has for
stop. A cue alone resumes nothing: the reference must still resolve to a
Session, which is what keeps `continue with the refactor` ordinary work.

This is the reader only. The disposition that consumes it lands with the
Gate admission in the same change.

Generated-by: Claude Opus
Stop was the only way WorkHub could change delegated execution, so work an
interruption left unfinished could only be restarted by leaving the
conversation, opening the Session and using its own banner. Slice 5 asks
for resume from the coordination transcript, and the capability to do it
already exists.

Resume composes the two operations that banner uses — ask the Host whether
this Session has a continuation to make, then make it — and adds no
recovery machinery of its own. A repeat is safe because the Host parks a
continuation that already exists rather than forking a second one, so the
Turn identity is derived from the delegation and the source run rather
than minted, and two attempts name the same Turn.

It goes through the Gate for the same reason every other disposition does:
it changes execution state, and the Gate is where that is admitted. It
carries no confirmation, because it destroys nothing and grants no
authority a delegation did not already grant — it proves only that the
words named one Session and that the Session still owns one link.

`WorkHubActionOperation` gains `resume` so one action identity still means
one operation. That is a durable vocabulary change with no migration: the
column already exists, and only a Host that predates this value would
refuse to read a claim carrying it.

Refs apache#3492

Generated-by: Claude Opus
The Host could resume interrupted work, but only its own Session could ask:
the user had to leave WorkHub, open the Session and use its banner. This
wires the conversation to the disposition the Gate already admits.

Stop and resume ask the same question of the same words — which visible
Session does this reference name — so they now ask it with one matcher.
Two copies would be two chances for `Resume Payments` and `Stop Payments`
to disagree about which Session they mean.

They diverge on what an unnamed reference means, and that asymmetry is the
point. A stop must answer it: stopping is destructive, so `Stop it` has to
be met with a question rather than a guess. A resume must not: `继续这个工作`
is how someone carries on with the Session they are already in, and
answering it would take an ordinary instruction away from ordinary
routing. Resuming nothing costs nothing, so an unnamed resume falls
through and only a named one becomes an action.

The three answers the Host can give are all reported as themselves.
`already_running` and `parked` are outcomes, not failures: the first says
the work never stopped, and the second is the Host declining, whose reason
is its own.

Refs apache#3492

Generated-by: Claude Opus
@ARE404
ARE404 force-pushed the feat/workhub-resume branch from 1430523 to 200e858 Compare September 6, 2026 00:52

@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.

Reviewed current head 200e8583f2a2dcdaf2e1073ba2367c0c412d7499 (OPEN, MERGEABLE/BLOCKED awaiting human review). The previous P1 is fixed, correctly; both previous P3s are fixed. Remaining: one unrepaired P2, two new P3s from the fix itself, one unchecked epoch item — plus one still-open structural request noted at the end. test:success bound to this head. The increment over b2289fba is two commits, substantively 200e8583 (+203/-19, ten files).

P1 fixed, verified to the mechanism

The author took the suggested path: the index is now non-unique (DROP INDEX IF EXISTS + plain CREATE INDEX, sqlite-core-execution-schema.ts:174-183) — no more "database won't open" — with uniqueness moved to the write path (agent-run-store.ts:596-615 rejects a second admitRootTurn with the same source triple) and the reader changed from .get() to ORDER BY admitted_at, turn_id LIMIT 2 .all(), throwing on multiples instead of silently returning either. The new migration tests are exactly the requested shape (seed two sharing rows, assert migration doesn't throw and both rows survive; racing second admit rejected).

New P3 (1) — this index now rebuilds on every database open

migrateSqliteCoreExecutionDatabase is an unversioned run-everything idempotent function; every other index uses CREATE INDEX IF NOT EXISTS, only this one is DROP + unconditional CREATE. Understandable (early installs of this branch carry the UNIQUE version, so the DROP is needed once) — but it shouldn't live forever. Cheap fix: rename the new index (e.g. ..._v2) with CREATE INDEX IF NOT EXISTS, keeping the DROP for the old name. Rebuild cost unmeasured; upgrade to P2 if the table grows significantly.

New P3 (2) — legacy duplicates now open fine but hard-fail recovery with no way out

Migration no longer throws (good), but readRootTurnContinuationAdmission throws on that pair every time — recovery goes from "silently takes either" to "hard failure" with no step ever clearing the duplicates. Directionally hard-fail beats silent-wrong, but it needs an exit: either dedupe at migration (keep earliest per the reader's own ordering) or degrade to "no recoverable continuation" instead of throwing. The new migration test builds exactly such a DB but asserts only migration-doesn't-throw and rows-survive — not what reading then does, which is the path users will hit.

P2 unrepaired — refactored, same round-trips

refreshFeedback now calls delegationFeedback(references) (workhub-controller.ts:421,437) with a generation guard (:444) — but the guard drops stale results, not duplicate requests: batch callback :469 and session subscription :458 each still fire void refreshFeedback(), so N burst batches still send N IPCs. The :434 empty-references early return covers only the no-delegation case. Unchanged recommendation: coalesce into trailing-edge (or set a pending-refresh flag when one is in flight).

Epoch item explicitly not re-verified

200e8583 touches no epoch files; main's current epoch and the #3299 contention were not re-checked — treat as open, not resolved.

Still-open structural request (account-level, pre-existing)

Issue comment 5550754376 (2026-09-05) asks the author to reshape the PR to a smaller form before fixing findings line-by-line, on the grounds that much of the +2159/-157 does not hold up. The author has not replied and 200e8583 does not address it (114 lines trimmed, structure unchanged). Authorship within the shared account could not be confirmed, so it is neither adopted as this review's finding nor withdrawn — but as an unanswered structural request it should be resolved before sign-off: if the structure is to change, many of the above line-level conclusions would fall with it. Author/maintainers please respond to it directly.

What I could not judge

No app/test/migration runs locally (the "migrations run every open" conclusion is structural reading; index-rebuild cost and real-world duplicate existence unknown — the latter asked of the author last round with no written answer on this head). Whether the hosted green test run actually executed the new migration test files was not verified. WorkHub controller/route/UI files beyond this head's touchpoints remain unreviewed line-by-line, as before.


Automated review notice: This comment was posted by an automated review agent operated by Astro-Han. It is not an independent human review and does not replace one.

简体中文

评审结论来自自动化审查流程;发布者没有读这份 diff,核的是当前 head 有没有漂移、以及 exact-head 的门禁状态。当前 head 是 200e858,未关闭,检查绿。P1 修对了,两条旧 P3 修掉;残留 P2(突发连发)没修,修法带出两条新 P3(每次重建索引、无恢复出口),纪元没复核。另有一条之前账号级结构性要求作者还没回,拍板前要先处理。合并由人类定。

@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.

Re-reviewed at 200e858 against main (70d1516a0b), whole PR, two passes. Production +901/−150, tests +1144/−3, clean merge.

The requested reshape is done: no persisted plan, no park reason past protocol/turn.ts, one delegation_resume fact, stop and resume share #soleWorkingDelegation, resolveNamedDelegationAction and directWorkHubNamedAction, migration 39 under BEGIN IMMEDIATE with a non-unique continuation index. All earlier P1/P2/P3 closed. Thank you.

Two P2s apply in either shape, followed by a suggestion on the shape that is your call.

P2: a duplicate continuation poisons the whole Session (path ②). agent-run-store.ts:596-614 throws a bare Error when admitRootTurn finds an existing continuation for the same source, and root-admission-owner.ts:107-110 answers any throw there by adding the Session to #poisonedSessions, after which no root Turn on that Session is admitted for the rest of the Host's life. Trigger: a client sends turn.resume.start for an already-admitted continuation; Desktop (runtime-host-session-execution-ipc-main.ts:698) and CLI (runtime-host-session-driver.ts:491) mint a new turnId each time, and findExistingContinuation (runtime-resume.ts:480-491) looks for a run, so an admitted-but-unrun continuation is invisible to it. Same shape on the read side: readRootTurnContinuationAdmission (agent-run-store.ts:684-686) throws on two rows, and that read now sits on the existing stop path (execution-composition.ts:1386, :1508), so a historical fork migrates fine and then fails every stop. Smallest fix: return { kind: 'conflict', admission } from admitRootTurn (the operationConflict branch in startTurnResume already handles it), and let the reader take the first row by admitted_at, turn_id, which is already the ORDER BY.

P2: zh-TW users cannot issue a resume, and the zh-TW copy tells them the one phrasing that never matches (path ①). DIRECT_CHINESE_RESUME_REQUEST (workhub-creation-intent.ts:115) accepts only 恢复 and 接着跑; the zh-TW hint at workhub-surface.tsx:980 says 恢復 支付任務. 恢復支付任務, 接著跑支付任務 and 恢復它 all miss, with no clarification; the text takes the ordinary route and, if 支付任務 matches a Session, is delegated there as a new instruction. Stop is unaffected because 停止 is the same in both scripts. Fix: 恢复|恢復|接着跑|接著跑.

Suggestion: ship resume as the smaller slice first, and add the coordination record later if it earns its place. Stop is a coordination action because it retires a delegation link. Resume changes no link (your Gate comment says so, and the domain language treats a target's running state as a projection) and only presses the same "continue" the target's banner presses, for which sessions:resumeLatest(sessionId) already exists in the preload (bridge-contract.d.ts:1196, runtime-host-session-execution-ipc-main.ts:689). The smaller slice keeps the intent verbs and shared named-Session resolution, calls resumeLatest(targetSessionId) from the renderer with the two clarifications and park mapped to the existing "not enabled" / "still recovering" copy, and lets the projection show the target running again. On the Host, keep only what main lacks for either resume trigger: readLatestRootTurnLineage, its index, and the admission-conflict fix above, so Stop X finds the continuation. The Gate branch and claim, migration 39, resume_work and the epoch bump, the core message type, the coordinator #resume and the fact go with the premise, about two thirds of the PR, and none of it then ships dark behind MAKA_RUNTIME_SAFE_BOUNDARY_RESUME. What it gives up is the transcript card saying a resume was issued from here. If that card is part of the feature, keep the current shape and I will approve once the two P2s land; if not, I would rather merge the smaller slice first. Your call.

P3, current shape only:

  • Committed resume cards never show live: visibleWorkHubConversation (workhub-surface.tsx:136-138, :145-147) admits stop but not resume, so props.turn.resume?.outcome and fallbackName at :586 / :613 only run after a reload; add 'resume' to both and a test on that branch.
  • A crash between claimAction and the fact commit makes the retry record already_running or throw operation_conflict, showing failure for completed work; no second execution, worth a sentence in the body.
  • Neutral copy: resumeTargetUnavailable also covers operation_conflict for non-terminal, completed or changed lineage (execution-composition.ts:1406, :1427, :1450), and unauthorized folds into operation_unavailable (workhub-coordination-coordinator.ts:579) where the copy says the feature is disabled.
  • #resumeSource (gate :577-579) lacks the explicit claim.subject !== resolved.delegationId check #stopSource has (:634-641).
  • Dead: the post-checks at agent-run-store.ts:697-704 restate the SQL WHERE; resumeActionFingerprint rechecks disposition !== 'resume_work'; resumeRecorded equals stopRecorded in all three locales; WorkHubSubmission.resume.targetTurnId has no reader outside tests; UNSAFE_STOP_TARGET kept its name after stripMatchingActionQuotes was renamed.

Body corrections: epoch is 118 here over 117 on main, not 114; "targeted admission-ledger queries" in readLatestRootTurnLineage (root-turn-coordinator.ts:523-542) is a chain of indexed point reads, one per prior resume, not a single query.

Ledger and hygiene: the four renderer files are already in legacyRendererFiles; no new file, import edge or app-shell change; the locale-hygiene comparison against base is empty for all five touched files.

Evidence boundary: static read of the PR against main plus runtime-resume.ts, root-admission-owner.ts, both client resume call sites and the banner IPC; regex results by executing the head's literals; no build, no suites, no Desktop. I did not run a banner resume followed by a WorkHub stop on main; that stop targets disposition.turnId directly there is from reading.

AI-assisted review: two Maka passes (Host/core/storage and renderer); I verified the reshape checklist, the admission-owner poisoning, the banner IPC, the regex gap and the card whitelist myself.

简体中文

重塑已按上轮要求做全,此前的 P1/P2/P3 全部关闭。两条 P2:admitRootTurn 遇到同一 source 已有 continuation 时抛裸 Errorroot-admission-owner.ts:107-110 会把整个 Session 加进 #poisonedSessions,本 Host 生命周期内该 Session 任何 root Turn 都进不来,读侧 readRootTurnContinuationAdmission 在历史分叉库上同样抛错并挂在 stop 路径上,改为返回已有的 conflict 结果、读侧取首行即可;繁体正则缺 恢復/接著跑,而 zh-TW 提示恰恰教用户输入 恢復 支付任務,不匹配就走普通路由被当成新指令派出去。建议先合最小切片:resume 不改任何委派链接,renderer 直接调已有的 sessions:resumeLatest,Host 只留 lineage 查找和上面的 conflict 修复,Gate、claim、迁移 39、协议、事实记录后置。这会放弃协调会话里那张"在此恢复过"的卡片;你认为卡片是功能一部分就保留现状,两条 P2 修完我 approve,由你决定。P3 和正文勘误见上文。

@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.

Re-reviewed at 08c3500 against main (a03dd577bb); the increment over 200e858 is +371/−64 across 16 files, CI green, clean merge.

Both P2s are fixed at the owner. admitRootTurn now returns { kind: 'conflict', admission } for a second continuation of the same source instead of throwing, root-admission-owner.ts:89-95 only accepts a conflict whose owner is already in the recovered chain, and root-turn-coordinator.ts:1993-2000 turns it into operationConflict, so the Session stays usable. The restart case is covered: recoverSession (:64-76) loads every admission for the Session and hosted-execution-recovery.ts:62-65 recovers every Session at startup, so the owner is always known in memory on a supported path. Write-path uniqueness holds because the SELECT and INSERT share the write transaction, and admit and read use the same ORDER BY, so the removed post-read check was dead. The Traditional regex matches 恢復支付任務 and 接著跑支付任務. The rest of the increment is consistent with stop: #resumeSource binds a replayed action to its claimed delegation, unauthorized is no longer folded into operation_unavailable, and the resume outcome is filtered from the local list.

One thing I need before this merges, and it is not code. My last review proposed the smaller slice (the renderer calls the existing sessions:resumeLatest, the Host keeps only the lineage lookup so Stop can follow a continuation) and left the choice to you. The head keeps the full shape and grew by another ~300 lines, and there is no comment, inline reply, body paragraph or commit message saying why. I am fine with either shape, but not with the larger one by default: the PR is +2356/−157 for a command whose job is "find the interrupted delegation, call the resume the banner already calls, tell the card", and the reader who meets this code in a year needs to know what the coordination claim, the durable delegation_resume fact and the conflict path buy that resumeLatest does not. Please either reshape to the slice, or write into the PR body the concrete case the slice cannot handle (a restart, a replay, a Stop-after-Resume that would go wrong). Once that paragraph exists I will approve at this head; the P3s below do not block.

P3:

  • workhub-controller.ts:400-405 classifies operation_conflict by regex over the English error.message, because the coordinator (workhub-coordination-coordinator.ts:584-591) collapses five gate codes into one. Correct today (the two sentences are exactly the two #soleWorkingDelegation throws), but the Host's prose is now renderer control flow; a distinct code is the smaller shape. The rethrown branch matters more: execution-composition.ts:1410/1431 WorkHub delegated execution is not resumable (the delegation simply finished) lands on action_changed, "this action has changed, resend", which is untrue and asks the user to repeat something that will fail again. Behind MAKA_RUNTIME_SAFE_BOUNDARY_RESUME=1, so P3, but "finished, nothing to resume" deserves its own sentence.
  • Traditional coverage stops at the verbs: workhub-creation-intent.ts:112/116 accept only 请|请帮我|帮我|麻烦 and 这个|该(会话|工作|任务), and UNSAFE_NAMED_ACTION_TARGET lists Simplified anchors only, so 請恢復支付任務 does not match and 恢復這個 yields target 這個; both fall to not_requested and get routed as a new instruction. One token each: 請|請幫我|幫我|麻煩, 這個, 會話|工作|任務|那個|當前. Same gap exists for stop on main.
  • The two-row fork test is reachable only by driving the store directly; a database in that state fails orderRootTurnAdmissionChain before the Host starts.

Evidence boundary: static read of the head and the interdiff; the regexes were executed against the head literals; no build, no test run, no Host restart.

AI-assisted review: drafted with Maka; I verified the conflict path, recoverSession, the transaction boundary and the renderer's failure mapping myself.

简体中文

两条 P2 都在权威处修好了,重启和事务边界我核过。合并前还差一件事,不是代码:上一轮我提了更小的切片(renderer 直接调现有的 sessions:resumeLatest,Host 只留 lineage 查找),选择权给了你。现在 head 保留完整形状又多了约 300 行,但评论、正文、commit message 里没有一句为什么。两种形状我都接受,不接受的是默认选大的那个:请要么改成小切片,要么在正文写清小切片处理不了的具体场景(重启、重放、Resume 之后 Stop 哪里会出错)。写清了我就在这个 head 上 approve,下面三条 P3 不阻塞。

@github-actions github-actions Bot added effort/XXL Over 2500 readable lines and removed effort/XL Under 2500 readable lines labels Sep 6, 2026
@ARE404

ARE404 commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for pressing on this distinction. The current PR does not extend sessions.resumeLatest directly. That API is a Desktop IPC adapter which performs turn.resume.query followed by turn.resume.start; the WorkHub path performs the same Host operations, so it does not introduce a second kernel resume mechanism, but it does add a parallel coordination layer around them.

I kept the full shape for two product semantics beyond merely making the target Session run again:

  1. A durable WorkHub action record. A resume issued from the coordination conversation remains visible there after renderer reload, Host restart, or another client reconnects. The delegation_resume fact records whether that command started a continuation or found it already running. A transient toast plus the target Session projection would not preserve that history.

  2. Delegation-level action idempotency. Host admission provides execution idempotency: it prevents two continuations from being started for the same source boundary. It does not by itself preserve the meaning of a WorkHub action across a lost response. For example, action R is admitted for delegation A, the response is lost, A later retires or is replaced, and delegation B becomes the sole active delegation on the same Session. Retrying R through a Session-wide resumeLatest(sessionId) would resolve against current Session state and could target B. The WorkHub action claim pins R to A, so that retry either converges on A or conflicts; it cannot silently migrate to B.

Those are the concrete things the smaller slice gives up. If the intended product contract is only “safely resume whatever the Session currently considers latest,” then I agree the smaller resumeLatest slice is the better design and the claim, migration, coordination fact, and durable card should be removed. For this PR I am treating the durable coordination history and stable delegation binding as part of the feature, which is why I kept the full shape.

@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.

Thanks for writing down the rationale; it clarifies the core finding. The overall shape departs from the WorkHub architecture fixed in #3492 and #3286, so I am treating this as a design finding (P1), not an implementation P2/P3, and reviewing the shape, not the code.

What the accepted design says. Three rules from #3286 and the #3492 authority table:

  1. WorkHub projections own nothing durable. Cards, status summaries and navigation aids are rebuildable views of ordinary Session facts; the target Session owns execution, recovery and the authoritative execution transcript.
  2. Delegation links transcripts instead of copying them. The Coordination Session records user intent and a bounded link (delegationId, coordinationTurnId, targetSessionId, targetTurnId, disposition, status), copying nothing from target execution.
  3. Every action goes through the same deterministic Action Gate, revalidating target, Host, lifecycle, idempotency and confirmation immediately before the effect. Stop binds to the exact WorkHub-owned Turn because Stop retires a link.

You restated 1 and 2 on #4868 (#4868 (comment)): Coordination owns the link, replacement relation, action claim and audit; the target Session owns queued/running/terminal facts.

Where this PR departs.

  • delegation_resume records outcome: resume_started | already_running and targetTurnId. Whether the target is running is a target Session execution fact; writing it into Coordination and rendering the card from it makes the card a durable copy, not a projection (rule 1). The record has 15 fields versus the 6-field link (rule 2) and copies userText, already present in the same transcript.
  • The claim (workhub_action_claims migration 39) binds a destructive action to one Turn so a replay cannot retire a different link. Resume retires nothing; Host admission already makes turn.resume.start idempotent via the deterministic Turn id. The claim's only added effect, the A-retired-then-B-replaced-then-retried path, needs three unsupported premises and ends in resuming the latest continuation, which is what the user asked for.
  • Durable resume history in the coordination conversation and delegation-level idempotency are product commitments absent from #3492 Slice 5 and #3286. They change the authority table and belong in #3492 or a Discussion before an implementation is reviewed; a PR cannot decide them.

The shape that fits the design. Intent → existing Session resolver → Action Gate (target exists, same Host, not archived, this delegation's Session) → the Host's own turn.resume.query / turn.resume.start. The Coordination transcript keeps the user message; the card projects from target Session state like every other card. No claim, migration, new record kind or epoch bump. That is most of your resumeDelegation effect in execution-composition.ts already, so the reshape is mostly deletion.

Next step. Either reshape to that, or open the two semantics in #3492 / a Discussion and park this PR until the contract is settled. I will not approve the full shape on code quality; the code is careful, the boundary is the problem.

中文

感谢写下设计理由,这明确了核心发现。这不是实现层面的 P2/P3:整体形态偏离了 #3492#3286 确立的 WorkHub 架构,所以我把它定为设计层面的发现(P1),评的是形态而不是代码。

已采纳的设计怎么说。 来自 #3286 已采纳提案与 #3492 权威表的三条规则:

  1. WorkHub projection 不持有任何持久事实。 卡片、状态摘要和导航辅助都是普通 Session 事实的可重建视图;目标 Session 持有执行、恢复和权威的执行 transcript。
  2. Delegation 只链接 transcript,不复制。 Coordination Session 只记录用户意图和一个有界链接(delegationIdcoordinationTurnIdtargetSessionIdtargetTurnIddispositionstatus),不复制目标执行的任何内容。
  3. 每个 action 都走同一个确定性 Action Gate,在产生效果前重新校验目标、Host、生命周期、幂等和确认。Stop 绑定到确切的 WorkHub-owned Turn,因为 Stop 会退休一条链接。

你在 #4868https://github.com/apache/maka/discussions/4868#discussioncomment-18310950)也重申了规则 1 和 2:Coordination 持有链接、替换关系、action claim 和审计;目标 Session 持有 queued/running/terminal 事实。

本 PR 偏离之处。

  • delegation_resume 记录了 outcome: resume_started | already_runningtargetTurnId。目标是否在运行是目标 Session 的执行事实;把它写进 Coordination 并据此渲染卡片,卡片就成了持久副本而不是 projection(违反规则 1)。这条记录有 15 个字段,链接只有 6 个(违反规则 2),还复制了同一 transcript 里已有的 userText
  • claim(workhub_action_claims migration 39)的作用是把破坏性 action 绑定到一个 Turn,防止重放退休掉另一条链接。Resume 不退休任何东西;Host admission 已经通过确定性 Turn id 保证了 turn.resume.start 幂等。claim 唯一多出来的效果是"A 退休、B 顶上、再重试"这条路径,它需要三个不受支持的前提同时成立,而结果仍是恢复最新的 continuation,正是用户要的。
  • 协调对话里的持久 resume 历史、delegation 级幂等,这两条是 #3492 Slice 5 和 #3286 里都没有的产品承诺。它们改动权威表,应先进 #3492 或 Discussion 定案再评实现;PR 不能替它们做决定。

符合设计的形态。 意图 → 现有 Session 解析器 → Action Gate(目标存在、同 Host、未归档、属于该 delegation 的 Session)→ Host 自己的 turn.resume.query / turn.resume.start。Coordination transcript 只留用户消息;卡片像其他卡片一样从目标 Session 状态做 projection。不需要 claim、migration、新记录类型和 epoch bump。这大部分就是你在 execution-composition.ts 里的 resumeDelegation,所以重塑主要是删代码。

下一步。 要么按这个形态改,要么把这两条语义拿到 #3492 / Discussion 定契约,本 PR 先挂起。我不会因为代码质量批准完整形态;代码写得很仔细,问题在边界。

@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.

Reviewed head 4ce0ea2b7 (OPEN, MERGEABLE). The design P1 from my last review is closed: you chose the reshape over the rationale, and the result fits the architecture. Approving.

What the reshape deleted

7570102dd removed the durable delegation_resume coordination message, its transcript projection (projectWorkHubCoordinationTurns), the resumeRecorded copy in all three locales and the persisted card branches in the renderer. Net against the previous head is +239/−412; against main (merge-base 93224038a) the PR is +2179/−156 across 28 files with no overlap with main's one commit since (#4852 touches session.ts/ai-sdk-turn.ts only).

What remains matches the #3286/#3492 rules:

  • No durable copy of target execution in Coordination. A resume is admitted and executed through the Gate and the Host's own turn.resume.query/turn.resume.start; the only outcome that returns is the transient act result. No claim, no migration, no new record kind, no epoch-carrying resume fact.
  • The card stays a projection. readMessageExecutionDisposition now walks readLatestRootTurnLineage, so turn.message.execution.query resolves the original WorkHub card to the running continuation rather than the interrupted Turn — that is the P1 stop fix and the card fix in one owner.
  • Host-side machinery is shared, not WorkHub-private. The continuation-admission conflict path (admitRootTurn{ kind: 'conflict' }, root-admission-owner accepting only a conflict inside the recovered chain, root-turn-coordinator turning it into operationConflict) lives in turn.resume.start, so the banner/CLI resume paths get the same protection.

Earlier P2/P3s, verified closed in code

  • readLatestRootTurnLineage is a chain of indexed point reads per prior continuation, not a full ledger scan.
  • Stop and resume share #soleWorkingDelegation, so both apply the same retirement filter; the two-delegations asymmetry is gone.
  • resume_feature_disabled surfaces as operation_unavailable → the "safe-boundary resume is not enabled" clarification with no recorded fact; host_not_ready stays distinct (resume_host_recovering, retryable).
  • zh-TW verbs 恢復/接著跑 are in the intent regex; Continue/Restart/继续/重新开始 remain excluded with negative tests.
  • The production composition test covers delegate → interrupt → Resume → close the owner and reopen (in-memory Gate state discarded) → replay of the same actionId fails operation_conflict → a fresh actionId resumes onto a new continuation → the card query resolves to the continuation → Stop Payments cancels exactly that continuation. That is the P1 regression, the restart story and the lineage projection in one chain.

Remaining P3s (non-blocking)

  • The renderer classifies operation_conflict by regex over the Gate's English prose (workhub-controller.ts) because the coordinator folds all Gate failures into one code. Correct today; fragile to copy changes. A distinct code would be the smaller shape.
  • A delegation changed race (candidate read vs act) falls through the regex and surfaces as a terminal failure rather than a clarification. Fail-closed and rare; a retry is the right answer either way, so I did not block on it.
  • The Gate's resume effect throwing operation_unavailable when the flag is off means the first user-visible contact with the feature on a default build is the clarification — worth confirming in the manual pass below, not a code defect.

Manual acceptance (not run here)

The test suite is green on this head, but I did not launch the app. Please confirm once on a real Desktop build, both light and dark: default build (flag unset), Resume X shows the "not enabled" clarification and leaves no record; flag set, the full delegate → interrupt → Resume XStop X flow with the card following the continuation; Host kill mid-run, restart, Resume X is retryable rather than already_running.

AI-assisted review: drafted with Maka; I verified the deleted-record diff, the Gate/effect error mapping, the lineage walk and the conflict path against the head myself.

简体中文

head 4ce0ea2b7(OPEN,MERGEABLE)。上轮的设计 P1 已闭合:你选择 reshape 而非保留完整形状的辩护,结果符合架构。approve。

缩掉的东西7570102dd 删除了持久 delegation_resume coordination 消息、transcript 投影、三种语言的 resumeRecorded 文案和 renderer 的持久卡片分支。相对上一 head +239/−412;相对 main(merge-base 93224038a)+2179/−156、28 文件,与 main 之后唯一提交(#4852,只碰 session.ts/ai-sdk-turn.ts)无重叠。

留下的形态符合 #3286/#3492 规则:Coordination 不再持有目标执行的持久副本(无 claim、无 migration、无新记录类型);卡片保持投影(disposition 走 readLatestRootTurnLineageturn.message.execution.query 把原卡片解析到运行中的 continuation——stop 修复和卡片修复在同一个权威处);Host 侧机制是共享的(continuation admission 冲突路径在 turn.resume.start,banner/CLI resume 同样受益)。

此前 P2/P3 已逐条在代码里验证闭合:lineage 是逐跳索引点读;stop/resume 共享 #soleWorkingDelegation(同样的 retirement filter);flag off 时是 operation_unavailable → "未启用安全边界恢复"澄清、不记录 fact,host_not_ready 保持可重试;zh-TW 恢復/接著跑 已入动词集,Continue/Restart/继续/重新开始 仍有负向测试排除。生产 composition 回归一条链覆盖了 delegate → interrupt → Resume → 重开 owner(丢内存)→ 同 actionId 重放得 operation_conflict → 新 actionId 起新 continuation → 卡片查询投影到 continuation → Stop Payments 精确取消它。

剩余 P3(不阻塞):renderer 靠英文文案正则给 operation_conflict 分类(coordinator 把所有 Gate 失败折叠成一个 code),文案一改就脆;delegation changed 竞态会落到 terminal failure 而非澄清(fail-closed,重试即对);flag off 时用户第一次接触该功能是澄清文案。

手工验收(我未运行):head 上测试绿,但我没启动应用。请真实跑一次(light/dark 都要):默认 build(不设 flag)Resume X 显示未启用澄清且无记录;设 flag 后完整 delegate → interrupt → Resume X → Stop X,卡片跟随 continuation;Host 中途被杀、重启后 Resume X 可重试而非 already_running。

AI 辅助评审:Maka 起草;删除差异、Gate/effect 错误映射、lineage 遍历和冲突路径我对着 head 亲自核过。

@Astro-Han
Astro-Han merged commit 8bb23a3 into apache:main Sep 6, 2026
1 check passed
@ARE404
ARE404 deleted the feat/workhub-resume branch September 6, 2026 15:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/XXL Over 2500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants