feat(workhub): resume delegated work from the coordination conversation - #4713
feat(workhub): resume delegated work from the coordination conversation#4713ARE404 wants to merge 11 commits into
Conversation
Astro-Han
left a comment
There was a problem hiding this comment.
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-556is:562-616with four tokens changed; route-policy already extractedresolveNamedDelegationActionfor the same reason. OnesubmitNamedDelegationActionhelper takes both to ~6 lines. Same in core:directWorkHubResumeCue/Targetcopy the stop pair line for line, and reuseUNSAFE_STOP_TARGET/stripMatchingStopQuotesunder stop's name.WorkHubRequestIntent.resume.cuehas no reader thatimperativedoes not already answer (workhub-route-policy.ts:190vs:255).whr_+ sha256(actionId) is already the runId prefix forassign; here it names a message id.workhub-surface.tsx:586-588: the frame state readsstop?.outcomebut notresume?.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 useworkHubNamedDelegationActionTargetsSession. 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 X → Stop 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.
4477263 to
6c62c31
Compare
Astro-Han
left a comment
There was a problem hiding this comment.
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) walkssafe_boundary_continuationadmissions forward from the Host's own ledger, andreadDelegationRetirement,retireDelegationandplanResumeall use it. The delegate → interrupt →Resume→Stopregression goes through the production composition and asserts the continuation iscancelled. - Transcript scan: gone. Lineage comes from the Host; the two new coordination messages carry the plan and its outcome, and
sourceRuntimeEventHighWatergives the persisted plan a real job on replay. Migration 39 rebuildsworkhub_action_claimsinside oneBEGIN IMMEDIATE, no index or FK on that table, so it is correct. already_runningnow asks the Host;recoveringthrowsoperation_unavailablewithout writing a fact; park reasons pass through;!okthrows.planResume/resumeDelegation, the Gate and theresume_workprotocol branches are under test through their owners.- Verbs are
resume/恢复/接着跑only, with the four rejected forms asserted negative. - The duplicated controller blocks,
cue, thewhr_reuse,resume?.outcomeon 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:
parkReasononWorkHubSubmissionhas no reader outside tests; either it lands on screen with the P1 fix or it goes, same ascuelast time.isWorkHubCoordinationFailurereplaces the deleted re-export with a new export;workhub-surface.tsx:30-38already 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
resolveNamedDelegationActiontheunnamedReasonbranch is unreachable for resume (imperativeis only true with atarget), and the twokind === 'resume' ? … : …return blocks insubmitNamedDelegationActionhave 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 X → Stop 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.
|
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 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 2. The kernel's park reason is treated as a domain fact that must be persisted and transported. It moves from What survives, and why: the intent verbs and The shape I am asking for, concretely: one This also folds the P1 in naturally: with no persisted request, Evidence boundary: static read of the delta plus the |
87b4c0b to
db17b3f
Compare
Astro-Han
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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 是无目标恢复没反馈、临时和永久失败混为一谈。功能类合并由人类拍板。
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
1430523 to
200e858
Compare
Astro-Han
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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) admitsstopbut notresume, soprops.turn.resume?.outcomeandfallbackNameat:586/:613only run after a reload; add'resume'to both and a test on that branch. - A crash between
claimActionand the fact commit makes the retry recordalready_runningor throwoperation_conflict, showing failure for completed work; no second execution, worth a sentence in the body. - Neutral copy:
resumeTargetUnavailablealso coversoperation_conflictfor non-terminal, completed or changed lineage (execution-composition.ts:1406,:1427,:1450), andunauthorizedfolds intooperation_unavailable(workhub-coordination-coordinator.ts:579) where the copy says the feature is disabled. #resumeSource(gate:577-579) lacks the explicitclaim.subject !== resolved.delegationIdcheck#stopSourcehas (:634-641).- Dead: the post-checks at
agent-run-store.ts:697-704restate the SQL WHERE;resumeActionFingerprintrechecksdisposition !== 'resume_work';resumeRecordedequalsstopRecordedin all three locales;WorkHubSubmission.resume.targetTurnIdhas no reader outside tests;UNSAFE_STOP_TARGETkept its name afterstripMatchingActionQuoteswas 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 时抛裸 Error,root-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
left a comment
There was a problem hiding this comment.
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-405classifiesoperation_conflictby regex over the Englisherror.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#soleWorkingDelegationthrows), 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/1431WorkHub delegated execution is not resumable(the delegation simply finished) lands onaction_changed, "this action has changed, resend", which is untrue and asks the user to repeat something that will fail again. BehindMAKA_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/116accept only请|请帮我|帮我|麻烦and这个|该(会话|工作|任务), andUNSAFE_NAMED_ACTION_TARGETlists Simplified anchors only, so請恢復支付任務does not match and恢復這個yields target這個; both fall tonot_requestedand get routed as a new instruction. One token each:請|請幫我|幫我|麻煩,這個,會話|工作|任務|那個|當前. Same gap exists for stop onmain. - The two-row fork test is reachable only by driving the store directly; a database in that state fails
orderRootTurnAdmissionChainbefore 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 不阻塞。
|
Thanks for pressing on this distinction. The current PR does not extend I kept the full shape for two product semantics beyond merely making the target Session run again:
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 |
Astro-Han
left a comment
There was a problem hiding this comment.
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:
- 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.
- 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. - 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_resumerecordsoutcome: resume_started | already_runningandtargetTurnId. 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 copiesuserText, already present in the same transcript.- The claim (
workhub_action_claimsmigration 39) binds a destructive action to one Turn so a replay cannot retire a different link. Resume retires nothing; Host admission already makesturn.resume.startidempotent 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 权威表的三条规则:
- WorkHub projection 不持有任何持久事实。 卡片、状态摘要和导航辅助都是普通 Session 事实的可重建视图;目标 Session 持有执行、恢复和权威的执行 transcript。
- Delegation 只链接 transcript,不复制。 Coordination Session 只记录用户意图和一个有界链接(
delegationId、coordinationTurnId、targetSessionId、targetTurnId、disposition、status),不复制目标执行的任何内容。 - 每个 action 都走同一个确定性 Action Gate,在产生效果前重新校验目标、Host、生命周期、幂等和确认。Stop 绑定到确切的 WorkHub-owned Turn,因为 Stop 会退休一条链接。
你在 #4868(https://github.com/apache/maka/discussions/4868#discussioncomment-18310950)也重申了规则 1 和 2:Coordination 持有链接、替换关系、action claim 和审计;目标 Session 持有 queued/running/terminal 事实。
本 PR 偏离之处。
delegation_resume记录了outcome: resume_started | already_running和targetTurnId。目标是否在运行是目标 Session 的执行事实;把它写进 Coordination 并据此渲染卡片,卡片就成了持久副本而不是 projection(违反规则 1)。这条记录有 15 个字段,链接只有 6 个(违反规则 2),还复制了同一 transcript 里已有的userText。- claim(
workhub_action_claimsmigration 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 先挂起。我不会因为代码质量批准完整形态;代码写得很仔细,问题在边界。
Summary
WorkHub could delegate, observe, correct, and stop work, but it could not carry interrupted delegated work on from the coordination conversation. This PR adds a narrow named command—
Resume Payments/恢复支付任务/接着跑支付任务/恢復支付任務/接著跑支付任務—that reuses the Host's existing safe-boundary resume operations.Refs #3492
Behavior
Continue Payments,继续支付任务, and restart requests remain ordinary work.turn.resume.query/turn.resume.start, and records onedelegation_resumefact with eitherresume_startedoralready_running.MAKA_RUNTIME_SAFE_BOUNDARY_RESUME=1) or the Host is recovering, WorkHub clarifies without recording a resume fact.Design boundary
The renderer owns trusted text parsing and Session-name resolution. The Action Gate owns admission and durable action identity. The Runtime Host owns execution lineage, recovery state, and
turn.resume.query/turn.resume.start.Resume does not copy stop's two-phase durable request/result protocol because it does not retire a durable link. Replay safety comes from the action claim, deterministic continuation Turn identity, and Host admission idempotency. Stop and resume share the same named-Session resolver, submission path, and non-retired-delegation selection; only stop carries destructive confirmation.
The implementation is rebased on the latest #4699 history. Relative to the current base, the PR delta is +931/-154 production lines and +1425/-3 test lines. The ablation removed the persisted resume plan/request, request replay path, cross-layer park-reason type and transport, unused renderer state, duplicate controller branches, and tests that existed only for those mechanisms.
Compatibility
RUNTIME_HOST_COMPATIBILITY_EPOCHis 118. The closed resume wire result has two outcomes and no park reason, so peers from another epoch are rejected during handshake.SQLite schema migration 39 rebuilds
workhub_action_claimsto widen its CHECK constraint with theresumeoperation, preserves existing rows, and removes the v38 table. SQLite cannot widen that CHECK constraint in place.Verification
Observed locally after the review fixes and ablation:
Resume Payments→Stop Payments; the continuation becomes cancelled and the stop targets its Turn.Resume Paymentsreturnsoperation_unavailableand writes no resume fact.git diff --checkpass.Not performed as manual evidence: launching the full Desktop and killing its Host process interactively. The equivalent authority paths above are covered by automated Host/production-composition tests. Packaging remains CI-owned.
AI use
Tool(s) and scope: Claude Code wrote the original implementation and tests under human direction. Codex analyzed review feedback, implemented the Host-lineage and recovery fixes, rebased on #4699/current main, performed the ablation, and executed the reported validation.
Checklist
Does this PR entail a change in behavior?