Skip to content

feat(workhub): add typed action gate - #3818

Merged
Astro-Han merged 9 commits into
apache:mainfrom
ARE404:feat/workhub-action-gate
Aug 26, 2026
Merged

feat(workhub): add typed action gate#3818
Astro-Han merged 9 commits into
apache:mainfrom
ARE404:feat/workhub-action-gate

Conversation

@ARE404

@ARE404ARE404 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements WorkHub Slice 4 from #3492 on top of the merged Slice 3 coordination-session work:

  • adds closed typed contracts for answer_here, delegate_existing, create_new, and clarify;
  • exposes a bounded Runtime-owned candidate set with opaque references;
  • introduces a deterministic Runtime Host Action Gate with fresh target validation, action replay, and non-destructive execution admission;
  • routes the production Desktop WorkHub path through that gate while keeping the R2.4 controller as a test-only regression harness.

This changes behavior: destructive cross-Session correction is deferred to Slice 5. Production recognizes correction before explicit creation, fails closed before a second delegation, and tells the user to stop the original work from its Session. Explicit creation without correction context remains available.

Refs #3492

Verification

  • Regression table was red on 89e4e20: normal variants containing please, an em dash, , or 不对 still reached create_new and failed with WorkHub Action Gate returned an unexpected disposition.
  • Production correction/create regressions: 2 passed. With an existing focus, all 6 English/Chinese correction-shaped creation cases fail closed before a second Action Gate action; without focus, the same 6 cases still execute create_new.
  • Desktop WorkHub affected suites: 108 passed.
  • Desktop main suites outside the filesystem sandbox: 1536 passed, 0 failed.
  • Desktop E2E: WorkHub defers destructive correction until linked delegation exists passed against the fixed production build (1 passed). The exact-head CI also runs the full Desktop E2E surface.
  • Runtime Host Slice 4 suites: 22 passed, including 7 Action Gate tests.
  • Desktop production renderer build passed.
  • Biome passed for all changed files; git diff --check passed.
  • Exact-head 2650a94ff5396454f0b239acced341a10273ce42 CI: test run 32948070394 / job 98113143994 passed, including the full Desktop E2E surface; windows_recovery run 32948070439 / job 98113065733 passed.

UI evidence

Before (ba1eec3): destructive correction exposed a “更正目标” control and replaced the running target.

workhub-slice4-before

After (89e4e20): the correction control is absent and production shows the Slice-5 deferral before a second delegation.

workhub-slice4-after

Both screenshots were captured from the real Electron E2E fixture using the same two-Session correction scenario. The before fixture passed 1/1 at ba1eec3; the after fixture passed 1/1 on the fixed branch.

Safety and authority boundaries

  • Strategy output cannot name arbitrary Session IDs, workspaces, tools, or permission modes.
  • Candidate discovery is scoped to the current Runtime Host and excludes archived, Coordination, side-conversation, and child Sessions.
  • The Gate refreshes candidates before admission and rejects stale or invented references, waiting targets, self-routing, invalid creation context, and conflicting action replays.
  • New ordinary Session identity is derived inside Runtime Host from the action identity; Desktop supplies only trusted workspace context.
  • Tool calls and permission elevation remain owned by the target ordinary Session. The closed WorkHub protocol cannot widen either ceiling.

Slice boundary: destructive correction is deferred to Slice 5

This PR intentionally does not expose replace or Stop through the Slice 4 Action Gate.

A destructive correction must prove durable linkage between the original delegation, the exact root Turn WorkHub owns, the correcting action, and the replacement submission. That linkage and recovery across the non-atomic Stop-to-submit seam must survive Runtime Host restart. Host-lifetime maps, TTLs, and retry lanes cannot provide that authority after restart.

Slice 4 therefore owns the typed gate, bounded candidates, fresh validation, replay, and non-destructive answer_here, clarify, delegate_existing, and create_new. Slice 5 will persist delegation/action linkage first, then add natural-language replacement and exact Stop ownership.

The decoder rejects replace; the production Gate has no Stop effect; Desktop exposes no correction picker. The legacy R2.4 correction path is test-only.

AI use

Select exactly one:

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

Tool(s) and scope: Codex implemented the Runtime Host/Desktop changes, regression tests, review fixes, and verification. Material commits include Generated-by: Codex trailers.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

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

Update on b65c5c2313:

CODE NO-GO — 4×P2

  • P2-1 correction without explicit intent fails confirmation_required before Stop.
  • P2-2 dropping replace when source outside bounded set silently forks while reporting corrected.
  • P2-3 concurrent replacements lack source lock → fan-out to different targets after Stop.
  • P2-4 record 48 KiB vs retry probe 32 KiB → retry after success hits commit_outcome_unknown.

Fix: make correction carry explicit stop text or relax gate; keep replace mandatory or fail; add source lease across actions; align byte limits. Hosted test: SUCCESS does not waive these.

简体中文四处权限/一致性阻塞。

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

Update on 48666a0ceb:

CODE NO-GO — 3×P2

  • P2 candidate path does not write admitted.targetTurnId to local map → natural-language correction cannot send replacement (fail-closed at 814). E2E workhub-reconstruction fails (32879372758).
  • P2 Stop-then-submit with swallowed replay leaves source stopped and target unconfirmed → retry gets stop_not_owned.
  • P2 48 KiB user + 8 KiB assistant JSON record can exceed 72 KiB retry lookup after escaping → retry becomes commit_outcome_unknown.
简体中文存在路径阻塞与重放问题。

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

Update on f2785c5dc0:

CODE NO-GO — 2×P2 plus required check red

  • P2 Stop-then-submit with swallowed replay leaves source stopped and target unconfirmed → retry stop_not_owned.
  • P2 JSON record escaped bytes exceed 72 KiB retry lookup → retry commit_outcome_unknown.

Note: natural-language correction P2 from prior head is now closed (gated receipt saved). Hosted test: FAILURE on format check (new test ternary).

简体中文仍有两处阻塞,另需格式化修复。

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

Update on 05d3d26e20:

CODE NO-GO — 2×P2 (carry-over, formatting fix only)

  • P2 Stop-then-submit without replay on target failure → retry stop_not_owned.
  • P2 JSON escaped record may exceed 72 KiB retry lookup → commit_outcome_unknown.

Formatting failure from prior head fixed; logic unchanged. Hosted test: QUEUED — not green.

简体中文仍有两处阻塞。

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

Update on 92d0947890:

[P2] #replacementRecoveries can exhaust to Host-wide outage

Capacity 256 is only released on Stop failure or target success. After Stop succeeds, permanent target rejections (e.g. session_busy) keep the recovery forever with no TTL/reaper — 256 failures exhaust replacements Host-wide as host_not_ready until restart.

Fix: give recoveries reconciled lifecycle / TTL for permanent failures.

Checks on 92d0947890d2aeec9a6363f17b68ff0850deb5b0 are test: SUCCESS — code is NO-GO.

简体中文异常恢复容量会耗尽。

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

I reviewed this head and found no blocking issues.

Fixes replacement recovery lifecycle (typed failure releases checkpoint, unknown retains fingerprint with 5-min TTL) — closes prior 256-capacity outage; definitive/unknown regression tests pass. Hosted test: SUCCESS (32924223915).

No new P0-P3.

简体中文该头无新增阻断。

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.

@ARE404
ARE404force-pushed the feat/workhub-action-gate branch from 3beac53 to 9532d2dCompareAugust 26, 2026 03:53

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

I reviewed this head and found no blocking issues.

Fixes Action Gate classification to check correction before explicit new-session creation; replace/Stop removed from production. Hosted test: SUCCESS (32935316010) and windows_recovery: SUCCESS.

No P0-P3.

简体中文该头无阻断。

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.

@jackwener
jackwenerforce-pushed the feat/workhub-action-gate branch from c9d74e5 to 2650a94CompareAugust 26, 2026 08:30

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

I reviewed this head and found no blocking issues.

Completes typed Action Gate: opaque candidateRef with fresh validation, idempotent replay with fingerprint conflict, self-route/target-waiting fail-closed; hosted test+windows_recovery SUCCESS.

No P0-P3.

简体中文该头无阻断。

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.

@M4n5ter
M4n5terforce-pushed the feat/workhub-action-gate branch from 2650a94 to 781a12fCompareAugust 26, 2026 09:45
Preserve Runtime-admitted root receipts for natural-language corrections and avoid deleting newer ownership after a concurrent Stop.\n\nGenerated-by: Codex
Resume the exact target submission after a replacement Stop and budget summary replay reads for worst-case JSON escaping.\n\nGenerated-by: Codex
Release recovery checkpoints after definitive target failures and expire uncertain outcomes after a bounded reconciliation window.\n\nGenerated-by: Codex
Recognize correction cues independently from punctuation, politeness, and the creation clause so focused corrections fail closed without weakening no-focus creation.
Generated-by: Codex
@M4n5ter
M4n5terforce-pushed the feat/workhub-action-gate branch from 781a12f to 3844445CompareAugust 26, 2026 09:53

@Astro-HanAstro-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 the Slice 4 gate as a whole rather than line by line, since the interesting question is whether the admission boundary actually holds.

It does. Two things I checked specifically because they looked risky and turned out to be right:

Epoch 51 is correctly derived above main's 50, with its own note — no collision.

The create_new create→submit pair is non-atomic but safe. A submit failure clears the replay entry, so a retry re-enters create. That is fine because workHubCreatedSessionId(actionId) is deterministic and #create probes probeStableSessionCreate by request fingerprint, returning success for existing. Same action id and same input yield the same fingerprint, so the retry reuses the session instead of conflicting or orphaning it. Worth a comment at the effect site, because the safety depends on a property two packages away.

The candidate-ref indirection is the right shape: a strategy proposal cannot name a Session id, and refreshing the set before admission means a model-selected reference cannot outlive the state it was chosen from.

Approving. Three P3s and one question inline; none of them blocks, and I am merging on that basis.

AI use: Claude Code assisted with source investigation; the analysis and conclusions are my own.

简体中文

我是把 Slice 4 的 gate 当成一个整体来看的,因为真正要回答的问题是这个准入边界站不站得住。

站得住。有两处看着危险、核完确认是对的:

epoch 51 在 main 的 50 之上正确推导,并带了自己的说明,没有撞号。

create_new 的 create→submit 非原子,但是安全的。 submit 失败会清掉重放记录,重试因此会重新进入 create。这没问题:workHubCreatedSessionId(actionId) 是确定性的,而 #create 通过 probeStableSessionCreate 按请求 fingerprint 探测,对 existing 直接返回成功。相同的 action id 和相同输入产生相同 fingerprint,所以重试会复用同一个 Session,既不冲突也不会留下孤儿。建议在 effect 处加一句注释,因为这个安全性依赖的是两个包之外的性质。

candidateRef 这层间接是对的形状:策略提案无法指名 Session id,而准入前刷新候选集意味着模型选出的引用不会比它所依据的状态活得更久。

Approve。行内三条 P3 和一个问题,都不阻塞,我据此合并。

