You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/10/10 10:53:12 UTC
(maka) branch main updated: fix(runtime-host): key the WorkHub candidate identity on the Session, not the set (#5862)
This is an automated email from the ASF dual-hosted git repository.
Astro-Han pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/main by this push:
new 34039510d9 fix(runtime-host): key the WorkHub candidate identity on the Session, not the set (#5862)
34039510d9 is described below
commit 34039510d9f107b15fc1399a5cb2ee3261fa09eb
Author: Adarsh Mishra <12...@users.noreply.github.com>
AuthorDate: Sat Oct 10 16:23:08 2026 +0530
fix(runtime-host): key the WorkHub candidate identity on the Session, not the set (#5862)
The digest behind every candidateRef included each eligible Session's `name`
and `updatedAt`, and it was taken in presentation order (`updatedAt`
descending). Any Session's automatic title commit or timestamp bump therefore
rotated every ref in the page, so a delegation into a target that had not
moved was rejected with `candidate_set_stale`. That is the #4785 flake, and
the same race a user loses by opening WorkHub and clicking delegate while an
unrelated Session is renamed.
`candidateSetId` now digests only the menu: Session id, workspace projection
and derived state, in id order. `candidateRef` keeps the full per-Session
identity, so a target that really did move still fails closed. The three
set-wide equality checks stay, so the tested contract that a stale
`candidateSetId` is rejected is untouched.
One expectation in the gate suite moves from `candidate_set_stale` to
`candidate_unavailable`: `rejects stale candidates before assignment` mutates
the target's own `lastMessageAt`, which is now visible only through its ref.
Measured at 6e21e611: gate, coordinator, protocol and tool-profile suites
86/86; the named flaky test 3/3; the new regression test passes with the
change and fails with only the gate file reverted, emitting
`candidate_set_stale`. Sunrioa's second reproduction, where the target's own
`updatedAt` catches up after its turn reports completed, is deliberately not
addressed here: `updatedAt` is still in the ref, so that window still
conflicts, and clearing it needs a call on whether a completing turn should
invalidate a delegation into it.
Generated-by: Qoder (AI assistant)
---
.../workhub-coordination-action-gate.test.ts | 28 +++++++++++++++++-
.../src/server/workhub-coordination-action-gate.ts | 34 +++++++++++++++-------
2 files changed, 51 insertions(+), 11 deletions(-)
diff --git a/packages/runtime-host/src/__tests__/workhub-coordination-action-gate.test.ts b/packages/runtime-host/src/__tests__/workhub-coordination-action-gate.test.ts
index 18703514f8..6308e780e2 100644
--- a/packages/runtime-host/src/__tests__/workhub-coordination-action-gate.test.ts
+++ b/packages/runtime-host/src/__tests__/workhub-coordination-action-gate.test.ts
@@ -230,7 +230,8 @@ describe('WorkHub Coordination Action Gate', () => {
},
CONTEXT,
),
- (error) => error instanceof WorkHubActionGateFailure && error.code === 'candidate_set_stale',
+ (error) =>
+ error instanceof WorkHubActionGateFailure && error.code === 'candidate_unavailable',
);
assert.equal(effects.assignments.length, 0);
@@ -266,6 +267,31 @@ describe('WorkHub Coordination Action Gate', () => {
assert.equal(effects.assignments.length, 1);
});
+ test('a title commit on another Session does not invalidate a delegation', async () => {
+ const effects = fakeEffects([session('payments'), session('design')]);
+ const gate = new WorkHubCoordinationActionGate(effects);
+ const offered = await gate.candidates();
+ const target = offered.candidates.find((candidate) => candidate.sessionId === 'payments')!;
+
+ // An unrelated Session's automatic title landing between reading candidates
+ // and delegating is the race the WorkHub flake loses (#4785).
+ effects.sessions[1] = session('design', { name: 'Design review', lastMessageAt: 99 });
+
+ const result = await gate.act(
+ {
+ actionId: 'unrelated-title',
+ userText: 'Continue payments',
+ candidateSetId: offered.candidateSetId,
+ proposal: { disposition: 'delegate_existing', candidateRef: target.candidateRef },
+ },
+ CONTEXT,
+ );
+
+ assert.equal(result.disposition, 'delegate_existing');
+ assert.equal(result.targetSessionId, 'payments');
+ assert.equal(effects.assignments.length, 1);
+ });
+
/**
* A stop proposal as Coordination policy produces it: opaque identities plus
* the active-delegation state it resolved against, never a display name.
diff --git a/packages/runtime-host/src/server/workhub-coordination-action-gate.ts b/packages/runtime-host/src/server/workhub-coordination-action-gate.ts
index 160d73be1d..234194f4dc 100644
--- a/packages/runtime-host/src/server/workhub-coordination-action-gate.ts
+++ b/packages/runtime-host/src/server/workhub-coordination-action-gate.ts
@@ -1120,19 +1120,25 @@ export function candidateSet(
.filter(isCandidateSession)
.sort((left, right) => updatedAt(right) - updatedAt(left) || left.id.localeCompare(right.id))
.slice(0, WORKHUB_COORDINATION_CANDIDATE_MAX_ITEMS);
+ // The set identity is the menu: which Sessions are offered, from which
+ // workspace, in which state. `name` and `updatedAt` stay out of it, and it is
+ // digested in id order rather than presentation order, because a title commit
+ // or a timestamp on one Session used to rotate the whole page and abort a
+ // delegation to a target that had not moved (#4785). Those fields are still in
+ // the per-Session ref, so a target that does move is still rejected.
const candidateSetId = digest(
- eligible.map((session) => ({
- id: session.id,
- name: session.name,
- workspace: workspaceProjection(session),
- status: session.status,
- updatedAt: updatedAt(session),
- })),
+ [...eligible]
+ .sort((left, right) => left.id.localeCompare(right.id))
+ .map((session) => ({
+ id: session.id,
+ workspace: workspaceProjection(session),
+ state: candidateState(session.status),
+ })),
);
return {
candidateSetId,
candidates: eligible.map((session) => ({
- candidateRef: candidateRef(candidateSetId, session.id),
+ candidateRef: candidateRef(session),
sessionId: session.id,
sessionName: session.name,
workspace: workspaceProjection(session),
@@ -1152,8 +1158,16 @@ function isCandidateSession(session: WorkHubActionGateSession): boolean {
);
}
-function candidateRef(candidateSetId: string, sessionId: string): string {
- return `whc_${hash(`${candidateSetId}\0${sessionId}`).slice(0, 48)}`;
+function candidateRef(session: WorkHubActionGateSession): string {
+ return `whc_${hash(
+ JSON.stringify({
+ id: session.id,
+ name: session.name,
+ workspace: workspaceProjection(session),
+ status: session.status,
+ updatedAt: updatedAt(session),
+ }),
+ ).slice(0, 48)}`;
}
function delegationAssignment(
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org