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