result: WorkHubSubmission,
): result is Extract<WorkHubSubmission, { kind: 'submitted' }> {
return result.kind === 'submitted' && !result.steered;
export function workHubSurfaceFailure(error: unknown): WorkHubSurfaceFailure {

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.

[P3] This reconstructs a typed distinction by matching English prose, in the PR whose thesis is that the distinction is typed.

The code already exists: WorkHubActionGateFailureCode is candidate_set_stale | candidate_unavailable | target_waiting_for_user | self_route | action_conflict. The coordinator's #act then discards it, collapsing everything to session_busy or operation_conflict plus a message, and this function greps the message to get it back.

Every branch matches correctly today, which is why this is P3 and not higher. What makes it worth fixing anyway is that nothing protects it: the producer and the consumer are in different packages, so rewording a Host error silently downgrades a user from specific guidance to delivery_failed, with no compile error and no test that spans the boundary. 'source or target is not in' already matches no message I can find in this diff, which is roughly what that drift looks like.

Carrying the gate's code through the operation failure instead of flattening it would remove the second representation entirely.

const candidates = await this.candidates();
if (candidates.candidateSetId !== input.candidateSetId) {
throw new WorkHubActionGateFailure(
'candidate_set_stale',

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.

[P3] Freshness is enforced set-wide, but the invariant that matters is per-target. candidateSetId digests every candidate's updatedAt and status, so a message in any unrelated Session rotates it and this rejects a delegation whose own target never changed.

The window is small — the routing policy is synchronous, so it is about one routingEvidence() round trip — which is why P3. But it is the multi-Session case that WorkHub exists for, and that is exactly when other Sessions are producing messages.

I do not think there is a one-line fix: candidateRef is derived from candidateSetId, so per-candidate validation would need refs bound to per-Session state instead of to the set. Worth deciding deliberately rather than inheriting.


#assertTarget(target: WorkHubCoordinationCandidate): void {
if (target.sessionId === WORKHUB_COORDINATION_SESSION_ID) {
throw new WorkHubActionGateFailure('self_route', 'WorkHub cannot delegate to itself');

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.

[P3] Unreachable. isCandidateSession already filters out the Coordination Session via isWorkHubCoordinationSessionTarget, and this only runs against a candidate found in that set, so self_route cannot fire. Either drop the branch or, if it is meant as a belt-and-braces assertion against a future candidate source, say so — as written it reads like a live guard.

}

/** @internal Transitional R2.4 regression harness; application code must use the Action Gate. */
export function createLegacyWorkHubControllerForTests(deps: {

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.

Not a finding — a question about the exit. The shared implementation now carries both behaviours behind deps.coordination, and production only ever takes one of them, so the other set of branches lives in production code kept reachable only by tests. That is the parallel path AGENTS.md asks us not to leave behind.

You have documented it as transitional, which is the right call for a slice boundary, so I am not treating it as a defect. What I would like recorded somewhere durable: which change deletes createLegacyWorkHubControllerForTests and the deps.coordination forks — Slice 5? If the answer lives only in this PR description it tends to become permanent.

@Astro-Han
Astro-Han merged commit 0e2074a into apache:mainAug 26, 2026
2 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

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

feat(workhub): add typed action gate - #3818

Merged
Astro-Han merged 9 commits into
apache:mainfrom
ARE404:feat/workhub-action-gate
Aug 26, 2026
Merged

feat(workhub): add typed action gate#3818
Astro-Han merged 9 commits into
apache:mainfrom
ARE404:feat/workhub-action-gate

Conversation

@ARE404

@ARE404ARE404 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements WorkHub Slice 4 from #3492 on top of the merged Slice 3 coordination-session work:

  • adds closed typed contracts for answer_here, delegate_existing, create_new, and clarify;
  • exposes a bounded Runtime-owned candidate set with opaque references;
  • introduces a deterministic Runtime Host Action Gate with fresh target validation, action replay, and non-destructive execution admission;
  • routes the production Desktop WorkHub path through that gate while keeping the R2.4 controller as a test-only regression harness.

This changes behavior: destructive cross-Session correction is deferred to Slice 5. Production recognizes correction before explicit creation, fails closed before a second delegation, and tells the user to stop the original work from its Session. Explicit creation without correction context remains available.

Refs #3492

Verification

  • Regression table was red on 89e4e20: normal variants containing please, an em dash, , or 不对 still reached create_new and failed with WorkHub Action Gate returned an unexpected disposition.
  • Production correction/create regressions: 2 passed. With an existing focus, all 6 English/Chinese correction-shaped creation cases fail closed before a second Action Gate action; without focus, the same 6 cases still execute create_new.
  • Desktop WorkHub affected suites: 108 passed.
  • Desktop main suites outside the filesystem sandbox: 1536 passed, 0 failed.
  • Desktop E2E: WorkHub defers destructive correction until linked delegation exists passed against the fixed production build (1 passed). The exact-head CI also runs the full Desktop E2E surface.
  • Runtime Host Slice 4 suites: 22 passed, including 7 Action Gate tests.
  • Desktop production renderer build passed.
  • Biome passed for all changed files; git diff --check passed.
  • Exact-head 2650a94ff5396454f0b239acced341a10273ce42 CI: test run 32948070394 / job 98113143994 passed, including the full Desktop E2E surface; windows_recovery run 32948070439 / job 98113065733 passed.

UI evidence

Before (ba1eec3): destructive correction exposed a “更正目标” control and replaced the running target.

workhub-slice4-before

After (89e4e20): the correction control is absent and production shows the Slice-5 deferral before a second delegation.

workhub-slice4-after

Both screenshots were captured from the real Electron E2E fixture using the same two-Session correction scenario. The before fixture passed 1/1 at ba1eec3; the after fixture passed 1/1 on the fixed branch.

Safety and authority boundaries

  • Strategy output cannot name arbitrary Session IDs, workspaces, tools, or permission modes.
  • Candidate discovery is scoped to the current Runtime Host and excludes archived, Coordination, side-conversation, and child Sessions.
  • The Gate refreshes candidates before admission and rejects stale or invented references, waiting targets, self-routing, invalid creation context, and conflicting action replays.
  • New ordinary Session identity is derived inside Runtime Host from the action identity; Desktop supplies only trusted workspace context.
  • Tool calls and permission elevation remain owned by the target ordinary Session. The closed WorkHub protocol cannot widen either ceiling.

Slice boundary: destructive correction is deferred to Slice 5

This PR intentionally does not expose replace or Stop through the Slice 4 Action Gate.

A destructive correction must prove durable linkage between the original delegation, the exact root Turn WorkHub owns, the correcting action, and the replacement submission. That linkage and recovery across the non-atomic Stop-to-submit seam must survive Runtime Host restart. Host-lifetime maps, TTLs, and retry lanes cannot provide that authority after restart.

Slice 4 therefore owns the typed gate, bounded candidates, fresh validation, replay, and non-destructive answer_here, clarify, delegate_existing, and create_new. Slice 5 will persist delegation/action linkage first, then add natural-language replacement and exact Stop ownership.

The decoder rejects replace; the production Gate has no Stop effect; Desktop exposes no correction picker. The legacy R2.4 correction path is test-only.

AI use

Select exactly one:

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

Tool(s) and scope: Codex implemented the Runtime Host/Desktop changes, regression tests, review fixes, and verification. Material commits include Generated-by: Codex trailers.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

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

Update on b65c5c2313:

CODE NO-GO — 4×P2

  • P2-1 correction without explicit intent fails confirmation_required before Stop.
  • P2-2 dropping replace when source outside bounded set silently forks while reporting corrected.
  • P2-3 concurrent replacements lack source lock → fan-out to different targets after Stop.
  • P2-4 record 48 KiB vs retry probe 32 KiB → retry after success hits commit_outcome_unknown.

Fix: make correction carry explicit stop text or relax gate; keep replace mandatory or fail; add source lease across actions; align byte limits. Hosted test: SUCCESS does not waive these.

简体中文四处权限/一致性阻塞。

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

Update on 48666a0ceb:

CODE NO-GO — 3×P2

  • P2 candidate path does not write admitted.targetTurnId to local map → natural-language correction cannot send replacement (fail-closed at 814). E2E workhub-reconstruction fails (32879372758).
  • P2 Stop-then-submit with swallowed replay leaves source stopped and target unconfirmed → retry gets stop_not_owned.
  • P2 48 KiB user + 8 KiB assistant JSON record can exceed 72 KiB retry lookup after escaping → retry becomes commit_outcome_unknown.
简体中文存在路径阻塞与重放问题。

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

Update on f2785c5dc0:

CODE NO-GO — 2×P2 plus required check red

  • P2 Stop-then-submit with swallowed replay leaves source stopped and target unconfirmed → retry stop_not_owned.
  • P2 JSON record escaped bytes exceed 72 KiB retry lookup → retry commit_outcome_unknown.

Note: natural-language correction P2 from prior head is now closed (gated receipt saved). Hosted test: FAILURE on format check (new test ternary).

简体中文仍有两处阻塞,另需格式化修复。

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

Update on 05d3d26e20:

CODE NO-GO — 2×P2 (carry-over, formatting fix only)

  • P2 Stop-then-submit without replay on target failure → retry stop_not_owned.
  • P2 JSON escaped record may exceed 72 KiB retry lookup → commit_outcome_unknown.

Formatting failure from prior head fixed; logic unchanged. Hosted test: QUEUED — not green.

简体中文仍有两处阻塞。

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

Update on 92d0947890:

[P2] #replacementRecoveries can exhaust to Host-wide outage

Capacity 256 is only released on Stop failure or target success. After Stop succeeds, permanent target rejections (e.g. session_busy) keep the recovery forever with no TTL/reaper — 256 failures exhaust replacements Host-wide as host_not_ready until restart.

Fix: give recoveries reconciled lifecycle / TTL for permanent failures.

Checks on 92d0947890d2aeec9a6363f17b68ff0850deb5b0 are test: SUCCESS — code is NO-GO.

简体中文异常恢复容量会耗尽。

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

I reviewed this head and found no blocking issues.

Fixes replacement recovery lifecycle (typed failure releases checkpoint, unknown retains fingerprint with 5-min TTL) — closes prior 256-capacity outage; definitive/unknown regression tests pass. Hosted test: SUCCESS (32924223915).

No new P0-P3.

简体中文该头无新增阻断。

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.

@ARE404
ARE404force-pushed the feat/workhub-action-gate branch from 3beac53 to 9532d2dCompareAugust 26, 2026 03:53

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

I reviewed this head and found no blocking issues.

Fixes Action Gate classification to check correction before explicit new-session creation; replace/Stop removed from production. Hosted test: SUCCESS (32935316010) and windows_recovery: SUCCESS.

No P0-P3.

简体中文该头无阻断。

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.

@jackwener
jackwenerforce-pushed the feat/workhub-action-gate branch from c9d74e5 to 2650a94CompareAugust 26, 2026 08:30

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

I reviewed this head and found no blocking issues.

Completes typed Action Gate: opaque candidateRef with fresh validation, idempotent replay with fingerprint conflict, self-route/target-waiting fail-closed; hosted test+windows_recovery SUCCESS.

No P0-P3.

简体中文该头无阻断。

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.

@M4n5ter
M4n5terforce-pushed the feat/workhub-action-gate branch from 2650a94 to 781a12fCompareAugust 26, 2026 09:45
Preserve Runtime-admitted root receipts for natural-language corrections and avoid deleting newer ownership after a concurrent Stop.\n\nGenerated-by: Codex
Resume the exact target submission after a replacement Stop and budget summary replay reads for worst-case JSON escaping.\n\nGenerated-by: Codex
Release recovery checkpoints after definitive target failures and expire uncertain outcomes after a bounded reconciliation window.\n\nGenerated-by: Codex
Recognize correction cues independently from punctuation, politeness, and the creation clause so focused corrections fail closed without weakening no-focus creation.
Generated-by: Codex
@M4n5ter
M4n5terforce-pushed the feat/workhub-action-gate branch from 781a12f to 3844445CompareAugust 26, 2026 09:53

@Astro-HanAstro-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 the Slice 4 gate as a whole rather than line by line, since the interesting question is whether the admission boundary actually holds.

It does. Two things I checked specifically because they looked risky and turned out to be right:

Epoch 51 is correctly derived above main's 50, with its own note — no collision.

The create_new create→submit pair is non-atomic but safe. A submit failure clears the replay entry, so a retry re-enters create. That is fine because workHubCreatedSessionId(actionId) is deterministic and #create probes probeStableSessionCreate by request fingerprint, returning success for existing. Same action id and same input yield the same fingerprint, so the retry reuses the session instead of conflicting or orphaning it. Worth a comment at the effect site, because the safety depends on a property two packages away.

The candidate-ref indirection is the right shape: a strategy proposal cannot name a Session id, and refreshing the set before admission means a model-selected reference cannot outlive the state it was chosen from.

Approving. Three P3s and one question inline; none of them blocks, and I am merging on that basis.

AI use: Claude Code assisted with source investigation; the analysis and conclusions are my own.

简体中文

我是把 Slice 4 的 gate 当成一个整体来看的,因为真正要回答的问题是这个准入边界站不站得住。

站得住。有两处看着危险、核完确认是对的:

epoch 51 在 main 的 50 之上正确推导,并带了自己的说明,没有撞号。

create_new 的 create→submit 非原子,但是安全的。 submit 失败会清掉重放记录,重试因此会重新进入 create。这没问题:workHubCreatedSessionId(actionId) 是确定性的,而 #create 通过 probeStableSessionCreate 按请求 fingerprint 探测,对 existing 直接返回成功。相同的 action id 和相同输入产生相同 fingerprint,所以重试会复用同一个 Session,既不冲突也不会留下孤儿。建议在 effect 处加一句注释,因为这个安全性依赖的是两个包之外的性质。

candidateRef 这层间接是对的形状:策略提案无法指名 Session id,而准入前刷新候选集意味着模型选出的引用不会比它所依据的状态活得更久。

Approve。行内三条 P3 和一个问题,都不阻塞,我据此合并。

result: WorkHubSubmission,
): result is Extract<WorkHubSubmission, { kind: 'submitted' }> {
return result.kind === 'submitted' && !result.steered;
export function workHubSurfaceFailure(error: unknown): WorkHubSurfaceFailure {

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.

[P3] This reconstructs a typed distinction by matching English prose, in the PR whose thesis is that the distinction is typed.

The code already exists: WorkHubActionGateFailureCode is candidate_set_stale | candidate_unavailable | target_waiting_for_user | self_route | action_conflict. The coordinator's #act then discards it, collapsing everything to session_busy or operation_conflict plus a message, and this function greps the message to get it back.

Every branch matches correctly today, which is why this is P3 and not higher. What makes it worth fixing anyway is that nothing protects it: the producer and the consumer are in different packages, so rewording a Host error silently downgrades a user from specific guidance to delivery_failed, with no compile error and no test that spans the boundary. 'source or target is not in' already matches no message I can find in this diff, which is roughly what that drift looks like.

Carrying the gate's code through the operation failure instead of flattening it would remove the second representation entirely.

const candidates = await this.candidates();
if (candidates.candidateSetId !== input.candidateSetId) {
throw new WorkHubActionGateFailure(
'candidate_set_stale',

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.

[P3] Freshness is enforced set-wide, but the invariant that matters is per-target. candidateSetId digests every candidate's updatedAt and status, so a message in any unrelated Session rotates it and this rejects a delegation whose own target never changed.

The window is small — the routing policy is synchronous, so it is about one routingEvidence() round trip — which is why P3. But it is the multi-Session case that WorkHub exists for, and that is exactly when other Sessions are producing messages.

I do not think there is a one-line fix: candidateRef is derived from candidateSetId, so per-candidate validation would need refs bound to per-Session state instead of to the set. Worth deciding deliberately rather than inheriting.


#assertTarget(target: WorkHubCoordinationCandidate): void {
if (target.sessionId === WORKHUB_COORDINATION_SESSION_ID) {
throw new WorkHubActionGateFailure('self_route', 'WorkHub cannot delegate to itself');

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.

[P3] Unreachable. isCandidateSession already filters out the Coordination Session via isWorkHubCoordinationSessionTarget, and this only runs against a candidate found in that set, so self_route cannot fire. Either drop the branch or, if it is meant as a belt-and-braces assertion against a future candidate source, say so — as written it reads like a live guard.

}

/** @internal Transitional R2.4 regression harness; application code must use the Action Gate. */
export function createLegacyWorkHubControllerForTests(deps: {

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.

Not a finding — a question about the exit. The shared implementation now carries both behaviours behind deps.coordination, and production only ever takes one of them, so the other set of branches lives in production code kept reachable only by tests. That is the parallel path AGENTS.md asks us not to leave behind.

You have documented it as transitional, which is the right call for a slice boundary, so I am not treating it as a defect. What I would like recorded somewhere durable: which change deletes createLegacyWorkHubControllerForTests and the deps.coordination forks — Slice 5? If the answer lives only in this PR description it tends to become permanent.

@Astro-Han
Astro-Han merged commit 0e2074a into apache:mainAug 26, 2026
2 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

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

feat(workhub): add typed action gate - #3818

Merged
Astro-Han merged 9 commits into
apache:mainfrom
ARE404:feat/workhub-action-gate
Aug 26, 2026
Merged

feat(workhub): add typed action gate#3818
Astro-Han merged 9 commits into
apache:mainfrom
ARE404:feat/workhub-action-gate

Conversation

@ARE404

@ARE404ARE404 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements WorkHub Slice 4 from #3492 on top of the merged Slice 3 coordination-session work:

  • adds closed typed contracts for answer_here, delegate_existing, create_new, and clarify;
  • exposes a bounded Runtime-owned candidate set with opaque references;
  • introduces a deterministic Runtime Host Action Gate with fresh target validation, action replay, and non-destructive execution admission;
  • routes the production Desktop WorkHub path through that gate while keeping the R2.4 controller as a test-only regression harness.

This changes behavior: destructive cross-Session correction is deferred to Slice 5. Production recognizes correction before explicit creation, fails closed before a second delegation, and tells the user to stop the original work from its Session. Explicit creation without correction context remains available.

Refs #3492

Verification

  • Regression table was red on 89e4e20: normal variants containing please, an em dash, , or 不对 still reached create_new and failed with WorkHub Action Gate returned an unexpected disposition.
  • Production correction/create regressions: 2 passed. With an existing focus, all 6 English/Chinese correction-shaped creation cases fail closed before a second Action Gate action; without focus, the same 6 cases still execute create_new.
  • Desktop WorkHub affected suites: 108 passed.
  • Desktop main suites outside the filesystem sandbox: 1536 passed, 0 failed.
  • Desktop E2E: WorkHub defers destructive correction until linked delegation exists passed against the fixed production build (1 passed). The exact-head CI also runs the full Desktop E2E surface.
  • Runtime Host Slice 4 suites: 22 passed, including 7 Action Gate tests.
  • Desktop production renderer build passed.
  • Biome passed for all changed files; git diff --check passed.
  • Exact-head 2650a94ff5396454f0b239acced341a10273ce42 CI: test run 32948070394 / job 98113143994 passed, including the full Desktop E2E surface; windows_recovery run 32948070439 / job 98113065733 passed.

UI evidence

Before (ba1eec3): destructive correction exposed a “更正目标” control and replaced the running target.

workhub-slice4-before

After (89e4e20): the correction control is absent and production shows the Slice-5 deferral before a second delegation.

workhub-slice4-after

Both screenshots were captured from the real Electron E2E fixture using the same two-Session correction scenario. The before fixture passed 1/1 at ba1eec3; the after fixture passed 1/1 on the fixed branch.

Safety and authority boundaries

  • Strategy output cannot name arbitrary Session IDs, workspaces, tools, or permission modes.
  • Candidate discovery is scoped to the current Runtime Host and excludes archived, Coordination, side-conversation, and child Sessions.
  • The Gate refreshes candidates before admission and rejects stale or invented references, waiting targets, self-routing, invalid creation context, and conflicting action replays.
  • New ordinary Session identity is derived inside Runtime Host from the action identity; Desktop supplies only trusted workspace context.
  • Tool calls and permission elevation remain owned by the target ordinary Session. The closed WorkHub protocol cannot widen either ceiling.

Slice boundary: destructive correction is deferred to Slice 5

This PR intentionally does not expose replace or Stop through the Slice 4 Action Gate.

A destructive correction must prove durable linkage between the original delegation, the exact root Turn WorkHub owns, the correcting action, and the replacement submission. That linkage and recovery across the non-atomic Stop-to-submit seam must survive Runtime Host restart. Host-lifetime maps, TTLs, and retry lanes cannot provide that authority after restart.

Slice 4 therefore owns the typed gate, bounded candidates, fresh validation, replay, and non-destructive answer_here, clarify, delegate_existing, and create_new. Slice 5 will persist delegation/action linkage first, then add natural-language replacement and exact Stop ownership.

The decoder rejects replace; the production Gate has no Stop effect; Desktop exposes no correction picker. The legacy R2.4 correction path is test-only.

AI use

Select exactly one:

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

Tool(s) and scope: Codex implemented the Runtime Host/Desktop changes, regression tests, review fixes, and verification. Material commits include Generated-by: Codex trailers.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

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

Update on b65c5c2313:

CODE NO-GO — 4×P2

  • P2-1 correction without explicit intent fails confirmation_required before Stop.
  • P2-2 dropping replace when source outside bounded set silently forks while reporting corrected.
  • P2-3 concurrent replacements lack source lock → fan-out to different targets after Stop.
  • P2-4 record 48 KiB vs retry probe 32 KiB → retry after success hits commit_outcome_unknown.

Fix: make correction carry explicit stop text or relax gate; keep replace mandatory or fail; add source lease across actions; align byte limits. Hosted test: SUCCESS does not waive these.

简体中文四处权限/一致性阻塞。

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

Update on 48666a0ceb:

CODE NO-GO — 3×P2

  • P2 candidate path does not write admitted.targetTurnId to local map → natural-language correction cannot send replacement (fail-closed at 814). E2E workhub-reconstruction fails (32879372758).
  • P2 Stop-then-submit with swallowed replay leaves source stopped and target unconfirmed → retry gets stop_not_owned.
  • P2 48 KiB user + 8 KiB assistant JSON record can exceed 72 KiB retry lookup after escaping → retry becomes commit_outcome_unknown.
简体中文存在路径阻塞与重放问题。

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

Update on f2785c5dc0:

CODE NO-GO — 2×P2 plus required check red

  • P2 Stop-then-submit with swallowed replay leaves source stopped and target unconfirmed → retry stop_not_owned.
  • P2 JSON record escaped bytes exceed 72 KiB retry lookup → retry commit_outcome_unknown.

Note: natural-language correction P2 from prior head is now closed (gated receipt saved). Hosted test: FAILURE on format check (new test ternary).

简体中文仍有两处阻塞,另需格式化修复。

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

Update on 05d3d26e20:

CODE NO-GO — 2×P2 (carry-over, formatting fix only)

  • P2 Stop-then-submit without replay on target failure → retry stop_not_owned.
  • P2 JSON escaped record may exceed 72 KiB retry lookup → commit_outcome_unknown.

Formatting failure from prior head fixed; logic unchanged. Hosted test: QUEUED — not green.

简体中文仍有两处阻塞。

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

Update on 92d0947890:

[P2] #replacementRecoveries can exhaust to Host-wide outage

Capacity 256 is only released on Stop failure or target success. After Stop succeeds, permanent target rejections (e.g. session_busy) keep the recovery forever with no TTL/reaper — 256 failures exhaust replacements Host-wide as host_not_ready until restart.

Fix: give recoveries reconciled lifecycle / TTL for permanent failures.

Checks on 92d0947890d2aeec9a6363f17b68ff0850deb5b0 are test: SUCCESS — code is NO-GO.

简体中文异常恢复容量会耗尽。

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

I reviewed this head and found no blocking issues.

Fixes replacement recovery lifecycle (typed failure releases checkpoint, unknown retains fingerprint with 5-min TTL) — closes prior 256-capacity outage; definitive/unknown regression tests pass. Hosted test: SUCCESS (32924223915).

No new P0-P3.

简体中文该头无新增阻断。

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.

@ARE404
ARE404force-pushed the feat/workhub-action-gate branch from 3beac53 to 9532d2dCompareAugust 26, 2026 03:53

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

I reviewed this head and found no blocking issues.

Fixes Action Gate classification to check correction before explicit new-session creation; replace/Stop removed from production. Hosted test: SUCCESS (32935316010) and windows_recovery: SUCCESS.

No P0-P3.

简体中文该头无阻断。

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.

@jackwener
jackwenerforce-pushed the feat/workhub-action-gate branch from c9d74e5 to 2650a94CompareAugust 26, 2026 08:30

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

I reviewed this head and found no blocking issues.

Completes typed Action Gate: opaque candidateRef with fresh validation, idempotent replay with fingerprint conflict, self-route/target-waiting fail-closed; hosted test+windows_recovery SUCCESS.

No P0-P3.

简体中文该头无阻断。

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.

@M4n5ter
M4n5terforce-pushed the feat/workhub-action-gate branch from 2650a94 to 781a12fCompareAugust 26, 2026 09:45
Preserve Runtime-admitted root receipts for natural-language corrections and avoid deleting newer ownership after a concurrent Stop.\n\nGenerated-by: Codex
Resume the exact target submission after a replacement Stop and budget summary replay reads for worst-case JSON escaping.\n\nGenerated-by: Codex
Release recovery checkpoints after definitive target failures and expire uncertain outcomes after a bounded reconciliation window.\n\nGenerated-by: Codex
Recognize correction cues independently from punctuation, politeness, and the creation clause so focused corrections fail closed without weakening no-focus creation.
Generated-by: Codex
@M4n5ter
M4n5terforce-pushed the feat/workhub-action-gate branch from 781a12f to 3844445CompareAugust 26, 2026 09:53

@Astro-HanAstro-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 the Slice 4 gate as a whole rather than line by line, since the interesting question is whether the admission boundary actually holds.

It does. Two things I checked specifically because they looked risky and turned out to be right:

Epoch 51 is correctly derived above main's 50, with its own note — no collision.

The create_new create→submit pair is non-atomic but safe. A submit failure clears the replay entry, so a retry re-enters create. That is fine because workHubCreatedSessionId(actionId) is deterministic and #create probes probeStableSessionCreate by request fingerprint, returning success for existing. Same action id and same input yield the same fingerprint, so the retry reuses the session instead of conflicting or orphaning it. Worth a comment at the effect site, because the safety depends on a property two packages away.

The candidate-ref indirection is the right shape: a strategy proposal cannot name a Session id, and refreshing the set before admission means a model-selected reference cannot outlive the state it was chosen from.

Approving. Three P3s and one question inline; none of them blocks, and I am merging on that basis.

AI use: Claude Code assisted with source investigation; the analysis and conclusions are my own.

简体中文

我是把 Slice 4 的 gate 当成一个整体来看的,因为真正要回答的问题是这个准入边界站不站得住。

站得住。有两处看着危险、核完确认是对的:

epoch 51 在 main 的 50 之上正确推导,并带了自己的说明,没有撞号。

create_new 的 create→submit 非原子,但是安全的。 submit 失败会清掉重放记录,重试因此会重新进入 create。这没问题:workHubCreatedSessionId(actionId) 是确定性的,而 #create 通过 probeStableSessionCreate 按请求 fingerprint 探测,对 existing 直接返回成功。相同的 action id 和相同输入产生相同 fingerprint,所以重试会复用同一个 Session,既不冲突也不会留下孤儿。建议在 effect 处加一句注释,因为这个安全性依赖的是两个包之外的性质。

candidateRef 这层间接是对的形状:策略提案无法指名 Session id,而准入前刷新候选集意味着模型选出的引用不会比它所依据的状态活得更久。

Approve。行内三条 P3 和一个问题,都不阻塞,我据此合并。

result: WorkHubSubmission,
): result is Extract<WorkHubSubmission, { kind: 'submitted' }> {
return result.kind === 'submitted' && !result.steered;
export function workHubSurfaceFailure(error: unknown): WorkHubSurfaceFailure {

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.

[P3] This reconstructs a typed distinction by matching English prose, in the PR whose thesis is that the distinction is typed.

The code already exists: WorkHubActionGateFailureCode is candidate_set_stale | candidate_unavailable | target_waiting_for_user | self_route | action_conflict. The coordinator's #act then discards it, collapsing everything to session_busy or operation_conflict plus a message, and this function greps the message to get it back.

Every branch matches correctly today, which is why this is P3 and not higher. What makes it worth fixing anyway is that nothing protects it: the producer and the consumer are in different packages, so rewording a Host error silently downgrades a user from specific guidance to delivery_failed, with no compile error and no test that spans the boundary. 'source or target is not in' already matches no message I can find in this diff, which is roughly what that drift looks like.

Carrying the gate's code through the operation failure instead of flattening it would remove the second representation entirely.

const candidates = await this.candidates();
if (candidates.candidateSetId !== input.candidateSetId) {
throw new WorkHubActionGateFailure(
'candidate_set_stale',

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.

[P3] Freshness is enforced set-wide, but the invariant that matters is per-target. candidateSetId digests every candidate's updatedAt and status, so a message in any unrelated Session rotates it and this rejects a delegation whose own target never changed.

The window is small — the routing policy is synchronous, so it is about one routingEvidence() round trip — which is why P3. But it is the multi-Session case that WorkHub exists for, and that is exactly when other Sessions are producing messages.

I do not think there is a one-line fix: candidateRef is derived from candidateSetId, so per-candidate validation would need refs bound to per-Session state instead of to the set. Worth deciding deliberately rather than inheriting.


#assertTarget(target: WorkHubCoordinationCandidate): void {
if (target.sessionId === WORKHUB_COORDINATION_SESSION_ID) {
throw new WorkHubActionGateFailure('self_route', 'WorkHub cannot delegate to itself');

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.

[P3] Unreachable. isCandidateSession already filters out the Coordination Session via isWorkHubCoordinationSessionTarget, and this only runs against a candidate found in that set, so self_route cannot fire. Either drop the branch or, if it is meant as a belt-and-braces assertion against a future candidate source, say so — as written it reads like a live guard.

}

/** @internal Transitional R2.4 regression harness; application code must use the Action Gate. */
export function createLegacyWorkHubControllerForTests(deps: {

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.

Not a finding — a question about the exit. The shared implementation now carries both behaviours behind deps.coordination, and production only ever takes one of them, so the other set of branches lives in production code kept reachable only by tests. That is the parallel path AGENTS.md asks us not to leave behind.

You have documented it as transitional, which is the right call for a slice boundary, so I am not treating it as a defect. What I would like recorded somewhere durable: which change deletes createLegacyWorkHubControllerForTests and the deps.coordination forks — Slice 5? If the answer lives only in this PR description it tends to become permanent.

@Astro-Han
Astro-Han merged commit 0e2074a into apache:mainAug 26, 2026
2 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

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

feat(workhub): add typed action gate - #3818

Merged
Astro-Han merged 9 commits into
apache:mainfrom
ARE404:feat/workhub-action-gate
Aug 26, 2026
Merged

feat(workhub): add typed action gate#3818
Astro-Han merged 9 commits into
apache:mainfrom
ARE404:feat/workhub-action-gate

Conversation

@ARE404

@ARE404ARE404 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements WorkHub Slice 4 from #3492 on top of the merged Slice 3 coordination-session work:

  • adds closed typed contracts for answer_here, delegate_existing, create_new, and clarify;
  • exposes a bounded Runtime-owned candidate set with opaque references;
  • introduces a deterministic Runtime Host Action Gate with fresh target validation, action replay, and non-destructive execution admission;
  • routes the production Desktop WorkHub path through that gate while keeping the R2.4 controller as a test-only regression harness.

This changes behavior: destructive cross-Session correction is deferred to Slice 5. Production recognizes correction before explicit creation, fails closed before a second delegation, and tells the user to stop the original work from its Session. Explicit creation without correction context remains available.

Refs #3492

Verification

  • Regression table was red on 89e4e20: normal variants containing please, an em dash, , or 不对 still reached create_new and failed with WorkHub Action Gate returned an unexpected disposition.
  • Production correction/create regressions: 2 passed. With an existing focus, all 6 English/Chinese correction-shaped creation cases fail closed before a second Action Gate action; without focus, the same 6 cases still execute create_new.
  • Desktop WorkHub affected suites: 108 passed.
  • Desktop main suites outside the filesystem sandbox: 1536 passed, 0 failed.
  • Desktop E2E: WorkHub defers destructive correction until linked delegation exists passed against the fixed production build (1 passed). The exact-head CI also runs the full Desktop E2E surface.
  • Runtime Host Slice 4 suites: 22 passed, including 7 Action Gate tests.
  • Desktop production renderer build passed.
  • Biome passed for all changed files; git diff --check passed.
  • Exact-head 2650a94ff5396454f0b239acced341a10273ce42 CI: test run 32948070394 / job 98113143994 passed, including the full Desktop E2E surface; windows_recovery run 32948070439 / job 98113065733 passed.

UI evidence

Before (ba1eec3): destructive correction exposed a “更正目标” control and replaced the running target.

workhub-slice4-before

After (89e4e20): the correction control is absent and production shows the Slice-5 deferral before a second delegation.

workhub-slice4-after

Both screenshots were captured from the real Electron E2E fixture using the same two-Session correction scenario. The before fixture passed 1/1 at ba1eec3; the after fixture passed 1/1 on the fixed branch.

Safety and authority boundaries

  • Strategy output cannot name arbitrary Session IDs, workspaces, tools, or permission modes.
  • Candidate discovery is scoped to the current Runtime Host and excludes archived, Coordination, side-conversation, and child Sessions.
  • The Gate refreshes candidates before admission and rejects stale or invented references, waiting targets, self-routing, invalid creation context, and conflicting action replays.
  • New ordinary Session identity is derived inside Runtime Host from the action identity; Desktop supplies only trusted workspace context.
  • Tool calls and permission elevation remain owned by the target ordinary Session. The closed WorkHub protocol cannot widen either ceiling.

Slice boundary: destructive correction is deferred to Slice 5

This PR intentionally does not expose replace or Stop through the Slice 4 Action Gate.

A destructive correction must prove durable linkage between the original delegation, the exact root Turn WorkHub owns, the correcting action, and the replacement submission. That linkage and recovery across the non-atomic Stop-to-submit seam must survive Runtime Host restart. Host-lifetime maps, TTLs, and retry lanes cannot provide that authority after restart.

Slice 4 therefore owns the typed gate, bounded candidates, fresh validation, replay, and non-destructive answer_here, clarify, delegate_existing, and create_new. Slice 5 will persist delegation/action linkage first, then add natural-language replacement and exact Stop ownership.

The decoder rejects replace; the production Gate has no Stop effect; Desktop exposes no correction picker. The legacy R2.4 correction path is test-only.

AI use

Select exactly one:

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

Tool(s) and scope: Codex implemented the Runtime Host/Desktop changes, regression tests, review fixes, and verification. Material commits include Generated-by: Codex trailers.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

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

Update on b65c5c2313:

CODE NO-GO — 4×P2

  • P2-1 correction without explicit intent fails confirmation_required before Stop.
  • P2-2 dropping replace when source outside bounded set silently forks while reporting corrected.
  • P2-3 concurrent replacements lack source lock → fan-out to different targets after Stop.
  • P2-4 record 48 KiB vs retry probe 32 KiB → retry after success hits commit_outcome_unknown.

Fix: make correction carry explicit stop text or relax gate; keep replace mandatory or fail; add source lease across actions; align byte limits. Hosted test: SUCCESS does not waive these.

简体中文四处权限/一致性阻塞。

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

Update on 48666a0ceb:

CODE NO-GO — 3×P2

  • P2 candidate path does not write admitted.targetTurnId to local map → natural-language correction cannot send replacement (fail-closed at 814). E2E workhub-reconstruction fails (32879372758).
  • P2 Stop-then-submit with swallowed replay leaves source stopped and target unconfirmed → retry gets stop_not_owned.
  • P2 48 KiB user + 8 KiB assistant JSON record can exceed 72 KiB retry lookup after escaping → retry becomes commit_outcome_unknown.
简体中文存在路径阻塞与重放问题。

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

Update on f2785c5dc0:

CODE NO-GO — 2×P2 plus required check red

  • P2 Stop-then-submit with swallowed replay leaves source stopped and target unconfirmed → retry stop_not_owned.
  • P2 JSON record escaped bytes exceed 72 KiB retry lookup → retry commit_outcome_unknown.

Note: natural-language correction P2 from prior head is now closed (gated receipt saved). Hosted test: FAILURE on format check (new test ternary).

简体中文仍有两处阻塞,另需格式化修复。

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

Update on 05d3d26e20:

CODE NO-GO — 2×P2 (carry-over, formatting fix only)

  • P2 Stop-then-submit without replay on target failure → retry stop_not_owned.
  • P2 JSON escaped record may exceed 72 KiB retry lookup → commit_outcome_unknown.

Formatting failure from prior head fixed; logic unchanged. Hosted test: QUEUED — not green.

简体中文仍有两处阻塞。

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

Update on 92d0947890:

[P2] #replacementRecoveries can exhaust to Host-wide outage

Capacity 256 is only released on Stop failure or target success. After Stop succeeds, permanent target rejections (e.g. session_busy) keep the recovery forever with no TTL/reaper — 256 failures exhaust replacements Host-wide as host_not_ready until restart.

Fix: give recoveries reconciled lifecycle / TTL for permanent failures.

Checks on 92d0947890d2aeec9a6363f17b68ff0850deb5b0 are test: SUCCESS — code is NO-GO.

简体中文异常恢复容量会耗尽。

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

I reviewed this head and found no blocking issues.

Fixes replacement recovery lifecycle (typed failure releases checkpoint, unknown retains fingerprint with 5-min TTL) — closes prior 256-capacity outage; definitive/unknown regression tests pass. Hosted test: SUCCESS (32924223915).

No new P0-P3.

简体中文该头无新增阻断。

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.

@ARE404
ARE404force-pushed the feat/workhub-action-gate branch from 3beac53 to 9532d2dCompareAugust 26, 2026 03:53

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

I reviewed this head and found no blocking issues.

Fixes Action Gate classification to check correction before explicit new-session creation; replace/Stop removed from production. Hosted test: SUCCESS (32935316010) and windows_recovery: SUCCESS.

No P0-P3.

简体中文该头无阻断。

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.

@jackwener
jackwenerforce-pushed the feat/workhub-action-gate branch from c9d74e5 to 2650a94CompareAugust 26, 2026 08:30

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

I reviewed this head and found no blocking issues.

Completes typed Action Gate: opaque candidateRef with fresh validation, idempotent replay with fingerprint conflict, self-route/target-waiting fail-closed; hosted test+windows_recovery SUCCESS.

No P0-P3.

简体中文该头无阻断。

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.

@M4n5ter
M4n5terforce-pushed the feat/workhub-action-gate branch from 2650a94 to 781a12fCompareAugust 26, 2026 09:45
Preserve Runtime-admitted root receipts for natural-language corrections and avoid deleting newer ownership after a concurrent Stop.\n\nGenerated-by: Codex
Resume the exact target submission after a replacement Stop and budget summary replay reads for worst-case JSON escaping.\n\nGenerated-by: Codex
Release recovery checkpoints after definitive target failures and expire uncertain outcomes after a bounded reconciliation window.\n\nGenerated-by: Codex
Recognize correction cues independently from punctuation, politeness, and the creation clause so focused corrections fail closed without weakening no-focus creation.
Generated-by: Codex
@M4n5ter
M4n5terforce-pushed the feat/workhub-action-gate branch from 781a12f to 3844445CompareAugust 26, 2026 09:53

@Astro-HanAstro-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 the Slice 4 gate as a whole rather than line by line, since the interesting question is whether the admission boundary actually holds.

It does. Two things I checked specifically because they looked risky and turned out to be right:

Epoch 51 is correctly derived above main's 50, with its own note — no collision.

The create_new create→submit pair is non-atomic but safe. A submit failure clears the replay entry, so a retry re-enters create. That is fine because workHubCreatedSessionId(actionId) is deterministic and #create probes probeStableSessionCreate by request fingerprint, returning success for existing. Same action id and same input yield the same fingerprint, so the retry reuses the session instead of conflicting or orphaning it. Worth a comment at the effect site, because the safety depends on a property two packages away.

The candidate-ref indirection is the right shape: a strategy proposal cannot name a Session id, and refreshing the set before admission means a model-selected reference cannot outlive the state it was chosen from.

Approving. Three P3s and one question inline; none of them blocks, and I am merging on that basis.

AI use: Claude Code assisted with source investigation; the analysis and conclusions are my own.

简体中文

我是把 Slice 4 的 gate 当成一个整体来看的,因为真正要回答的问题是这个准入边界站不站得住。

站得住。有两处看着危险、核完确认是对的:

epoch 51 在 main 的 50 之上正确推导,并带了自己的说明,没有撞号。

create_new 的 create→submit 非原子,但是安全的。 submit 失败会清掉重放记录,重试因此会重新进入 create。这没问题:workHubCreatedSessionId(actionId) 是确定性的,而 #create 通过 probeStableSessionCreate 按请求 fingerprint 探测,对 existing 直接返回成功。相同的 action id 和相同输入产生相同 fingerprint,所以重试会复用同一个 Session,既不冲突也不会留下孤儿。建议在 effect 处加一句注释,因为这个安全性依赖的是两个包之外的性质。

candidateRef 这层间接是对的形状:策略提案无法指名 Session id,而准入前刷新候选集意味着模型选出的引用不会比它所依据的状态活得更久。

Approve。行内三条 P3 和一个问题,都不阻塞,我据此合并。

result: WorkHubSubmission,
): result is Extract<WorkHubSubmission, { kind: 'submitted' }> {
return result.kind === 'submitted' && !result.steered;
export function workHubSurfaceFailure(error: unknown): WorkHubSurfaceFailure {

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.

[P3] This reconstructs a typed distinction by matching English prose, in the PR whose thesis is that the distinction is typed.

The code already exists: WorkHubActionGateFailureCode is candidate_set_stale | candidate_unavailable | target_waiting_for_user | self_route | action_conflict. The coordinator's #act then discards it, collapsing everything to session_busy or operation_conflict plus a message, and this function greps the message to get it back.

Every branch matches correctly today, which is why this is P3 and not higher. What makes it worth fixing anyway is that nothing protects it: the producer and the consumer are in different packages, so rewording a Host error silently downgrades a user from specific guidance to delivery_failed, with no compile error and no test that spans the boundary. 'source or target is not in' already matches no message I can find in this diff, which is roughly what that drift looks like.

Carrying the gate's code through the operation failure instead of flattening it would remove the second representation entirely.

const candidates = await this.candidates();
if (candidates.candidateSetId !== input.candidateSetId) {
throw new WorkHubActionGateFailure(
'candidate_set_stale',

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.

[P3] Freshness is enforced set-wide, but the invariant that matters is per-target. candidateSetId digests every candidate's updatedAt and status, so a message in any unrelated Session rotates it and this rejects a delegation whose own target never changed.

The window is small — the routing policy is synchronous, so it is about one routingEvidence() round trip — which is why P3. But it is the multi-Session case that WorkHub exists for, and that is exactly when other Sessions are producing messages.

I do not think there is a one-line fix: candidateRef is derived from candidateSetId, so per-candidate validation would need refs bound to per-Session state instead of to the set. Worth deciding deliberately rather than inheriting.


#assertTarget(target: WorkHubCoordinationCandidate): void {
if (target.sessionId === WORKHUB_COORDINATION_SESSION_ID) {
throw new WorkHubActionGateFailure('self_route', 'WorkHub cannot delegate to itself');

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.

[P3] Unreachable. isCandidateSession already filters out the Coordination Session via isWorkHubCoordinationSessionTarget, and this only runs against a candidate found in that set, so self_route cannot fire. Either drop the branch or, if it is meant as a belt-and-braces assertion against a future candidate source, say so — as written it reads like a live guard.

}

/** @internal Transitional R2.4 regression harness; application code must use the Action Gate. */
export function createLegacyWorkHubControllerForTests(deps: {

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.

Not a finding — a question about the exit. The shared implementation now carries both behaviours behind deps.coordination, and production only ever takes one of them, so the other set of branches lives in production code kept reachable only by tests. That is the parallel path AGENTS.md asks us not to leave behind.

You have documented it as transitional, which is the right call for a slice boundary, so I am not treating it as a defect. What I would like recorded somewhere durable: which change deletes createLegacyWorkHubControllerForTests and the deps.coordination forks — Slice 5? If the answer lives only in this PR description it tends to become permanent.

@Astro-Han
Astro-Han merged commit 0e2074a into apache:mainAug 26, 2026
2 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

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

feat(workhub): add typed action gate - #3818

Merged
Astro-Han merged 9 commits into
apache:mainfrom
ARE404:feat/workhub-action-gate
Aug 26, 2026
Merged

feat(workhub): add typed action gate#3818
Astro-Han merged 9 commits into
apache:mainfrom
ARE404:feat/workhub-action-gate

Conversation

@ARE404

@ARE404ARE404 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements WorkHub Slice 4 from #3492 on top of the merged Slice 3 coordination-session work:

  • adds closed typed contracts for answer_here, delegate_existing, create_new, and clarify;
  • exposes a bounded Runtime-owned candidate set with opaque references;
  • introduces a deterministic Runtime Host Action Gate with fresh target validation, action replay, and non-destructive execution admission;
  • routes the production Desktop WorkHub path through that gate while keeping the R2.4 controller as a test-only regression harness.

This changes behavior: destructive cross-Session correction is deferred to Slice 5. Production recognizes correction before explicit creation, fails closed before a second delegation, and tells the user to stop the original work from its Session. Explicit creation without correction context remains available.

Refs #3492

Verification

  • Regression table was red on 89e4e20: normal variants containing please, an em dash, , or 不对 still reached create_new and failed with WorkHub Action Gate returned an unexpected disposition.
  • Production correction/create regressions: 2 passed. With an existing focus, all 6 English/Chinese correction-shaped creation cases fail closed before a second Action Gate action; without focus, the same 6 cases still execute create_new.
  • Desktop WorkHub affected suites: 108 passed.
  • Desktop main suites outside the filesystem sandbox: 1536 passed, 0 failed.
  • Desktop E2E: WorkHub defers destructive correction until linked delegation exists passed against the fixed production build (1 passed). The exact-head CI also runs the full Desktop E2E surface.
  • Runtime Host Slice 4 suites: 22 passed, including 7 Action Gate tests.
  • Desktop production renderer build passed.
  • Biome passed for all changed files; git diff --check passed.
  • Exact-head 2650a94ff5396454f0b239acced341a10273ce42 CI: test run 32948070394 / job 98113143994 passed, including the full Desktop E2E surface; windows_recovery run 32948070439 / job 98113065733 passed.

UI evidence

Before (ba1eec3): destructive correction exposed a “更正目标” control and replaced the running target.

workhub-slice4-before

After (89e4e20): the correction control is absent and production shows the Slice-5 deferral before a second delegation.

workhub-slice4-after

Both screenshots were captured from the real Electron E2E fixture using the same two-Session correction scenario. The before fixture passed 1/1 at ba1eec3; the after fixture passed 1/1 on the fixed branch.

Safety and authority boundaries

  • Strategy output cannot name arbitrary Session IDs, workspaces, tools, or permission modes.
  • Candidate discovery is scoped to the current Runtime Host and excludes archived, Coordination, side-conversation, and child Sessions.
  • The Gate refreshes candidates before admission and rejects stale or invented references, waiting targets, self-routing, invalid creation context, and conflicting action replays.
  • New ordinary Session identity is derived inside Runtime Host from the action identity; Desktop supplies only trusted workspace context.
  • Tool calls and permission elevation remain owned by the target ordinary Session. The closed WorkHub protocol cannot widen either ceiling.

Slice boundary: destructive correction is deferred to Slice 5

This PR intentionally does not expose replace or Stop through the Slice 4 Action Gate.

A destructive correction must prove durable linkage between the original delegation, the exact root Turn WorkHub owns, the correcting action, and the replacement submission. That linkage and recovery across the non-atomic Stop-to-submit seam must survive Runtime Host restart. Host-lifetime maps, TTLs, and retry lanes cannot provide that authority after restart.

Slice 4 therefore owns the typed gate, bounded candidates, fresh validation, replay, and non-destructive answer_here, clarify, delegate_existing, and create_new. Slice 5 will persist delegation/action linkage first, then add natural-language replacement and exact Stop ownership.

The decoder rejects replace; the production Gate has no Stop effect; Desktop exposes no correction picker. The legacy R2.4 correction path is test-only.

AI use

Select exactly one:

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

Tool(s) and scope: Codex implemented the Runtime Host/Desktop changes, regression tests, review fixes, and verification. Material commits include Generated-by: Codex trailers.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

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

Update on b65c5c2313:

CODE NO-GO — 4×P2

  • P2-1 correction without explicit intent fails confirmation_required before Stop.
  • P2-2 dropping replace when source outside bounded set silently forks while reporting corrected.
  • P2-3 concurrent replacements lack source lock → fan-out to different targets after Stop.
  • P2-4 record 48 KiB vs retry probe 32 KiB → retry after success hits commit_outcome_unknown.

Fix: make correction carry explicit stop text or relax gate; keep replace mandatory or fail; add source lease across actions; align byte limits. Hosted test: SUCCESS does not waive these.

简体中文四处权限/一致性阻塞。

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

Update on 48666a0ceb:

CODE NO-GO — 3×P2

  • P2 candidate path does not write admitted.targetTurnId to local map → natural-language correction cannot send replacement (fail-closed at 814). E2E workhub-reconstruction fails (32879372758).
  • P2 Stop-then-submit with swallowed replay leaves source stopped and target unconfirmed → retry gets stop_not_owned.
  • P2 48 KiB user + 8 KiB assistant JSON record can exceed 72 KiB retry lookup after escaping → retry becomes commit_outcome_unknown.
简体中文存在路径阻塞与重放问题。

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

Update on f2785c5dc0:

CODE NO-GO — 2×P2 plus required check red

  • P2 Stop-then-submit with swallowed replay leaves source stopped and target unconfirmed → retry stop_not_owned.
  • P2 JSON record escaped bytes exceed 72 KiB retry lookup → retry commit_outcome_unknown.

Note: natural-language correction P2 from prior head is now closed (gated receipt saved). Hosted test: FAILURE on format check (new test ternary).

简体中文仍有两处阻塞,另需格式化修复。

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

Update on 05d3d26e20:

CODE NO-GO — 2×P2 (carry-over, formatting fix only)

  • P2 Stop-then-submit without replay on target failure → retry stop_not_owned.
  • P2 JSON escaped record may exceed 72 KiB retry lookup → commit_outcome_unknown.

Formatting failure from prior head fixed; logic unchanged. Hosted test: QUEUED — not green.

简体中文仍有两处阻塞。

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

Update on 92d0947890:

[P2] #replacementRecoveries can exhaust to Host-wide outage

Capacity 256 is only released on Stop failure or target success. After Stop succeeds, permanent target rejections (e.g. session_busy) keep the recovery forever with no TTL/reaper — 256 failures exhaust replacements Host-wide as host_not_ready until restart.

Fix: give recoveries reconciled lifecycle / TTL for permanent failures.

Checks on 92d0947890d2aeec9a6363f17b68ff0850deb5b0 are test: SUCCESS — code is NO-GO.

简体中文异常恢复容量会耗尽。

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

I reviewed this head and found no blocking issues.

Fixes replacement recovery lifecycle (typed failure releases checkpoint, unknown retains fingerprint with 5-min TTL) — closes prior 256-capacity outage; definitive/unknown regression tests pass. Hosted test: SUCCESS (32924223915).

No new P0-P3.

简体中文该头无新增阻断。

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.

@ARE404
ARE404force-pushed the feat/workhub-action-gate branch from 3beac53 to 9532d2dCompareAugust 26, 2026 03:53

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

I reviewed this head and found no blocking issues.

Fixes Action Gate classification to check correction before explicit new-session creation; replace/Stop removed from production. Hosted test: SUCCESS (32935316010) and windows_recovery: SUCCESS.

No P0-P3.

简体中文该头无阻断。

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.

@jackwener
jackwenerforce-pushed the feat/workhub-action-gate branch from c9d74e5 to 2650a94CompareAugust 26, 2026 08:30

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

I reviewed this head and found no blocking issues.

Completes typed Action Gate: opaque candidateRef with fresh validation, idempotent replay with fingerprint conflict, self-route/target-waiting fail-closed; hosted test+windows_recovery SUCCESS.

No P0-P3.

简体中文该头无阻断。

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.

@M4n5ter
M4n5terforce-pushed the feat/workhub-action-gate branch from 2650a94 to 781a12fCompareAugust 26, 2026 09:45
Preserve Runtime-admitted root receipts for natural-language corrections and avoid deleting newer ownership after a concurrent Stop.\n\nGenerated-by: Codex
Resume the exact target submission after a replacement Stop and budget summary replay reads for worst-case JSON escaping.\n\nGenerated-by: Codex
Release recovery checkpoints after definitive target failures and expire uncertain outcomes after a bounded reconciliation window.\n\nGenerated-by: Codex
Recognize correction cues independently from punctuation, politeness, and the creation clause so focused corrections fail closed without weakening no-focus creation.
Generated-by: Codex
@M4n5ter
M4n5terforce-pushed the feat/workhub-action-gate branch from 781a12f to 3844445CompareAugust 26, 2026 09:53

@Astro-HanAstro-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 the Slice 4 gate as a whole rather than line by line, since the interesting question is whether the admission boundary actually holds.

It does. Two things I checked specifically because they looked risky and turned out to be right:

Epoch 51 is correctly derived above main's 50, with its own note — no collision.

The create_new create→submit pair is non-atomic but safe. A submit failure clears the replay entry, so a retry re-enters create. That is fine because workHubCreatedSessionId(actionId) is deterministic and #create probes probeStableSessionCreate by request fingerprint, returning success for existing. Same action id and same input yield the same fingerprint, so the retry reuses the session instead of conflicting or orphaning it. Worth a comment at the effect site, because the safety depends on a property two packages away.

The candidate-ref indirection is the right shape: a strategy proposal cannot name a Session id, and refreshing the set before admission means a model-selected reference cannot outlive the state it was chosen from.

Approving. Three P3s and one question inline; none of them blocks, and I am merging on that basis.

AI use: Claude Code assisted with source investigation; the analysis and conclusions are my own.

简体中文

我是把 Slice 4 的 gate 当成一个整体来看的,因为真正要回答的问题是这个准入边界站不站得住。

站得住。有两处看着危险、核完确认是对的:

epoch 51 在 main 的 50 之上正确推导,并带了自己的说明,没有撞号。

create_new 的 create→submit 非原子,但是安全的。 submit 失败会清掉重放记录,重试因此会重新进入 create。这没问题:workHubCreatedSessionId(actionId) 是确定性的,而 #create 通过 probeStableSessionCreate 按请求 fingerprint 探测,对 existing 直接返回成功。相同的 action id 和相同输入产生相同 fingerprint,所以重试会复用同一个 Session,既不冲突也不会留下孤儿。建议在 effect 处加一句注释,因为这个安全性依赖的是两个包之外的性质。

candidateRef 这层间接是对的形状:策略提案无法指名 Session id,而准入前刷新候选集意味着模型选出的引用不会比它所依据的状态活得更久。

Approve。行内三条 P3 和一个问题,都不阻塞,我据此合并。

result: WorkHubSubmission,
): result is Extract<WorkHubSubmission, { kind: 'submitted' }> {
return result.kind === 'submitted' && !result.steered;
export function workHubSurfaceFailure(error: unknown): WorkHubSurfaceFailure {

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.

[P3] This reconstructs a typed distinction by matching English prose, in the PR whose thesis is that the distinction is typed.

The code already exists: WorkHubActionGateFailureCode is candidate_set_stale | candidate_unavailable | target_waiting_for_user | self_route | action_conflict. The coordinator's #act then discards it, collapsing everything to session_busy or operation_conflict plus a message, and this function greps the message to get it back.

Every branch matches correctly today, which is why this is P3 and not higher. What makes it worth fixing anyway is that nothing protects it: the producer and the consumer are in different packages, so rewording a Host error silently downgrades a user from specific guidance to delivery_failed, with no compile error and no test that spans the boundary. 'source or target is not in' already matches no message I can find in this diff, which is roughly what that drift looks like.

Carrying the gate's code through the operation failure instead of flattening it would remove the second representation entirely.

const candidates = await this.candidates();
if (candidates.candidateSetId !== input.candidateSetId) {
throw new WorkHubActionGateFailure(
'candidate_set_stale',

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.

[P3] Freshness is enforced set-wide, but the invariant that matters is per-target. candidateSetId digests every candidate's updatedAt and status, so a message in any unrelated Session rotates it and this rejects a delegation whose own target never changed.

The window is small — the routing policy is synchronous, so it is about one routingEvidence() round trip — which is why P3. But it is the multi-Session case that WorkHub exists for, and that is exactly when other Sessions are producing messages.

I do not think there is a one-line fix: candidateRef is derived from candidateSetId, so per-candidate validation would need refs bound to per-Session state instead of to the set. Worth deciding deliberately rather than inheriting.


#assertTarget(target: WorkHubCoordinationCandidate): void {
if (target.sessionId === WORKHUB_COORDINATION_SESSION_ID) {
throw new WorkHubActionGateFailure('self_route', 'WorkHub cannot delegate to itself');

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.

[P3] Unreachable. isCandidateSession already filters out the Coordination Session via isWorkHubCoordinationSessionTarget, and this only runs against a candidate found in that set, so self_route cannot fire. Either drop the branch or, if it is meant as a belt-and-braces assertion against a future candidate source, say so — as written it reads like a live guard.

}

/** @internal Transitional R2.4 regression harness; application code must use the Action Gate. */
export function createLegacyWorkHubControllerForTests(deps: {

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.

Not a finding — a question about the exit. The shared implementation now carries both behaviours behind deps.coordination, and production only ever takes one of them, so the other set of branches lives in production code kept reachable only by tests. That is the parallel path AGENTS.md asks us not to leave behind.

You have documented it as transitional, which is the right call for a slice boundary, so I am not treating it as a defect. What I would like recorded somewhere durable: which change deletes createLegacyWorkHubControllerForTests and the deps.coordination forks — Slice 5? If the answer lives only in this PR description it tends to become permanent.

@Astro-Han
Astro-Han merged commit 0e2074a into apache:mainAug 26, 2026
2 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ARE404@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(workhub): add typed action gate by ARE404 · Pull Request #3818 · apache/maka · GitHub
Skip to content

feat(workhub): add typed action gate - #3818

Merged
Astro-Han merged 9 commits into
apache:mainfrom
ARE404:feat/workhub-action-gate
Aug 26, 2026
Merged

feat(workhub): add typed action gate#3818
Astro-Han merged 9 commits into
apache:mainfrom
ARE404:feat/workhub-action-gate

Conversation

@ARE404

@ARE404ARE404 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements WorkHub Slice 4 from #3492 on top of the merged Slice 3 coordination-session work:

  • adds closed typed contracts for answer_here, delegate_existing, create_new, and clarify;
  • exposes a bounded Runtime-owned candidate set with opaque references;
  • introduces a deterministic Runtime Host Action Gate with fresh target validation, action replay, and non-destructive execution admission;
  • routes the production Desktop WorkHub path through that gate while keeping the R2.4 controller as a test-only regression harness.

This changes behavior: destructive cross-Session correction is deferred to Slice 5. Production recognizes correction before explicit creation, fails closed before a second delegation, and tells the user to stop the original work from its Session. Explicit creation without correction context remains available.

Refs #3492

Verification

  • Regression table was red on 89e4e20: normal variants containing please, an em dash, , or 不对 still reached create_new and failed with WorkHub Action Gate returned an unexpected disposition.
  • Production correction/create regressions: 2 passed. With an existing focus, all 6 English/Chinese correction-shaped creation cases fail closed before a second Action Gate action; without focus, the same 6 cases still execute create_new.
  • Desktop WorkHub affected suites: 108 passed.
  • Desktop main suites outside the filesystem sandbox: 1536 passed, 0 failed.
  • Desktop E2E: WorkHub defers destructive correction until linked delegation exists passed against the fixed production build (1 passed). The exact-head CI also runs the full Desktop E2E surface.
  • Runtime Host Slice 4 suites: 22 passed, including 7 Action Gate tests.
  • Desktop production renderer build passed.
  • Biome passed for all changed files; git diff --check passed.
  • Exact-head 2650a94ff5396454f0b239acced341a10273ce42 CI: test run 32948070394 / job 98113143994 passed, including the full Desktop E2E surface; windows_recovery run 32948070439 / job 98113065733 passed.

UI evidence

Before (ba1eec3): destructive correction exposed a “更正目标” control and replaced the running target.

workhub-slice4-before

After (89e4e20): the correction control is absent and production shows the Slice-5 deferral before a second delegation.

workhub-slice4-after

Both screenshots were captured from the real Electron E2E fixture using the same two-Session correction scenario. The before fixture passed 1/1 at ba1eec3; the after fixture passed 1/1 on the fixed branch.

Safety and authority boundaries

  • Strategy output cannot name arbitrary Session IDs, workspaces, tools, or permission modes.
  • Candidate discovery is scoped to the current Runtime Host and excludes archived, Coordination, side-conversation, and child Sessions.
  • The Gate refreshes candidates before admission and rejects stale or invented references, waiting targets, self-routing, invalid creation context, and conflicting action replays.
  • New ordinary Session identity is derived inside Runtime Host from the action identity; Desktop supplies only trusted workspace context.
  • Tool calls and permission elevation remain owned by the target ordinary Session. The closed WorkHub protocol cannot widen either ceiling.

Slice boundary: destructive correction is deferred to Slice 5

This PR intentionally does not expose replace or Stop through the Slice 4 Action Gate.

A destructive correction must prove durable linkage between the original delegation, the exact root Turn WorkHub owns, the correcting action, and the replacement submission. That linkage and recovery across the non-atomic Stop-to-submit seam must survive Runtime Host restart. Host-lifetime maps, TTLs, and retry lanes cannot provide that authority after restart.

Slice 4 therefore owns the typed gate, bounded candidates, fresh validation, replay, and non-destructive answer_here, clarify, delegate_existing, and create_new. Slice 5 will persist delegation/action linkage first, then add natural-language replacement and exact Stop ownership.

The decoder rejects replace; the production Gate has no Stop effect; Desktop exposes no correction picker. The legacy R2.4 correction path is test-only.

AI use

Select exactly one:

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

Tool(s) and scope: Codex implemented the Runtime Host/Desktop changes, regression tests, review fixes, and verification. Material commits include Generated-by: Codex trailers.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

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

Update on b65c5c2313:

CODE NO-GO — 4×P2

  • P2-1 correction without explicit intent fails confirmation_required before Stop.
  • P2-2 dropping replace when source outside bounded set silently forks while reporting corrected.
  • P2-3 concurrent replacements lack source lock → fan-out to different targets after Stop.
  • P2-4 record 48 KiB vs retry probe 32 KiB → retry after success hits commit_outcome_unknown.

Fix: make correction carry explicit stop text or relax gate; keep replace mandatory or fail; add source lease across actions; align byte limits. Hosted test: SUCCESS does not waive these.

简体中文四处权限/一致性阻塞。

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

Update on 48666a0ceb:

CODE NO-GO — 3×P2

  • P2 candidate path does not write admitted.targetTurnId to local map → natural-language correction cannot send replacement (fail-closed at 814). E2E workhub-reconstruction fails (32879372758).
  • P2 Stop-then-submit with swallowed replay leaves source stopped and target unconfirmed → retry gets stop_not_owned.
  • P2 48 KiB user + 8 KiB assistant JSON record can exceed 72 KiB retry lookup after escaping → retry becomes commit_outcome_unknown.
简体中文存在路径阻塞与重放问题。

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

Update on f2785c5dc0:

CODE NO-GO — 2×P2 plus required check red

  • P2 Stop-then-submit with swallowed replay leaves source stopped and target unconfirmed → retry stop_not_owned.
  • P2 JSON record escaped bytes exceed 72 KiB retry lookup → retry commit_outcome_unknown.

Note: natural-language correction P2 from prior head is now closed (gated receipt saved). Hosted test: FAILURE on format check (new test ternary).

简体中文仍有两处阻塞,另需格式化修复。

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

Update on 05d3d26e20:

CODE NO-GO — 2×P2 (carry-over, formatting fix only)

  • P2 Stop-then-submit without replay on target failure → retry stop_not_owned.
  • P2 JSON escaped record may exceed 72 KiB retry lookup → commit_outcome_unknown.

Formatting failure from prior head fixed; logic unchanged. Hosted test: QUEUED — not green.

简体中文仍有两处阻塞。

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

Update on 92d0947890:

[P2] #replacementRecoveries can exhaust to Host-wide outage

Capacity 256 is only released on Stop failure or target success. After Stop succeeds, permanent target rejections (e.g. session_busy) keep the recovery forever with no TTL/reaper — 256 failures exhaust replacements Host-wide as host_not_ready until restart.

Fix: give recoveries reconciled lifecycle / TTL for permanent failures.

Checks on 92d0947890d2aeec9a6363f17b68ff0850deb5b0 are test: SUCCESS — code is NO-GO.

简体中文异常恢复容量会耗尽。

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

I reviewed this head and found no blocking issues.

Fixes replacement recovery lifecycle (typed failure releases checkpoint, unknown retains fingerprint with 5-min TTL) — closes prior 256-capacity outage; definitive/unknown regression tests pass. Hosted test: SUCCESS (32924223915).

No new P0-P3.

简体中文该头无新增阻断。

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.

@ARE404
ARE404force-pushed the feat/workhub-action-gate branch from 3beac53 to 9532d2dCompareAugust 26, 2026 03:53

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

I reviewed this head and found no blocking issues.

Fixes Action Gate classification to check correction before explicit new-session creation; replace/Stop removed from production. Hosted test: SUCCESS (32935316010) and windows_recovery: SUCCESS.

No P0-P3.

简体中文该头无阻断。

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.

@jackwener
jackwenerforce-pushed the feat/workhub-action-gate branch from c9d74e5 to 2650a94CompareAugust 26, 2026 08:30

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

I reviewed this head and found no blocking issues.

Completes typed Action Gate: opaque candidateRef with fresh validation, idempotent replay with fingerprint conflict, self-route/target-waiting fail-closed; hosted test+windows_recovery SUCCESS.

No P0-P3.

简体中文该头无阻断。

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.

@M4n5ter
M4n5terforce-pushed the feat/workhub-action-gate branch from 2650a94 to 781a12fCompareAugust 26, 2026 09:45
Preserve Runtime-admitted root receipts for natural-language corrections and avoid deleting newer ownership after a concurrent Stop.\n\nGenerated-by: Codex
Resume the exact target submission after a replacement Stop and budget summary replay reads for worst-case JSON escaping.\n\nGenerated-by: Codex
Release recovery checkpoints after definitive target failures and expire uncertain outcomes after a bounded reconciliation window.\n\nGenerated-by: Codex
Recognize correction cues independently from punctuation, politeness, and the creation clause so focused corrections fail closed without weakening no-focus creation.
Generated-by: Codex
@M4n5ter
M4n5terforce-pushed the feat/workhub-action-gate branch from 781a12f to 3844445CompareAugust 26, 2026 09:53

@Astro-HanAstro-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 the Slice 4 gate as a whole rather than line by line, since the interesting question is whether the admission boundary actually holds.

It does. Two things I checked specifically because they looked risky and turned out to be right:

Epoch 51 is correctly derived above main's 50, with its own note — no collision.

The create_new create→submit pair is non-atomic but safe. A submit failure clears the replay entry, so a retry re-enters create. That is fine because workHubCreatedSessionId(actionId) is deterministic and #create probes probeStableSessionCreate by request fingerprint, returning success for existing. Same action id and same input yield the same fingerprint, so the retry reuses the session instead of conflicting or orphaning it. Worth a comment at the effect site, because the safety depends on a property two packages away.

The candidate-ref indirection is the right shape: a strategy proposal cannot name a Session id, and refreshing the set before admission means a model-selected reference cannot outlive the state it was chosen from.

Approving. Three P3s and one question inline; none of them blocks, and I am merging on that basis.

AI use: Claude Code assisted with source investigation; the analysis and conclusions are my own.

简体中文

我是把 Slice 4 的 gate 当成一个整体来看的,因为真正要回答的问题是这个准入边界站不站得住。

站得住。有两处看着危险、核完确认是对的:

epoch 51 在 main 的 50 之上正确推导,并带了自己的说明,没有撞号。

create_new 的 create→submit 非原子,但是安全的。 submit 失败会清掉重放记录,重试因此会重新进入 create。这没问题:workHubCreatedSessionId(actionId) 是确定性的,而 #create 通过 probeStableSessionCreate 按请求 fingerprint 探测,对 existing 直接返回成功。相同的 action id 和相同输入产生相同 fingerprint,所以重试会复用同一个 Session,既不冲突也不会留下孤儿。建议在 effect 处加一句注释,因为这个安全性依赖的是两个包之外的性质。

candidateRef 这层间接是对的形状:策略提案无法指名 Session id,而准入前刷新候选集意味着模型选出的引用不会比它所依据的状态活得更久。

Approve。行内三条 P3 和一个问题,都不阻塞,我据此合并。

result: WorkHubSubmission,
): result is Extract<WorkHubSubmission, { kind: 'submitted' }> {
return result.kind === 'submitted' && !result.steered;
export function workHubSurfaceFailure(error: unknown): WorkHubSurfaceFailure {

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.

[P3] This reconstructs a typed distinction by matching English prose, in the PR whose thesis is that the distinction is typed.

The code already exists: WorkHubActionGateFailureCode is candidate_set_stale | candidate_unavailable | target_waiting_for_user | self_route | action_conflict. The coordinator's #act then discards it, collapsing everything to session_busy or operation_conflict plus a message, and this function greps the message to get it back.

Every branch matches correctly today, which is why this is P3 and not higher. What makes it worth fixing anyway is that nothing protects it: the producer and the consumer are in different packages, so rewording a Host error silently downgrades a user from specific guidance to delivery_failed, with no compile error and no test that spans the boundary. 'source or target is not in' already matches no message I can find in this diff, which is roughly what that drift looks like.

Carrying the gate's code through the operation failure instead of flattening it would remove the second representation entirely.

const candidates = await this.candidates();
if (candidates.candidateSetId !== input.candidateSetId) {
throw new WorkHubActionGateFailure(
'candidate_set_stale',

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.

[P3] Freshness is enforced set-wide, but the invariant that matters is per-target. candidateSetId digests every candidate's updatedAt and status, so a message in any unrelated Session rotates it and this rejects a delegation whose own target never changed.

The window is small — the routing policy is synchronous, so it is about one routingEvidence() round trip — which is why P3. But it is the multi-Session case that WorkHub exists for, and that is exactly when other Sessions are producing messages.

I do not think there is a one-line fix: candidateRef is derived from candidateSetId, so per-candidate validation would need refs bound to per-Session state instead of to the set. Worth deciding deliberately rather than inheriting.


#assertTarget(target: WorkHubCoordinationCandidate): void {
if (target.sessionId === WORKHUB_COORDINATION_SESSION_ID) {
throw new WorkHubActionGateFailure('self_route', 'WorkHub cannot delegate to itself');

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.

[P3] Unreachable. isCandidateSession already filters out the Coordination Session via isWorkHubCoordinationSessionTarget, and this only runs against a candidate found in that set, so self_route cannot fire. Either drop the branch or, if it is meant as a belt-and-braces assertion against a future candidate source, say so — as written it reads like a live guard.

}

/** @internal Transitional R2.4 regression harness; application code must use the Action Gate. */
export function createLegacyWorkHubControllerForTests(deps: {

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.

Not a finding — a question about the exit. The shared implementation now carries both behaviours behind deps.coordination, and production only ever takes one of them, so the other set of branches lives in production code kept reachable only by tests. That is the parallel path AGENTS.md asks us not to leave behind.

You have documented it as transitional, which is the right call for a slice boundary, so I am not treating it as a defect. What I would like recorded somewhere durable: which change deletes createLegacyWorkHubControllerForTests and the deps.coordination forks — Slice 5? If the answer lives only in this PR description it tends to become permanent.

@Astro-Han
Astro-Han merged commit 0e2074a into apache:mainAug 26, 2026
2 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ARE404@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(workhub): add typed action gate by ARE404 · Pull Request #3818 · apache/maka · GitHub
Skip to content

feat(workhub): add typed action gate - #3818

Merged
Astro-Han merged 9 commits into
apache:mainfrom
ARE404:feat/workhub-action-gate
Aug 26, 2026
Merged

feat(workhub): add typed action gate#3818
Astro-Han merged 9 commits into
apache:mainfrom
ARE404:feat/workhub-action-gate

Conversation

@ARE404

@ARE404ARE404 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements WorkHub Slice 4 from #3492 on top of the merged Slice 3 coordination-session work:

  • adds closed typed contracts for answer_here, delegate_existing, create_new, and clarify;
  • exposes a bounded Runtime-owned candidate set with opaque references;
  • introduces a deterministic Runtime Host Action Gate with fresh target validation, action replay, and non-destructive execution admission;
  • routes the production Desktop WorkHub path through that gate while keeping the R2.4 controller as a test-only regression harness.

This changes behavior: destructive cross-Session correction is deferred to Slice 5. Production recognizes correction before explicit creation, fails closed before a second delegation, and tells the user to stop the original work from its Session. Explicit creation without correction context remains available.

Refs #3492

Verification

  • Regression table was red on 89e4e20: normal variants containing please, an em dash, , or 不对 still reached create_new and failed with WorkHub Action Gate returned an unexpected disposition.
  • Production correction/create regressions: 2 passed. With an existing focus, all 6 English/Chinese correction-shaped creation cases fail closed before a second Action Gate action; without focus, the same 6 cases still execute create_new.
  • Desktop WorkHub affected suites: 108 passed.
  • Desktop main suites outside the filesystem sandbox: 1536 passed, 0 failed.
  • Desktop E2E: WorkHub defers destructive correction until linked delegation exists passed against the fixed production build (1 passed). The exact-head CI also runs the full Desktop E2E surface.
  • Runtime Host Slice 4 suites: 22 passed, including 7 Action Gate tests.
  • Desktop production renderer build passed.
  • Biome passed for all changed files; git diff --check passed.
  • Exact-head 2650a94ff5396454f0b239acced341a10273ce42 CI: test run 32948070394 / job 98113143994 passed, including the full Desktop E2E surface; windows_recovery run 32948070439 / job 98113065733 passed.

UI evidence

Before (ba1eec3): destructive correction exposed a “更正目标” control and replaced the running target.

workhub-slice4-before

After (89e4e20): the correction control is absent and production shows the Slice-5 deferral before a second delegation.

workhub-slice4-after

Both screenshots were captured from the real Electron E2E fixture using the same two-Session correction scenario. The before fixture passed 1/1 at ba1eec3; the after fixture passed 1/1 on the fixed branch.

Safety and authority boundaries

  • Strategy output cannot name arbitrary Session IDs, workspaces, tools, or permission modes.
  • Candidate discovery is scoped to the current Runtime Host and excludes archived, Coordination, side-conversation, and child Sessions.
  • The Gate refreshes candidates before admission and rejects stale or invented references, waiting targets, self-routing, invalid creation context, and conflicting action replays.
  • New ordinary Session identity is derived inside Runtime Host from the action identity; Desktop supplies only trusted workspace context.
  • Tool calls and permission elevation remain owned by the target ordinary Session. The closed WorkHub protocol cannot widen either ceiling.

Slice boundary: destructive correction is deferred to Slice 5

This PR intentionally does not expose replace or Stop through the Slice 4 Action Gate.

A destructive correction must prove durable linkage between the original delegation, the exact root Turn WorkHub owns, the correcting action, and the replacement submission. That linkage and recovery across the non-atomic Stop-to-submit seam must survive Runtime Host restart. Host-lifetime maps, TTLs, and retry lanes cannot provide that authority after restart.

Slice 4 therefore owns the typed gate, bounded candidates, fresh validation, replay, and non-destructive answer_here, clarify, delegate_existing, and create_new. Slice 5 will persist delegation/action linkage first, then add natural-language replacement and exact Stop ownership.

The decoder rejects replace; the production Gate has no Stop effect; Desktop exposes no correction picker. The legacy R2.4 correction path is test-only.

AI use

Select exactly one:

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

Tool(s) and scope: Codex implemented the Runtime Host/Desktop changes, regression tests, review fixes, and verification. Material commits include Generated-by: Codex trailers.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

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

Update on b65c5c2313:

CODE NO-GO — 4×P2

  • P2-1 correction without explicit intent fails confirmation_required before Stop.
  • P2-2 dropping replace when source outside bounded set silently forks while reporting corrected.
  • P2-3 concurrent replacements lack source lock → fan-out to different targets after Stop.
  • P2-4 record 48 KiB vs retry probe 32 KiB → retry after success hits commit_outcome_unknown.

Fix: make correction carry explicit stop text or relax gate; keep replace mandatory or fail; add source lease across actions; align byte limits. Hosted test: SUCCESS does not waive these.

简体中文四处权限/一致性阻塞。

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

Update on 48666a0ceb:

CODE NO-GO — 3×P2

  • P2 candidate path does not write admitted.targetTurnId to local map → natural-language correction cannot send replacement (fail-closed at 814). E2E workhub-reconstruction fails (32879372758).
  • P2 Stop-then-submit with swallowed replay leaves source stopped and target unconfirmed → retry gets stop_not_owned.
  • P2 48 KiB user + 8 KiB assistant JSON record can exceed 72 KiB retry lookup after escaping → retry becomes commit_outcome_unknown.
简体中文存在路径阻塞与重放问题。

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

Update on f2785c5dc0:

CODE NO-GO — 2×P2 plus required check red

  • P2 Stop-then-submit with swallowed replay leaves source stopped and target unconfirmed → retry stop_not_owned.
  • P2 JSON record escaped bytes exceed 72 KiB retry lookup → retry commit_outcome_unknown.

Note: natural-language correction P2 from prior head is now closed (gated receipt saved). Hosted test: FAILURE on format check (new test ternary).

简体中文仍有两处阻塞,另需格式化修复。

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

Update on 05d3d26e20:

CODE NO-GO — 2×P2 (carry-over, formatting fix only)

  • P2 Stop-then-submit without replay on target failure → retry stop_not_owned.
  • P2 JSON escaped record may exceed 72 KiB retry lookup → commit_outcome_unknown.

Formatting failure from prior head fixed; logic unchanged. Hosted test: QUEUED — not green.

简体中文仍有两处阻塞。

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

Update on 92d0947890:

[P2] #replacementRecoveries can exhaust to Host-wide outage

Capacity 256 is only released on Stop failure or target success. After Stop succeeds, permanent target rejections (e.g. session_busy) keep the recovery forever with no TTL/reaper — 256 failures exhaust replacements Host-wide as host_not_ready until restart.

Fix: give recoveries reconciled lifecycle / TTL for permanent failures.

Checks on 92d0947890d2aeec9a6363f17b68ff0850deb5b0 are test: SUCCESS — code is NO-GO.

简体中文异常恢复容量会耗尽。

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

I reviewed this head and found no blocking issues.

Fixes replacement recovery lifecycle (typed failure releases checkpoint, unknown retains fingerprint with 5-min TTL) — closes prior 256-capacity outage; definitive/unknown regression tests pass. Hosted test: SUCCESS (32924223915).

No new P0-P3.

简体中文该头无新增阻断。

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.

@ARE404
ARE404force-pushed the feat/workhub-action-gate branch from 3beac53 to 9532d2dCompareAugust 26, 2026 03:53

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

I reviewed this head and found no blocking issues.

Fixes Action Gate classification to check correction before explicit new-session creation; replace/Stop removed from production. Hosted test: SUCCESS (32935316010) and windows_recovery: SUCCESS.

No P0-P3.

简体中文该头无阻断。

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.

@jackwener
jackwenerforce-pushed the feat/workhub-action-gate branch from c9d74e5 to 2650a94CompareAugust 26, 2026 08:30

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

I reviewed this head and found no blocking issues.

Completes typed Action Gate: opaque candidateRef with fresh validation, idempotent replay with fingerprint conflict, self-route/target-waiting fail-closed; hosted test+windows_recovery SUCCESS.

No P0-P3.

简体中文该头无阻断。

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.

@M4n5ter
M4n5terforce-pushed the feat/workhub-action-gate branch from 2650a94 to 781a12fCompareAugust 26, 2026 09:45
Preserve Runtime-admitted root receipts for natural-language corrections and avoid deleting newer ownership after a concurrent Stop.\n\nGenerated-by: Codex
Resume the exact target submission after a replacement Stop and budget summary replay reads for worst-case JSON escaping.\n\nGenerated-by: Codex
Release recovery checkpoints after definitive target failures and expire uncertain outcomes after a bounded reconciliation window.\n\nGenerated-by: Codex
Recognize correction cues independently from punctuation, politeness, and the creation clause so focused corrections fail closed without weakening no-focus creation.
Generated-by: Codex
@M4n5ter
M4n5terforce-pushed the feat/workhub-action-gate branch from 781a12f to 3844445CompareAugust 26, 2026 09:53

@Astro-HanAstro-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 the Slice 4 gate as a whole rather than line by line, since the interesting question is whether the admission boundary actually holds.

It does. Two things I checked specifically because they looked risky and turned out to be right:

Epoch 51 is correctly derived above main's 50, with its own note — no collision.

The create_new create→submit pair is non-atomic but safe. A submit failure clears the replay entry, so a retry re-enters create. That is fine because workHubCreatedSessionId(actionId) is deterministic and #create probes probeStableSessionCreate by request fingerprint, returning success for existing. Same action id and same input yield the same fingerprint, so the retry reuses the session instead of conflicting or orphaning it. Worth a comment at the effect site, because the safety depends on a property two packages away.

The candidate-ref indirection is the right shape: a strategy proposal cannot name a Session id, and refreshing the set before admission means a model-selected reference cannot outlive the state it was chosen from.

Approving. Three P3s and one question inline; none of them blocks, and I am merging on that basis.

AI use: Claude Code assisted with source investigation; the analysis and conclusions are my own.

简体中文

我是把 Slice 4 的 gate 当成一个整体来看的,因为真正要回答的问题是这个准入边界站不站得住。

站得住。有两处看着危险、核完确认是对的:

epoch 51 在 main 的 50 之上正确推导,并带了自己的说明,没有撞号。

create_new 的 create→submit 非原子,但是安全的。 submit 失败会清掉重放记录,重试因此会重新进入 create。这没问题:workHubCreatedSessionId(actionId) 是确定性的,而 #create 通过 probeStableSessionCreate 按请求 fingerprint 探测,对 existing 直接返回成功。相同的 action id 和相同输入产生相同 fingerprint,所以重试会复用同一个 Session,既不冲突也不会留下孤儿。建议在 effect 处加一句注释,因为这个安全性依赖的是两个包之外的性质。

candidateRef 这层间接是对的形状:策略提案无法指名 Session id,而准入前刷新候选集意味着模型选出的引用不会比它所依据的状态活得更久。

Approve。行内三条 P3 和一个问题,都不阻塞,我据此合并。

result: WorkHubSubmission,
): result is Extract<WorkHubSubmission, { kind: 'submitted' }> {
return result.kind === 'submitted' && !result.steered;
export function workHubSurfaceFailure(error: unknown): WorkHubSurfaceFailure {

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.

[P3] This reconstructs a typed distinction by matching English prose, in the PR whose thesis is that the distinction is typed.

The code already exists: WorkHubActionGateFailureCode is candidate_set_stale | candidate_unavailable | target_waiting_for_user | self_route | action_conflict. The coordinator's #act then discards it, collapsing everything to session_busy or operation_conflict plus a message, and this function greps the message to get it back.

Every branch matches correctly today, which is why this is P3 and not higher. What makes it worth fixing anyway is that nothing protects it: the producer and the consumer are in different packages, so rewording a Host error silently downgrades a user from specific guidance to delivery_failed, with no compile error and no test that spans the boundary. 'source or target is not in' already matches no message I can find in this diff, which is roughly what that drift looks like.

Carrying the gate's code through the operation failure instead of flattening it would remove the second representation entirely.

const candidates = await this.candidates();
if (candidates.candidateSetId !== input.candidateSetId) {
throw new WorkHubActionGateFailure(
'candidate_set_stale',

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.

[P3] Freshness is enforced set-wide, but the invariant that matters is per-target. candidateSetId digests every candidate's updatedAt and status, so a message in any unrelated Session rotates it and this rejects a delegation whose own target never changed.

The window is small — the routing policy is synchronous, so it is about one routingEvidence() round trip — which is why P3. But it is the multi-Session case that WorkHub exists for, and that is exactly when other Sessions are producing messages.

I do not think there is a one-line fix: candidateRef is derived from candidateSetId, so per-candidate validation would need refs bound to per-Session state instead of to the set. Worth deciding deliberately rather than inheriting.


#assertTarget(target: WorkHubCoordinationCandidate): void {
if (target.sessionId === WORKHUB_COORDINATION_SESSION_ID) {
throw new WorkHubActionGateFailure('self_route', 'WorkHub cannot delegate to itself');

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.

[P3] Unreachable. isCandidateSession already filters out the Coordination Session via isWorkHubCoordinationSessionTarget, and this only runs against a candidate found in that set, so self_route cannot fire. Either drop the branch or, if it is meant as a belt-and-braces assertion against a future candidate source, say so — as written it reads like a live guard.

}

/** @internal Transitional R2.4 regression harness; application code must use the Action Gate. */
export function createLegacyWorkHubControllerForTests(deps: {

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.

Not a finding — a question about the exit. The shared implementation now carries both behaviours behind deps.coordination, and production only ever takes one of them, so the other set of branches lives in production code kept reachable only by tests. That is the parallel path AGENTS.md asks us not to leave behind.

You have documented it as transitional, which is the right call for a slice boundary, so I am not treating it as a defect. What I would like recorded somewhere durable: which change deletes createLegacyWorkHubControllerForTests and the deps.coordination forks — Slice 5? If the answer lives only in this PR description it tends to become permanent.

@Astro-Han
Astro-Han merged commit 0e2074a into apache:mainAug 26, 2026
2 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

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

feat(workhub): add typed action gate - #3818

Merged
Astro-Han merged 9 commits into
apache:mainfrom
ARE404:feat/workhub-action-gate
Aug 26, 2026
Merged

feat(workhub): add typed action gate#3818
Astro-Han merged 9 commits into
apache:mainfrom
ARE404:feat/workhub-action-gate

Conversation

@ARE404

@ARE404ARE404 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements WorkHub Slice 4 from #3492 on top of the merged Slice 3 coordination-session work:

  • adds closed typed contracts for answer_here, delegate_existing, create_new, and clarify;
  • exposes a bounded Runtime-owned candidate set with opaque references;
  • introduces a deterministic Runtime Host Action Gate with fresh target validation, action replay, and non-destructive execution admission;
  • routes the production Desktop WorkHub path through that gate while keeping the R2.4 controller as a test-only regression harness.

This changes behavior: destructive cross-Session correction is deferred to Slice 5. Production recognizes correction before explicit creation, fails closed before a second delegation, and tells the user to stop the original work from its Session. Explicit creation without correction context remains available.

Refs #3492

Verification

  • Regression table was red on 89e4e20: normal variants containing please, an em dash, , or 不对 still reached create_new and failed with WorkHub Action Gate returned an unexpected disposition.
  • Production correction/create regressions: 2 passed. With an existing focus, all 6 English/Chinese correction-shaped creation cases fail closed before a second Action Gate action; without focus, the same 6 cases still execute create_new.
  • Desktop WorkHub affected suites: 108 passed.
  • Desktop main suites outside the filesystem sandbox: 1536 passed, 0 failed.
  • Desktop E2E: WorkHub defers destructive correction until linked delegation exists passed against the fixed production build (1 passed). The exact-head CI also runs the full Desktop E2E surface.
  • Runtime Host Slice 4 suites: 22 passed, including 7 Action Gate tests.
  • Desktop production renderer build passed.
  • Biome passed for all changed files; git diff --check passed.
  • Exact-head 2650a94ff5396454f0b239acced341a10273ce42 CI: test run 32948070394 / job 98113143994 passed, including the full Desktop E2E surface; windows_recovery run 32948070439 / job 98113065733 passed.

UI evidence

Before (ba1eec3): destructive correction exposed a “更正目标” control and replaced the running target.

workhub-slice4-before

After (89e4e20): the correction control is absent and production shows the Slice-5 deferral before a second delegation.

workhub-slice4-after

Both screenshots were captured from the real Electron E2E fixture using the same two-Session correction scenario. The before fixture passed 1/1 at ba1eec3; the after fixture passed 1/1 on the fixed branch.

Safety and authority boundaries

  • Strategy output cannot name arbitrary Session IDs, workspaces, tools, or permission modes.
  • Candidate discovery is scoped to the current Runtime Host and excludes archived, Coordination, side-conversation, and child Sessions.
  • The Gate refreshes candidates before admission and rejects stale or invented references, waiting targets, self-routing, invalid creation context, and conflicting action replays.
  • New ordinary Session identity is derived inside Runtime Host from the action identity; Desktop supplies only trusted workspace context.
  • Tool calls and permission elevation remain owned by the target ordinary Session. The closed WorkHub protocol cannot widen either ceiling.

Slice boundary: destructive correction is deferred to Slice 5

This PR intentionally does not expose replace or Stop through the Slice 4 Action Gate.

A destructive correction must prove durable linkage between the original delegation, the exact root Turn WorkHub owns, the correcting action, and the replacement submission. That linkage and recovery across the non-atomic Stop-to-submit seam must survive Runtime Host restart. Host-lifetime maps, TTLs, and retry lanes cannot provide that authority after restart.

Slice 4 therefore owns the typed gate, bounded candidates, fresh validation, replay, and non-destructive answer_here, clarify, delegate_existing, and create_new. Slice 5 will persist delegation/action linkage first, then add natural-language replacement and exact Stop ownership.

The decoder rejects replace; the production Gate has no Stop effect; Desktop exposes no correction picker. The legacy R2.4 correction path is test-only.

AI use

Select exactly one:

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

Tool(s) and scope: Codex implemented the Runtime Host/Desktop changes, regression tests, review fixes, and verification. Material commits include Generated-by: Codex trailers.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

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

Update on b65c5c2313:

CODE NO-GO — 4×P2

  • P2-1 correction without explicit intent fails confirmation_required before Stop.
  • P2-2 dropping replace when source outside bounded set silently forks while reporting corrected.
  • P2-3 concurrent replacements lack source lock → fan-out to different targets after Stop.
  • P2-4 record 48 KiB vs retry probe 32 KiB → retry after success hits commit_outcome_unknown.

Fix: make correction carry explicit stop text or relax gate; keep replace mandatory or fail; add source lease across actions; align byte limits. Hosted test: SUCCESS does not waive these.

简体中文四处权限/一致性阻塞。

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

Update on 48666a0ceb:

CODE NO-GO — 3×P2

  • P2 candidate path does not write admitted.targetTurnId to local map → natural-language correction cannot send replacement (fail-closed at 814). E2E workhub-reconstruction fails (32879372758).
  • P2 Stop-then-submit with swallowed replay leaves source stopped and target unconfirmed → retry gets stop_not_owned.
  • P2 48 KiB user + 8 KiB assistant JSON record can exceed 72 KiB retry lookup after escaping → retry becomes commit_outcome_unknown.
简体中文存在路径阻塞与重放问题。

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

Update on f2785c5dc0:

CODE NO-GO — 2×P2 plus required check red

  • P2 Stop-then-submit with swallowed replay leaves source stopped and target unconfirmed → retry stop_not_owned.
  • P2 JSON record escaped bytes exceed 72 KiB retry lookup → retry commit_outcome_unknown.

Note: natural-language correction P2 from prior head is now closed (gated receipt saved). Hosted test: FAILURE on format check (new test ternary).

简体中文仍有两处阻塞,另需格式化修复。

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

Update on 05d3d26e20:

CODE NO-GO — 2×P2 (carry-over, formatting fix only)

  • P2 Stop-then-submit without replay on target failure → retry stop_not_owned.
  • P2 JSON escaped record may exceed 72 KiB retry lookup → commit_outcome_unknown.

Formatting failure from prior head fixed; logic unchanged. Hosted test: QUEUED — not green.

简体中文仍有两处阻塞。

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

Update on 92d0947890:

[P2] #replacementRecoveries can exhaust to Host-wide outage

Capacity 256 is only released on Stop failure or target success. After Stop succeeds, permanent target rejections (e.g. session_busy) keep the recovery forever with no TTL/reaper — 256 failures exhaust replacements Host-wide as host_not_ready until restart.

Fix: give recoveries reconciled lifecycle / TTL for permanent failures.

Checks on 92d0947890d2aeec9a6363f17b68ff0850deb5b0 are test: SUCCESS — code is NO-GO.

简体中文异常恢复容量会耗尽。

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

I reviewed this head and found no blocking issues.

Fixes replacement recovery lifecycle (typed failure releases checkpoint, unknown retains fingerprint with 5-min TTL) — closes prior 256-capacity outage; definitive/unknown regression tests pass. Hosted test: SUCCESS (32924223915).

No new P0-P3.

简体中文该头无新增阻断。

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.

@ARE404
ARE404force-pushed the feat/workhub-action-gate branch from 3beac53 to 9532d2dCompareAugust 26, 2026 03:53

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

I reviewed this head and found no blocking issues.

Fixes Action Gate classification to check correction before explicit new-session creation; replace/Stop removed from production. Hosted test: SUCCESS (32935316010) and windows_recovery: SUCCESS.

No P0-P3.

简体中文该头无阻断。

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.

@jackwener
jackwenerforce-pushed the feat/workhub-action-gate branch from c9d74e5 to 2650a94CompareAugust 26, 2026 08:30

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

I reviewed this head and found no blocking issues.

Completes typed Action Gate: opaque candidateRef with fresh validation, idempotent replay with fingerprint conflict, self-route/target-waiting fail-closed; hosted test+windows_recovery SUCCESS.

No P0-P3.

简体中文该头无阻断。

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.

@M4n5ter
M4n5terforce-pushed the feat/workhub-action-gate branch from 2650a94 to 781a12fCompareAugust 26, 2026 09:45
Preserve Runtime-admitted root receipts for natural-language corrections and avoid deleting newer ownership after a concurrent Stop.\n\nGenerated-by: Codex
Resume the exact target submission after a replacement Stop and budget summary replay reads for worst-case JSON escaping.\n\nGenerated-by: Codex
Release recovery checkpoints after definitive target failures and expire uncertain outcomes after a bounded reconciliation window.\n\nGenerated-by: Codex
Recognize correction cues independently from punctuation, politeness, and the creation clause so focused corrections fail closed without weakening no-focus creation.
Generated-by: Codex
@M4n5ter
M4n5terforce-pushed the feat/workhub-action-gate branch from 781a12f to 3844445CompareAugust 26, 2026 09:53

@Astro-HanAstro-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 the Slice 4 gate as a whole rather than line by line, since the interesting question is whether the admission boundary actually holds.

It does. Two things I checked specifically because they looked risky and turned out to be right:

Epoch 51 is correctly derived above main's 50, with its own note — no collision.

The create_new create→submit pair is non-atomic but safe. A submit failure clears the replay entry, so a retry re-enters create. That is fine because workHubCreatedSessionId(actionId) is deterministic and #create probes probeStableSessionCreate by request fingerprint, returning success for existing. Same action id and same input yield the same fingerprint, so the retry reuses the session instead of conflicting or orphaning it. Worth a comment at the effect site, because the safety depends on a property two packages away.

The candidate-ref indirection is the right shape: a strategy proposal cannot name a Session id, and refreshing the set before admission means a model-selected reference cannot outlive the state it was chosen from.

Approving. Three P3s and one question inline; none of them blocks, and I am merging on that basis.

AI use: Claude Code assisted with source investigation; the analysis and conclusions are my own.

简体中文

我是把 Slice 4 的 gate 当成一个整体来看的,因为真正要回答的问题是这个准入边界站不站得住。

站得住。有两处看着危险、核完确认是对的:

epoch 51 在 main 的 50 之上正确推导,并带了自己的说明,没有撞号。

create_new 的 create→submit 非原子,但是安全的。 submit 失败会清掉重放记录,重试因此会重新进入 create。这没问题:workHubCreatedSessionId(actionId) 是确定性的,而 #create 通过 probeStableSessionCreate 按请求 fingerprint 探测,对 existing 直接返回成功。相同的 action id 和相同输入产生相同 fingerprint,所以重试会复用同一个 Session,既不冲突也不会留下孤儿。建议在 effect 处加一句注释,因为这个安全性依赖的是两个包之外的性质。

candidateRef 这层间接是对的形状:策略提案无法指名 Session id,而准入前刷新候选集意味着模型选出的引用不会比它所依据的状态活得更久。

Approve。行内三条 P3 和一个问题,都不阻塞,我据此合并。

result: WorkHubSubmission,
): result is Extract<WorkHubSubmission, { kind: 'submitted' }> {
return result.kind === 'submitted' && !result.steered;
export function workHubSurfaceFailure(error: unknown): WorkHubSurfaceFailure {

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.

[P3] This reconstructs a typed distinction by matching English prose, in the PR whose thesis is that the distinction is typed.

The code already exists: WorkHubActionGateFailureCode is candidate_set_stale | candidate_unavailable | target_waiting_for_user | self_route | action_conflict. The coordinator's #act then discards it, collapsing everything to session_busy or operation_conflict plus a message, and this function greps the message to get it back.

Every branch matches correctly today, which is why this is P3 and not higher. What makes it worth fixing anyway is that nothing protects it: the producer and the consumer are in different packages, so rewording a Host error silently downgrades a user from specific guidance to delivery_failed, with no compile error and no test that spans the boundary. 'source or target is not in' already matches no message I can find in this diff, which is roughly what that drift looks like.

Carrying the gate's code through the operation failure instead of flattening it would remove the second representation entirely.

const candidates = await this.candidates();
if (candidates.candidateSetId !== input.candidateSetId) {
throw new WorkHubActionGateFailure(
'candidate_set_stale',

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.

[P3] Freshness is enforced set-wide, but the invariant that matters is per-target. candidateSetId digests every candidate's updatedAt and status, so a message in any unrelated Session rotates it and this rejects a delegation whose own target never changed.

The window is small — the routing policy is synchronous, so it is about one routingEvidence() round trip — which is why P3. But it is the multi-Session case that WorkHub exists for, and that is exactly when other Sessions are producing messages.

I do not think there is a one-line fix: candidateRef is derived from candidateSetId, so per-candidate validation would need refs bound to per-Session state instead of to the set. Worth deciding deliberately rather than inheriting.


#assertTarget(target: WorkHubCoordinationCandidate): void {
if (target.sessionId === WORKHUB_COORDINATION_SESSION_ID) {
throw new WorkHubActionGateFailure('self_route', 'WorkHub cannot delegate to itself');

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.

[P3] Unreachable. isCandidateSession already filters out the Coordination Session via isWorkHubCoordinationSessionTarget, and this only runs against a candidate found in that set, so self_route cannot fire. Either drop the branch or, if it is meant as a belt-and-braces assertion against a future candidate source, say so — as written it reads like a live guard.

}

/** @internal Transitional R2.4 regression harness; application code must use the Action Gate. */
export function createLegacyWorkHubControllerForTests(deps: {

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.

Not a finding — a question about the exit. The shared implementation now carries both behaviours behind deps.coordination, and production only ever takes one of them, so the other set of branches lives in production code kept reachable only by tests. That is the parallel path AGENTS.md asks us not to leave behind.

You have documented it as transitional, which is the right call for a slice boundary, so I am not treating it as a defect. What I would like recorded somewhere durable: which change deletes createLegacyWorkHubControllerForTests and the deps.coordination forks — Slice 5? If the answer lives only in this PR description it tends to become permanent.

@Astro-Han
Astro-Han merged commit 0e2074a into apache:mainAug 26, 2026
2 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@ARE404@Astro-Han