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/08/29 14:53:34 UTC
(maka) branch main updated: feat(workhub): project delegated execution status (#4115)
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 1ae45418b4 feat(workhub): project delegated execution status (#4115)
1ae45418b4 is described below
commit 1ae45418b4affa8c4ba94aa68e79756e8c4cfee3
Author: ChengBo Zhang <66...@users.noreply.github.com>
AuthorDate: Sat Aug 29 22:53:29 2026 +0800
feat(workhub): project delegated execution status (#4115)
* feat(workhub): project delegated execution status
Generated-by: Codex
* fix(workhub): follow delegated message ownership
Generated-by: Codex
* fix(workhub): project cancelled delegations
Generated-by: Codex
* fix(workhub): converge execution authority
Generated-by: Codex
---
apps/desktop/e2e/workhub-reconstruction.spec.ts | 6 +-
...runtime-host-session-execution-ipc-main.test.ts | 40 +
.../src/main/__tests__/workhub-controller.test.ts | 1718 ++++----------------
.../workhub-coordination-host-scope.test.ts | 31 +-
.../main/__tests__/workhub-session-port.test.ts | 389 ++---
.../main/__tests__/workhub-surface-flow.test.ts | 162 +-
apps/desktop/src/main/runtime-host-client.ts | 6 +
.../runtime-host-session-execution-ipc-main.ts | 9 +
apps/desktop/src/preload/bridge-contract.d.ts | 4 +
apps/desktop/src/preload/preload.ts | 3 +
apps/desktop/src/renderer/app-shell.tsx | 5 -
apps/desktop/src/renderer/workhub-controller.ts | 670 ++------
.../renderer/workhub-coordination-host-scope.ts | 23 +-
.../src/renderer/workhub-coordination-port.ts | 6 +-
apps/desktop/src/renderer/workhub-route-policy.ts | 62 +-
apps/desktop/src/renderer/workhub-session-port.ts | 219 +--
apps/desktop/src/renderer/workhub-surface.tsx | 69 +-
.../workhub-coordination-session-adr.md | 20 +-
.../src/__tests__/message-coordinator.test.ts | 130 +-
.../runtime-host/src/__tests__/protocol.test.ts | 41 +-
.../src/__tests__/root-turn-coordinator.test.ts | 2 +-
packages/runtime-host/src/protocol/index.ts | 4 +-
packages/runtime-host/src/protocol/message.ts | 79 +
packages/runtime-host/src/protocol/operations.ts | 1 +
.../runtime-host/src/server/message-coordinator.ts | 66 +
.../src/server/operation-dispatcher.ts | 1 +
26 files changed, 1305 insertions(+), 2461 deletions(-)
diff --git a/apps/desktop/e2e/workhub-reconstruction.spec.ts b/apps/desktop/e2e/workhub-reconstruction.spec.ts
index 938df54eb7..0b29525c03 100644
--- a/apps/desktop/e2e/workhub-reconstruction.spec.ts
+++ b/apps/desktop/e2e/workhub-reconstruction.spec.ts
@@ -19,7 +19,7 @@
import { COMPOSER_INPUT, ensureSidebarExpanded, expect, test } from './fixtures';
-test('WorkHub rebuilds Session conversation after navigating away and back', async ({
+test('WorkHub rebuilds delegated execution feedback after navigating away and back', async ({
window: page,
}) => {
const initialPrompt = '检查支付回调重复投递时的幂等性';
@@ -64,6 +64,10 @@ test('WorkHub rebuilds Session conversation after navigating away and back', asy
hasText: routedPrompt,
}),
).toBeVisible();
+ await expect(
+ page.locator('.workhub-projected-turn', { hasText: routedPrompt })
+ .locator('.workhub-submitted-state'),
+ ).toHaveText('进行中');
});
test('WorkHub defers destructive correction until linked delegation exists', async ({
diff --git a/apps/desktop/src/main/__tests__/runtime-host-session-execution-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-session-execution-ipc-main.test.ts
index b95338d162..8bdf462bed 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-session-execution-ipc-main.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-session-execution-ipc-main.test.ts
@@ -630,6 +630,45 @@ test('returns Host-owned cancellation proof to the renderer', async () => {
);
});
+test('returns Host-owned Message execution resolutions to the renderer', async () => {
+ const ipc = ipcHarness();
+ registerExecutionIpc(
+ {
+ client: executionClient({
+ queryMessageExecutions: async (input) => ({
+ resolutions: input.messageIds.map((messageId) => messageId === 'message-cancelled'
+ ? { messageId, state: 'cancelled' as const }
+ : {
+ messageId,
+ state: 'owned' as const,
+ turnId: 'successor-turn',
+ runId: 'successor-run',
+ }),
+ }),
+ }),
+ },
+ ipc,
+ );
+
+ assert.deepEqual(
+ await ipc.invoke('sessions:queryMessageExecutions', 'session-1', [
+ 'message-delegated',
+ 'message-cancelled',
+ ]),
+ {
+ resolutions: [
+ {
+ messageId: 'message-delegated',
+ state: 'owned',
+ turnId: 'successor-turn',
+ runId: 'successor-run',
+ },
+ { messageId: 'message-cancelled', state: 'cancelled' },
+ ],
+ },
+ );
+});
+
test('submits a slash Skill message and reports the Host Skill outcome', async () => {
const submits: unknown[] = [];
const ipc = ipcHarness();
@@ -1502,6 +1541,7 @@ function executionClient(overrides: Partial<ExecutionClient>): ExecutionClient {
interruptTurn: unavailable,
listSessionTurnLandmarks: unavailable,
listSessionTurns: unavailable,
+ queryMessageExecutions: unavailable,
queryMessages: unavailable,
queryTurnResume: unavailable,
readExecutionBoundary: unavailable,
diff --git a/apps/desktop/src/main/__tests__/workhub-controller.test.ts b/apps/desktop/src/main/__tests__/workhub-controller.test.ts
index ff438b0540..81e7538696 100644
--- a/apps/desktop/src/main/__tests__/workhub-controller.test.ts
+++ b/apps/desktop/src/main/__tests__/workhub-controller.test.ts
@@ -21,12 +21,11 @@ import assert from 'node:assert/strict';
import { existsSync, readFileSync } from 'node:fs';
import test from 'node:test';
import {
- createLegacyWorkHubControllerForTests as createWorkHubController,
createWorkHubController as createGatedWorkHubController,
- WorkHubSessionSubmitError,
WORKHUB_ROUTING_STRATEGY_ID,
type WorkHubSessionFacts,
type WorkHubSessionPort,
+ type WorkHubCoordinationTurn,
} from '../../renderer/workhub-controller.js';
const appShellUrl = [
@@ -71,25 +70,217 @@ function session(
};
}
-function port(sessions: WorkHubSessionFacts[]): WorkHubSessionPort {
+interface TestSessionPort extends WorkHubSessionPort {
+ create(input: { name: string }): Promise<WorkHubSessionFacts>;
+ submit(
+ target: { sessionId: string },
+ text: string,
+ turnId: string,
+ ): Promise<{ turnId: string; steered?: true }>;
+}
+
+function port(sessions: WorkHubSessionFacts[]): TestSessionPort {
let nextTurnId = 0;
return {
list: async () => sessions,
recentTurns: async () => [],
+ delegationFeedback: async (references) =>
+ references.map(({ delegationId }) => ({ delegationId, state: 'accepted' })),
routingEvidence: async () => [],
create: async () => {
throw new Error('create is not used by this read test');
},
- reserveTurnId: () => `reserved-turn-${++nextTurnId}`,
- submit: async () => {
- throw new Error('submit is not used by this read test');
- },
- reconcileSubmission: async () => ({ kind: 'unknown' }),
- stop: async () => {},
+ submit: async (_target, _text, turnId) => ({
+ turnId: turnId || `reserved-turn-${++nextTurnId}`,
+ }),
subscribe: () => () => {},
};
}
+function createWorkHubController({ sessions }: { sessions: TestSessionPort }) {
+ let candidateByRef = new Map<string, WorkHubSessionFacts>();
+ return createGatedWorkHubController({
+ sessions,
+ coordination: {
+ open: async () => ({ close: async () => undefined }),
+ record: async (input) => ({ turnId: input.turnId }),
+ candidates: async () => {
+ const candidates = (await sessions.list())
+ .filter((entry) => entry.kind === 'ordinary' && !entry.archived)
+ .map((entry) => ({
+ candidateRef: `candidate-${entry.target.sessionId}`,
+ sessionId: entry.target.sessionId,
+ sessionName: entry.sessionName,
+ workspace: {
+ target: { kind: 'host_path' as const, path: `/workspace/${entry.target.sessionId}` },
+ hostCwd: `/workspace/${entry.target.sessionId}`,
+ },
+ state: entry.state,
+ updatedAt: entry.updatedAt,
+ }));
+ const byId = new Map(
+ (await sessions.list()).map((entry) => [entry.target.sessionId, entry]),
+ );
+ candidateByRef = new Map(candidates.flatMap((candidate) => {
+ const entry = byId.get(candidate.sessionId);
+ return entry ? [[candidate.candidateRef, entry] as const] : [];
+ }));
+ return {
+ candidateSetId: `sha256:${'a'.repeat(64)}`,
+ candidates,
+ };
+ },
+ act: async (input) => {
+ if (input.proposal.disposition === 'answer_here') {
+ return {
+ disposition: 'answer_here',
+ coordinationTurnId: input.actionId,
+ };
+ }
+ if (input.proposal.disposition === 'clarify') {
+ return {
+ disposition: 'clarify',
+ coordinationTurnId: input.actionId,
+ };
+ }
+ if (input.proposal.disposition === 'create_new') {
+ const created = await sessions.create({ name: input.proposal.title });
+ const admitted = await sessions.submit(created.target, input.userText, input.actionId);
+ return {
+ disposition: 'create_new',
+ targetSessionId: created.target.sessionId,
+ targetTurnId: admitted.turnId,
+ ...(admitted.steered ? { steered: true as const } : {}),
+ };
+ }
+ const target = candidateByRef.get(input.proposal.candidateRef);
+ if (!target) throw new Error('unknown test candidate');
+ const admitted = await sessions.submit(target.target, input.userText, input.actionId);
+ return {
+ disposition: 'delegate_existing',
+ targetSessionId: target.target.sessionId,
+ targetTurnId: admitted.turnId,
+ ...(admitted.steered ? { steered: true as const } : {}),
+ };
+ },
+ },
+ });
+}
+
+function coordinationAssignmentTurn(): WorkHubCoordinationTurn {
+ return {
+ messageId: 'assignment-1',
+ turnId: 'action-1',
+ text: 'Continue payments',
+ state: 'completed',
+ assignment: {
+ delegationId: 'delegation-1',
+ targetSessionId: 'payment',
+ targetSessionName: 'Payments',
+ targetMessageId: 'payment-message',
+ targetTurnId: 'payment-turn',
+ feedbackState: 'accepted',
+ },
+ updatedAt: 10,
+ };
+}
+
+test('conversation acknowledges a durable assignment before projecting target execution', async () => {
+ const sessions = port([session('payment')]);
+ let onSessionChanged: (() => void) | undefined;
+ let feedbackState: 'completed' | 'waiting_for_user' = 'completed';
+ sessions.subscribe = (handler) => {
+ onSessionChanged = handler;
+ return () => {
+ onSessionChanged = undefined;
+ };
+ };
+ sessions.delegationFeedback = async (references) =>
+ references.map(({ delegationId }) => ({ delegationId, state: feedbackState }));
+ const assignment = coordinationAssignmentTurn();
+ const snapshots: string[] = [];
+ const controller = createGatedWorkHubController({
+ sessions,
+ coordination: {
+ open: async (handler) => {
+ handler([assignment]);
+ return { close: async () => undefined };
+ },
+ record: async (input) => ({ turnId: input.turnId }),
+ candidates: async () => ({ candidateSetId: `sha256:${'a'.repeat(64)}`, candidates: [] }),
+ act: async () => ({ disposition: 'answer_here', coordinationTurnId: 'unused' }),
+ },
+ });
+
+ const handle = await controller.openConversation((turns) => {
+ snapshots.push(turns[0]?.assignment?.feedbackState ?? 'missing');
+ }, () => undefined);
+ await Promise.resolve();
+
+ assert.deepEqual(snapshots.slice(0, 2), ['accepted', 'completed']);
+
+ feedbackState = 'waiting_for_user';
+ onSessionChanged?.();
+ await Promise.resolve();
+ await Promise.resolve();
+ assert.equal(snapshots.at(-1), 'waiting_for_user');
+
+ await handle.close();
+});
+
+test('conversation feedback never lets an older refresh overwrite newer target state', async () => {
+ const sessions = port([session('payment')]);
+ let onSessionChanged: (() => void) | undefined;
+ sessions.subscribe = (handler) => {
+ onSessionChanged = handler;
+ return () => undefined;
+ };
+ type Feedback = Awaited<ReturnType<WorkHubSessionPort['delegationFeedback']>>;
+ const pending: Array<{
+ references: Parameters<WorkHubSessionPort['delegationFeedback']>[0];
+ resolve(feedback: Feedback): void;
+ }> = [];
+ sessions.delegationFeedback = (references) =>
+ new Promise((resolve) => pending.push({ references, resolve }));
+ const snapshots: string[] = [];
+ const controller = createGatedWorkHubController({
+ sessions,
+ coordination: {
+ open: async (handler) => {
+ handler([coordinationAssignmentTurn()]);
+ return { close: async () => undefined };
+ },
+ record: async (input) => ({ turnId: input.turnId }),
+ candidates: async () => ({ candidateSetId: `sha256:${'b'.repeat(64)}`, candidates: [] }),
+ act: async () => ({ disposition: 'answer_here', coordinationTurnId: 'unused' }),
+ },
+ });
+
+ const handle = await controller.openConversation((turns) => {
+ snapshots.push(turns[0]?.assignment?.feedbackState ?? 'missing');
+ }, () => undefined);
+ assert.equal(pending.length, 1);
+ onSessionChanged?.();
+ assert.equal(pending.length, 2);
+
+ pending[1]!.resolve(pending[1]!.references.map(({ delegationId }) => ({
+ delegationId,
+ state: 'completed',
+ })));
+ await Promise.resolve();
+ await Promise.resolve();
+ pending[0]!.resolve(pending[0]!.references.map(({ delegationId }) => ({
+ delegationId,
+ state: 'failed',
+ })));
+ await Promise.resolve();
+ await Promise.resolve();
+
+ assert.equal(snapshots.at(-1), 'completed');
+ assert.equal(snapshots.includes('failed'), false);
+ await handle.close();
+});
+
test('read exposes existing ordinary Sessions as factual Work summaries', async () => {
const controller = createWorkHubController({
sessions: port([
@@ -993,1466 +1184,145 @@ test('English core evidence requires a distinctive word or multiple whole-word m
assert.deepEqual(submitted, ['parser']);
});
-test('route correction stops the wrong Session and teaches a similar request', async () => {
- const submitted: string[] = [];
- const stopped: string[] = [];
+test('waiting Session rejects a second root request without calling submit', async () => {
+ let submitted = false;
const sessions = port([
- session('login', { sessionName: '登录稳定性' }),
- session('payment', { sessionName: '支付稳定性' }),
+ session('login', {
+ sessionName: '排查令牌过期重复登录问题',
+ state: 'waiting_for_user',
+ }),
]);
- sessions.submit = async (target) => {
- submitted.push(target.sessionId);
- return { turnId: `turn-${submitted.length}` };
- };
- sessions.stop = async (target) => {
- stopped.push(target.sessionId);
+ sessions.submit = async () => {
+ submitted = true;
+ return { turnId: 'unexpected' };
};
const controller = createWorkHubController({ sessions });
- await controller.submit({
- requestId: 'request-focus-payment',
- text: '先看支付',
- explicitTarget: { sessionId: 'payment' },
- });
-
- const wrong = await controller.submit({
- requestId: 'request-alias',
- text: '继续白鹭点,列出验收项。',
- });
- assert.deepEqual(wrong.kind === 'submitted' ? wrong.target : undefined, {
- sessionId: 'payment',
- });
- const corrected = await controller.submit({
- requestId: 'request-alias',
- text: '继续白鹭点,列出验收项。',
- explicitTarget: { sessionId: 'login' },
- correction: { from: { sessionId: 'payment' }, turnId: 'turn-2' },
- });
- assert.equal(corrected.kind, 'submitted');
- assert.equal(corrected.kind === 'submitted' ? corrected.evidence : undefined, 'route_correction');
- assert.deepEqual(corrected.kind === 'submitted' ? corrected.correctedFrom : undefined, {
- sessionId: 'payment',
+ const result = await controller.submit({
+ requestId: 'request-waiting',
+ text: '排查令牌过期重复登录问题:补充一条等待状态下的新请求。',
});
- const learned = await controller.submit({
- requestId: 'request-alias-similar',
- text: '继续白鹭点,补充失败判定。',
- });
- assert.deepEqual(learned.kind === 'submitted' ? learned.target : undefined, {
- sessionId: 'login',
+ assert.deepEqual(result, {
+ kind: 'waiting',
+ strategyId: WORKHUB_ROUTING_STRATEGY_ID,
+ requestId: 'request-waiting',
+ text: '排查令牌过期重复登录问题:补充一条等待状态下的新请求。',
+ target: { sessionId: 'login' },
});
- assert.equal(learned.kind === 'submitted' ? learned.evidence : undefined, 'route_correction');
- assert.deepEqual(stopped, ['payment']);
- assert.deepEqual(submitted, ['payment', 'payment', 'login', 'login']);
+ assert.equal(submitted, false);
});
-test('route correction never stops a root Turn that WorkHub only steered into', async () => {
- const stopped: string[] = [];
- let submissionCount = 0;
+test('submit returns to the previous focused Session', async () => {
+ const submitted: string[] = [];
const sessions = port([
- session('login', { sessionName: '登录稳定性' }),
- session('payment', { sessionName: '支付稳定性', state: 'running' }),
+ session('login', { sessionName: '登录刷新令牌' }),
+ session('payment', { sessionName: '支付回调幂等性' }),
]);
- sessions.submit = async () => {
- submissionCount += 1;
- return submissionCount === 1
- ? { turnId: 'turn-existing', steered: true }
- : { turnId: 'turn-login' };
- };
- sessions.stop = async (target) => {
- stopped.push(target.sessionId);
+ sessions.submit = async (target) => {
+ submitted.push(target.sessionId);
+ return { turnId: `turn-${submitted.length}` };
};
const controller = createWorkHubController({ sessions });
-
- const wrong = await controller.submit({
- requestId: 'request-steered',
- text: '继续补充支付验收项',
+ await controller.submit({
+ requestId: 'request-login',
+ text: '先看登录',
+ explicitTarget: { sessionId: 'login' },
+ });
+ await controller.submit({
+ requestId: 'request-payment',
+ text: '再看支付',
explicitTarget: { sessionId: 'payment' },
});
- assert.equal(wrong.kind === 'submitted' ? wrong.steered : undefined, true);
- const correction = {
- from: { sessionId: 'payment' },
- turnId: 'turn-existing',
- steered: true as const,
- };
- const corrected = await controller.submit({
- requestId: 'request-steered',
- text: '不是支付,应该补充登录验收项',
- explicitTarget: { sessionId: 'login' },
- correction,
+ const result = await controller.submit({
+ requestId: 'request-previous',
+ text: '回到上一个工作',
});
- assert.equal(corrected.kind, 'submitted');
- assert.deepEqual(stopped, []);
+ assert.equal(result.kind, 'submitted');
+ assert.deepEqual(result.kind === 'submitted' ? result.target : undefined, { sessionId: 'login' });
+ assert.deepEqual(submitted, ['login', 'payment', 'login']);
});
-test('first natural-language correction reroutes and stops the wrong WorkHub-owned Turn', async () => {
+test('submit lets strong foreign core evidence override a vague focus word', async () => {
const submitted: string[] = [];
- const stopped: Array<[string, string]> = [];
const sessions = port([
session('login', {
sessionName: '登录稳定性',
- latestResult: '刷新令牌过期导致重复登录',
- updatedAt: 20,
+ latestResult: '处理刷新令牌过期导致的重复登录',
}),
session('payment', {
sessionName: '支付稳定性',
- latestResult: '支付回调重复投递',
- updatedAt: 30,
+ latestResult: '处理支付回调重复投递',
}),
]);
sessions.submit = async (target) => {
submitted.push(target.sessionId);
return { turnId: `turn-${submitted.length}` };
};
- sessions.stop = async (target, turnId) => {
- stopped.push([target.sessionId, turnId]);
- };
const controller = createWorkHubController({ sessions });
- await controller.read();
await controller.submit({
- requestId: 'request-wrong-payment',
- text: '继续这个工作,补充验收项',
+ requestId: 'request-payment-focus',
+ text: '先看支付',
+ explicitTarget: { sessionId: 'payment' },
});
- const corrected = await controller.submit({
- requestId: 'request-natural-correction',
- text: '不是这个,换成登录那个,补充刷新令牌失败判定',
+ const result = await controller.submit({
+ requestId: 'request-foreign-core',
+ text: '继续处理刷新令牌过期',
});
- assert.equal(corrected.kind, 'submitted');
- assert.deepEqual(corrected.kind === 'submitted' ? corrected.target : undefined, {
- sessionId: 'login',
- });
- assert.equal(
- corrected.kind === 'submitted' ? corrected.evidence : undefined,
- 'route_correction',
- );
- assert.deepEqual(corrected.kind === 'submitted' ? corrected.correctedFrom : undefined, {
- sessionId: 'payment',
- });
- assert.deepEqual(stopped, [['payment', 'turn-1']]);
+ assert.equal(result.kind, 'submitted');
+ assert.deepEqual(result.kind === 'submitted' ? result.target : undefined, { sessionId: 'login' });
assert.deepEqual(submitted, ['payment', 'login']);
});
-test('content-level replacement instructions stay inside the focused Session', async () => {
- const submitted: string[] = [];
- const stopped: string[] = [];
- const sessions = port([
- session('login', { sessionName: '登录稳定性', updatedAt: 20 }),
- session('payment', { sessionName: '支付稳定性', updatedAt: 30 }),
- session('database', {
- sessionName: '数据库迁移',
- latestResult: 'Postgres schema migration',
- updatedAt: 10,
- }),
- ]);
- sessions.submit = async (target) => {
- submitted.push(target.sessionId);
- return { turnId: `turn-${submitted.length}` };
- };
- sessions.stop = async (target) => {
- stopped.push(target.sessionId);
+test('submit keeps unmatched non-executable conversation in WorkHub', async () => {
+ let created = false;
+ const actions: unknown[] = [];
+ const sessions = port([]);
+ sessions.create = async () => {
+ created = true;
+ return session('unexpected');
};
- const controller = createWorkHubController({ sessions });
- await controller.read();
- await controller.submit({
- requestId: 'request-before-content-change',
- text: '继续这个工作',
+ const controller = createGatedWorkHubController({
+ sessions,
+ coordination: {
+ open: async () => ({ close: async () => undefined }),
+ record: async (input) => ({ turnId: input.turnId }),
+ candidates: async () => ({
+ candidateSetId: `sha256:${'a'.repeat(64)}`,
+ candidates: [],
+ }),
+ act: async (input) => {
+ actions.push(input);
+ return {
+ disposition: 'answer_here',
+ coordinationTurnId: 'coordination-turn',
+ };
+ },
+ },
});
const result = await controller.submit({
- requestId: 'request-content-change',
- text: '继续这个工作,Redis 配置不对,改成 Postgres',
+ requestId: 'request-discussion',
+ text: '你觉得统一入口最重要的价值是什么?',
});
- assert.deepEqual(result.kind === 'submitted' ? result.target : undefined, {
- sessionId: 'payment',
+ assert.deepEqual(result, {
+ kind: 'discussion',
+ strategyId: WORKHUB_ROUTING_STRATEGY_ID,
+ requestId: 'request-discussion',
+ text: '你觉得统一入口最重要的价值是什么?',
});
- assert.equal(result.kind === 'submitted' ? result.evidence : undefined, 'recent_focus');
- assert.deepEqual(stopped, []);
-});
-
-test('steering the same WorkHub-owned root preserves ownership for a later correction', async () => {
- const submitted: string[] = [];
- const stopped: Array<[string, string]> = [];
- const sessions = port([
- session('login', { sessionName: '登录稳定性', updatedAt: 20 }),
- session('payment', { sessionName: '支付稳定性', updatedAt: 30 }),
- ]);
- let paymentSubmissions = 0;
- sessions.submit = async (target) => {
- submitted.push(target.sessionId);
- if (target.sessionId === 'payment') {
- paymentSubmissions += 1;
- return paymentSubmissions === 1
- ? { turnId: 'turn-payment-root' }
- : { turnId: 'turn-payment-steering-command', steered: true };
- }
- return { turnId: 'turn-login-' + submitted.length };
- };
- sessions.stop = async (target, turnId) => {
- stopped.push([target.sessionId, turnId]);
- };
- const controller = createWorkHubController({ sessions });
- await controller.read();
- await controller.submit({
- requestId: 'request-owned-root',
- text: '继续这个工作',
- });
- await controller.submit({
- requestId: 'request-other-owned-root',
- text: '先检查登录稳定性',
- explicitTarget: { sessionId: 'login' },
- });
- await controller.submit({
- requestId: 'request-steer-owned-root',
- text: '继续这个工作,补充测试点',
- explicitTarget: { sessionId: 'payment' },
- });
-
- const corrected = await controller.submit({
- requestId: 'request-correct-owned-root',
- text: '不是这个工作,换成登录稳定性',
- });
-
- assert.deepEqual(corrected.kind === 'submitted' ? corrected.target : undefined, {
- sessionId: 'login',
- });
- assert.deepEqual(stopped, [['payment', 'turn-payment-root']]);
-});
-
-test('a late root completion cannot overwrite newer ownership after remount', async () => {
- const stopped: Array<[string, string]> = [];
- const sessions = port([
- session('login', { sessionName: '登录稳定性', updatedAt: 20 }),
- session('payment', { sessionName: '支付稳定性', updatedAt: 30 }),
- ]);
- let signalOlderStarted!: () => void;
- const olderStarted = new Promise<void>((resolve) => {
- signalOlderStarted = resolve;
- });
- let finishOlder!: (value: { turnId: string }) => void;
- const olderTurn = new Promise<{ turnId: string }>((resolve) => {
- finishOlder = resolve;
- });
- let paymentSubmissions = 0;
- sessions.submit = async (target) => {
- if (target.sessionId === 'payment') {
- paymentSubmissions += 1;
- if (paymentSubmissions === 1) {
- signalOlderStarted();
- return olderTurn;
- }
- return { turnId: 'turn-payment-new' };
- }
- return { turnId: 'turn-login' };
- };
- sessions.stop = async (target, turnId) => {
- stopped.push([target.sessionId, turnId]);
- };
- const controller = createWorkHubController({ sessions });
-
- const olderSubmission = controller.submit({
- requestId: 'request-payment-old',
- text: '先继续支付稳定性',
- explicitTarget: { sessionId: 'payment' },
- });
- await olderStarted;
- controller.resetVisitContext();
- await controller.submit({
- requestId: 'request-payment-new',
- text: '重新继续支付稳定性',
- explicitTarget: { sessionId: 'payment' },
- });
- finishOlder({ turnId: 'turn-payment-old' });
- await olderSubmission;
-
- const corrected = await controller.submit({
- requestId: 'request-correct-after-late-root',
- text: '不是这个工作,换成登录稳定性',
- });
-
- assert.deepEqual(corrected.kind === 'submitted' ? corrected.target : undefined, {
- sessionId: 'login',
- });
- assert.deepEqual(stopped, [['payment', 'turn-payment-new']]);
-});
-
-test('a correction after remount stops a root whose admission is still pending', async () => {
- const stopped: Array<[string, string]> = [];
- const sessions = port([
- session('login', { sessionName: '登录稳定性', updatedAt: 20 }),
- session('payment', { sessionName: '支付稳定性', updatedAt: 30 }),
- ]);
- let signalPaymentStarted!: () => void;
- const paymentStarted = new Promise<void>((resolve) => {
- signalPaymentStarted = resolve;
- });
- let finishPayment!: (value: { turnId: string }) => void;
- const paymentTurn = new Promise<{ turnId: string }>((resolve) => {
- finishPayment = resolve;
- });
- let nextReservedTurnId = 0;
- sessions.reserveTurnId = () => `turn-reserved-${++nextReservedTurnId}`;
- sessions.submit = async (target, _text, turnId) => {
- if (target.sessionId === 'payment') {
- assert.equal(turnId, 'turn-reserved-1');
- signalPaymentStarted();
- return paymentTurn;
- }
- return { turnId };
- };
- sessions.stop = async (target, turnId) => {
- stopped.push([target.sessionId, turnId]);
- };
- const controller = createWorkHubController({ sessions });
-
- const pendingSubmission = controller.submit({
- requestId: 'request-payment-pending',
- text: '继续支付稳定性',
- explicitTarget: { sessionId: 'payment' },
- });
- await paymentStarted;
- controller.resetVisitContext();
- await controller.read({ focus: { sessionId: 'payment' } });
-
- const corrected = await controller.submit({
- requestId: 'request-correct-pending-root',
- text: '不是这个工作,换成登录稳定性',
- });
-
- assert.deepEqual(corrected.kind === 'submitted' ? corrected.target : undefined, {
- sessionId: 'login',
- });
- assert.deepEqual(stopped, [['payment', 'turn-reserved-1']]);
- finishPayment({ turnId: 'turn-payment-host-rebound' });
- await pendingSubmission;
- assert.deepEqual(stopped, [
- ['payment', 'turn-reserved-1'],
- ['payment', 'turn-payment-host-rebound'],
- ]);
-});
-
-test('a correction stops an uncertain root under the Turn identity the Host minted', async () => {
- const stopped: Array<[string, string]> = [];
- const sessions = port([
- session('login', { sessionName: '登录稳定性', updatedAt: 20 }),
- session('payment', { sessionName: '支付稳定性', updatedAt: 30 }),
- ]);
- sessions.reserveTurnId = () => 'reserved-payment';
- // The Host admitted the Message and opened a Turn under its own identity,
- // then the answer was lost. Only the transcript can tie the reserved Message
- // identity back to that Turn.
- sessions.submit = async (target, _text, turnId) => {
- if (target.sessionId !== 'payment') return { turnId };
- throw new WorkHubSessionSubmitError('delivery outcome is unknown', 'unknown');
- };
- // The transcript has not caught up at delivery time, so the candidate stays
- // uncertain; the correction is the next chance to resolve it.
- let transcriptCaughtUp = false;
- sessions.reconcileSubmission = async (_target, reservedTurnId) => {
- if (reservedTurnId !== 'reserved-payment' || !transcriptCaughtUp) {
- transcriptCaughtUp = true;
- return { kind: 'unknown' };
- }
- return { kind: 'root', turnId: 'turn-payment-host' };
- };
- sessions.stop = async (target, turnId) => {
- stopped.push([target.sessionId, turnId]);
- };
- const controller = createWorkHubController({ sessions });
-
- await assert.rejects(controller.submit({
- requestId: 'request-payment-uncertain',
- text: '继续支付稳定性',
- explicitTarget: { sessionId: 'payment' },
- }));
-
- const corrected = await controller.submit({
- requestId: 'request-correct-uncertain-root',
- text: '不是这个工作,换成登录稳定性',
- });
-
- assert.deepEqual(corrected.kind === 'submitted' ? corrected.target : undefined, {
- sessionId: 'login',
- });
- assert.deepEqual(stopped, [['payment', 'turn-payment-host']]);
-});
-
-test('a correction retries Stop when the same reserved root is admitted before Stop settles', async () => {
- const stopped: Array<[string, string]> = [];
- const sessions = port([
- session('login', { sessionName: '登录稳定性', updatedAt: 20 }),
- session('payment', { sessionName: '支付稳定性', updatedAt: 30 }),
- ]);
- let signalPaymentStarted!: () => void;
- const paymentStarted = new Promise<void>((resolve) => {
- signalPaymentStarted = resolve;
- });
- let finishPayment!: (value: { turnId: string }) => void;
- const paymentTurn = new Promise<{ turnId: string }>((resolve) => {
- finishPayment = resolve;
- });
- sessions.reserveTurnId = () => 'turn-reserved-1';
- sessions.submit = async (target, _text, turnId) => {
- if (target.sessionId === 'payment') {
- assert.equal(turnId, 'turn-reserved-1');
- signalPaymentStarted();
- return paymentTurn;
- }
- return { turnId };
- };
- let signalFirstStopStarted!: () => void;
- const firstStopStarted = new Promise<void>((resolve) => {
- signalFirstStopStarted = resolve;
- });
- let finishFirstStop!: () => void;
- const firstStop = new Promise<void>((resolve) => {
- finishFirstStop = resolve;
- });
- sessions.stop = async (target, turnId) => {
- stopped.push([target.sessionId, turnId]);
- if (stopped.length === 1) {
- signalFirstStopStarted();
- await firstStop;
- }
- };
- const controller = createWorkHubController({ sessions });
-
- const pendingSubmission = controller.submit({
- requestId: 'request-payment-same-id-pending',
- text: '继续支付稳定性',
- explicitTarget: { sessionId: 'payment' },
- });
- await paymentStarted;
- controller.resetVisitContext();
- await controller.read({ focus: { sessionId: 'payment' } });
-
- const correction = controller.submit({
- requestId: 'request-correct-same-id-pending-root',
- text: '不是这个工作,换成登录稳定性',
- });
- await firstStopStarted;
- assert.deepEqual(stopped, [['payment', 'turn-reserved-1']]);
-
- finishPayment({ turnId: 'turn-reserved-1' });
- finishFirstStop();
- await Promise.all([pendingSubmission, correction]);
-
- assert.deepEqual(stopped, [
- ['payment', 'turn-reserved-1'],
- ['payment', 'turn-reserved-1'],
- ]);
-});
-
-test('a stopped ownership tombstone blocks an older root completion', async () => {
- const stopped: Array<[string, string]> = [];
- const sessions = port([
- session('login', { sessionName: '登录稳定性', updatedAt: 20 }),
- session('payment', { sessionName: '支付稳定性', updatedAt: 30 }),
- ]);
- let signalStaleStarted!: () => void;
- const staleStarted = new Promise<void>((resolve) => {
- signalStaleStarted = resolve;
- });
- let finishStale!: (value: { turnId: string }) => void;
- const staleTurn = new Promise<{ turnId: string }>((resolve) => {
- finishStale = resolve;
- });
- let paymentSubmissions = 0;
- sessions.submit = async (target) => {
- if (target.sessionId !== 'payment') return { turnId: 'turn-login' };
- paymentSubmissions += 1;
- if (paymentSubmissions === 1) return { turnId: 'turn-payment-root' };
- signalStaleStarted();
- return staleTurn;
- };
- sessions.stop = async (target, turnId) => {
- stopped.push([target.sessionId, turnId]);
- };
- const controller = createWorkHubController({ sessions });
- await controller.submit({
- requestId: 'request-payment-owned',
- text: '继续支付稳定性',
- explicitTarget: { sessionId: 'payment' },
- });
- const staleSubmission = controller.submit({
- requestId: 'request-payment-stale',
- text: '再继续支付稳定性',
- explicitTarget: { sessionId: 'payment' },
- });
- await staleStarted;
-
- await controller.submit({
- requestId: 'request-stop-before-stale-finishes',
- text: '不是这个工作,换成登录稳定性',
- });
- finishStale({ turnId: 'turn-payment-stale' });
- await staleSubmission;
- controller.resetVisitContext();
- await controller.read({ focus: { sessionId: 'payment' } });
- await controller.submit({
- requestId: 'request-correct-after-stale-finishes',
- text: '不是这个工作,换成登录稳定性',
- });
-
- assert.deepEqual(stopped, [
- ['payment', 'turn-payment-root'],
- ['payment', 'reserved-turn-2'],
- ['payment', 'turn-payment-stale'],
- ]);
-});
-
-test('tombstone retention never evicts live ownership for another Session', async () => {
- const stopped: Array<[string, string]> = [];
- const fillers = Array.from({ length: 32 }, (_, index) =>
- session(`filler-${index}`, { sessionName: `填充工作 ${index}` }));
- const sessions = port([
- session('long-running', { sessionName: '长期工作', updatedAt: 100 }),
- session('sink', { sessionName: '收件箱工作', updatedAt: 90 }),
- ...fillers,
- ]);
- sessions.submit = async (target, _text, turnId) => target.sessionId === 'sink'
- ? { turnId, steered: true }
- : { turnId };
- sessions.stop = async (target, turnId) => {
- stopped.push([target.sessionId, turnId]);
- };
- const controller = createWorkHubController({ sessions });
-
- const live = await controller.submit({
- requestId: 'request-live-root',
- text: '开始长期工作',
- explicitTarget: { sessionId: 'long-running' },
- });
- assert.equal(live.kind, 'submitted');
-
- for (const [index, filler] of fillers.entries()) {
- const owned = await controller.submit({
- requestId: `request-filler-${index}`,
- text: `开始填充工作 ${index}`,
- explicitTarget: filler.target,
- });
- assert.equal(owned.kind, 'submitted');
- if (owned.kind !== 'submitted') continue;
- await controller.submit({
- requestId: `request-stop-filler-${index}`,
- text: `填充工作 ${index} 路由错了`,
- explicitTarget: { sessionId: 'sink' },
- correction: {
- from: filler.target,
- turnId: owned.turnId,
- },
- });
- }
-
- stopped.length = 0;
- controller.resetVisitContext();
- await controller.read({ focus: { sessionId: 'long-running' } });
- await controller.submit({
- requestId: 'request-correct-live-root',
- text: '不是这个工作,换成收件箱工作',
- });
-
- assert.equal(stopped.length, 1);
- assert.equal(stopped[0]?.[0], 'long-running');
- assert.equal(stopped[0]?.[1], live.kind === 'submitted' ? live.turnId : undefined);
-});
-
-test('correction barrier rejects a new root while Stop is pending', async () => {
- const stopped: Array<[string, string]> = [];
- const sessions = port([
- session('login', { sessionName: '登录稳定性', updatedAt: 20 }),
- session('payment', { sessionName: '支付稳定性', updatedAt: 30 }),
- ]);
- let signalStopStarted!: () => void;
- const stopStarted = new Promise<void>((resolve) => {
- signalStopStarted = resolve;
- });
- let finishFirstStop!: () => void;
- const firstStop = new Promise<void>((resolve) => {
- finishFirstStop = resolve;
- });
- let stopCalls = 0;
- sessions.submit = async (_target, _text, turnId) => ({ turnId });
- sessions.stop = async (target, turnId) => {
- stopped.push([target.sessionId, turnId]);
- stopCalls += 1;
- if (stopCalls === 1) {
- signalStopStarted();
- await firstStop;
- }
- };
- const controller = createWorkHubController({ sessions });
- const original = await controller.submit({
- requestId: 'request-original-payment',
- text: '继续支付稳定性',
- explicitTarget: { sessionId: 'payment' },
- });
- assert.equal(original.kind, 'submitted');
-
- const correction = controller.submit({
- requestId: 'request-correct-original-payment',
- text: '不是这个工作,换成登录稳定性',
- });
- await stopStarted;
- await assert.rejects(controller.submit({
- requestId: 'request-overlapping-payment',
- text: '重新处理支付稳定性',
- explicitTarget: { sessionId: 'payment' },
- }), /still reconciling/u);
- finishFirstStop();
- await correction;
-
- const newer = await controller.submit({
- requestId: 'request-newer-payment',
- text: '重新处理支付稳定性',
- explicitTarget: { sessionId: 'payment' },
- });
- assert.equal(newer.kind, 'submitted');
-
- controller.resetVisitContext();
- await controller.read({ focus: { sessionId: 'payment' } });
- await controller.submit({
- requestId: 'request-correct-newer-payment',
- text: '不是这个工作,换成登录稳定性',
- });
-
- assert.deepEqual(stopped, [
- ['payment', original.kind === 'submitted' ? original.turnId : ''],
- ['payment', newer.kind === 'submitted' ? newer.turnId : ''],
- ]);
-});
-
-test('a partially failed correction records only successful Stops and keeps failures reachable', async () => {
- const stopAttempts: Array<[string, string]> = [];
- const sessions = port([
- session('login', { sessionName: '登录稳定性', updatedAt: 20 }),
- session('payment', { sessionName: '支付稳定性', updatedAt: 30 }),
- ]);
- let finishPending!: (value: { turnId: string }) => void;
- const pendingTurn = new Promise<{ turnId: string }>((resolve) => {
- finishPending = resolve;
- });
- let paymentSubmissions = 0;
- sessions.reserveTurnId = (() => {
- let next = 0;
- return () => `reserved-${++next}`;
- })();
- sessions.submit = async (target, _text, turnId) => {
- if (target.sessionId !== 'payment') return { turnId };
- paymentSubmissions += 1;
- return paymentSubmissions === 1 ? { turnId: 'payment-root' } : pendingTurn;
- };
- let failPaymentRoot = true;
- sessions.stop = async (target, turnId) => {
- stopAttempts.push([target.sessionId, turnId]);
- if (turnId === 'payment-root' && failPaymentRoot) {
- failPaymentRoot = false;
- throw new Error('Host rejected the first Stop');
- }
- };
- const controller = createWorkHubController({ sessions });
- const confirmed = await controller.submit({
- requestId: 'confirmed-payment-root',
- text: '继续支付稳定性',
- explicitTarget: { sessionId: 'payment' },
- });
- assert.equal(confirmed.kind, 'submitted');
- const pending = controller.submit({
- requestId: 'pending-payment-root',
- text: '再次处理支付稳定性',
- explicitTarget: { sessionId: 'payment' },
- });
- await Promise.resolve();
- await Promise.resolve();
-
- await assert.rejects(controller.submit({
- requestId: 'partially-failed-correction',
- text: '不是支付,改成登录',
- explicitTarget: { sessionId: 'login' },
- correction: {
- from: { sessionId: 'payment' },
- turnId: 'payment-root',
- },
- }), /Host rejected the first Stop/u);
-
- assert.equal(stopAttempts.some(([, turnId]) => turnId === 'reserved-2'), true);
- finishPending({ turnId: 'payment-host-rebound' });
- await pending;
- assert.equal(stopAttempts.some(([, turnId]) => turnId === 'payment-host-rebound'), true);
-
- await controller.submit({
- requestId: 'retry-failed-correction',
- text: '不是支付,改成登录',
- explicitTarget: { sessionId: 'login' },
- correction: {
- from: { sessionId: 'payment' },
- turnId: 'payment-root',
- },
- });
- assert.equal(
- stopAttempts.filter(([, turnId]) => turnId === 'payment-root').length,
- 2,
- );
-});
-
-test('the 33rd unresolved root is back-pressured before Host admission', async () => {
- const facts = Array.from({ length: 33 }, (_, index) =>
- session(`work-${index}`, { runningTurnIds: [] }));
- const sessions = port(facts);
- const finishes = new Map<string, (value: { turnId: string }) => void>();
- let admitted = 0;
- let signalThirtyTwo!: () => void;
- const thirtyTwoAdmitted = new Promise<void>((resolve) => {
- signalThirtyTwo = resolve;
- });
- sessions.submit = async (target, _text, turnId) => {
- admitted += 1;
- if (admitted === 32) signalThirtyTwo();
- return await new Promise<{ turnId: string }>((resolve) => {
- finishes.set(target.sessionId, resolve);
- });
- };
- const controller = createWorkHubController({ sessions });
- const inFlight = facts.slice(0, 32).map((fact, index) => controller.submit({
- requestId: `root-${index}`,
- text: `开始工作 ${index}`,
- explicitTarget: fact.target,
- }));
- await thirtyTwoAdmitted;
-
- await assert.rejects(controller.submit({
- requestId: 'root-33',
- text: '开始工作 33',
- explicitTarget: facts[32]!.target,
- }), /too many unresolved root submissions/u);
- assert.equal(admitted, 32);
-
- finishes.get('work-0')?.({ turnId: 'settled-work-0' });
- await inFlight[0];
- const thirtyThird = controller.submit({
- requestId: 'root-33-after-capacity',
- text: '开始工作 33',
- explicitTarget: facts[32]!.target,
- });
- await Promise.resolve();
- await Promise.resolve();
- assert.equal(admitted, 33);
- finishes.get('work-32')?.({ turnId: 'settled-work-32' });
- await thirtyThird;
-});
-
-test('an old correction barrier survives more than 32 newer corrections', async () => {
- const stopped: Array<[string, string]> = [];
- const fillers = Array.from({ length: 32 }, (_, index) =>
- session(`barrier-filler-${index}`, { sessionName: `屏障填充 ${index}` }));
- const sessions = port([
- session('old-pending', { sessionName: '旧的待定工作', updatedAt: 100 }),
- session('sink', { sessionName: '安全收件箱', updatedAt: 90 }),
- ...fillers,
- ]);
- let finishOld!: (value: { turnId: string }) => void;
- const oldTurn = new Promise<{ turnId: string }>((resolve) => {
- finishOld = resolve;
- });
- sessions.submit = async (target, _text, turnId) => {
- if (target.sessionId === 'old-pending') return oldTurn;
- if (target.sessionId === 'sink') return { turnId, steered: true };
- return { turnId };
- };
- sessions.stop = async (target, turnId) => {
- stopped.push([target.sessionId, turnId]);
- };
- const controller = createWorkHubController({ sessions });
- const oldSubmission = controller.submit({
- requestId: 'old-pending-root',
- text: '开始旧的待定工作',
- explicitTarget: { sessionId: 'old-pending' },
- });
- await Promise.resolve();
- await Promise.resolve();
- await controller.submit({
- requestId: 'correct-old-pending',
- text: '旧工作路由错了',
- explicitTarget: { sessionId: 'sink' },
- correction: {
- from: { sessionId: 'old-pending' },
- turnId: 'reserved-turn-1',
- },
- });
-
- for (const [index, filler] of fillers.entries()) {
- const owned = await controller.submit({
- requestId: `newer-barrier-root-${index}`,
- text: `开始屏障填充 ${index}`,
- explicitTarget: filler.target,
- });
- assert.equal(owned.kind, 'submitted');
- if (owned.kind !== 'submitted') continue;
- await controller.submit({
- requestId: `newer-barrier-correction-${index}`,
- text: `屏障填充 ${index} 路由错了`,
- explicitTarget: { sessionId: 'sink' },
- correction: { from: filler.target, turnId: owned.turnId },
- });
- }
-
- finishOld({ turnId: 'old-host-rebound' });
- await oldSubmission;
- assert.equal(
- stopped.some(([sessionId, turnId]) =>
- sessionId === 'old-pending' && turnId === 'old-host-rebound'),
- true,
- );
-});
-
-test('a definite Host rejection releases only its own pending admission', async () => {
- const stopped: Array<[string, string]> = [];
- const sessions = port([
- session('login', { sessionName: '登录稳定性' }),
- session('payment', { sessionName: '支付稳定性' }),
- ]);
- let shouldReject = true;
- sessions.submit = async (_target, _text, turnId) => {
- if (shouldReject) {
- shouldReject = false;
- throw new WorkHubSessionSubmitError('not admitted', 'rejected');
- }
- return { turnId };
- };
- sessions.stop = async (target, turnId) => {
- stopped.push([target.sessionId, turnId]);
- };
- const controller = createWorkHubController({ sessions });
-
- await assert.rejects(controller.submit({
- requestId: 'definitely-rejected',
- text: '继续支付稳定性',
- explicitTarget: { sessionId: 'payment' },
- }), /not admitted/u);
- const admitted = await controller.submit({
- requestId: 'later-admitted',
- text: '继续支付稳定性',
- explicitTarget: { sessionId: 'payment' },
- });
- assert.equal(admitted.kind, 'submitted');
- await controller.submit({
- requestId: 'correct-later-admitted',
- text: '不是支付,改成登录',
- explicitTarget: { sessionId: 'login' },
- correction: { from: { sessionId: 'payment' } },
- });
-
- assert.deepEqual(stopped, [[
- 'payment',
- admitted.kind === 'submitted' ? admitted.turnId : '',
- ]]);
-});
-
-test('a lost delivery reply is reconciled to its authoritative root ownership', async () => {
- const stopped: Array<[string, string]> = [];
- const sessions = port([
- session('login', { sessionName: '登录稳定性' }),
- session('payment', { sessionName: '支付稳定性' }),
- ]);
- sessions.submit = async () => {
- throw new WorkHubSessionSubmitError('reply lost', 'unknown');
- };
- sessions.reconcileSubmission = async () => ({
- kind: 'root',
- turnId: 'authoritative-payment-root',
- });
- sessions.stop = async (target, turnId) => {
- stopped.push([target.sessionId, turnId]);
- };
- const controller = createWorkHubController({ sessions });
-
- await assert.rejects(controller.submit({
- requestId: 'reply-lost',
- text: '继续支付稳定性',
- explicitTarget: { sessionId: 'payment' },
- }), /reply lost/u);
- sessions.submit = async (_target, _text, turnId) => ({ turnId });
- await controller.submit({
- requestId: 'correct-reconciled-root',
- text: '不是支付,改成登录',
- explicitTarget: { sessionId: 'login' },
- correction: { from: { sessionId: 'payment' } },
- });
-
- assert.deepEqual(stopped, [['payment', 'authoritative-payment-root']]);
-});
-
-test('an unknown delivery remains pending until a later authoritative reconciliation', async () => {
- const stopped: Array<[string, string]> = [];
- const sessions = port([
- session('login', { sessionName: '登录稳定性' }),
- session('payment', { sessionName: '支付稳定性' }),
- ]);
- sessions.submit = async () => {
- throw new WorkHubSessionSubmitError('reply lost', 'unknown');
- };
- let reconciliation: Awaited<ReturnType<WorkHubSessionPort['reconcileSubmission']>> = {
- kind: 'unknown',
- };
- sessions.reconcileSubmission = async () => reconciliation;
- sessions.stop = async (target, turnId) => {
- stopped.push([target.sessionId, turnId]);
- };
- const controller = createWorkHubController({ sessions });
-
- await assert.rejects(controller.submit({
- requestId: 'unknown-delivery',
- text: '继续支付稳定性',
- explicitTarget: { sessionId: 'payment' },
- }), /reply lost/u);
- reconciliation = { kind: 'root', turnId: 'later-authoritative-root' };
- await controller.read();
- sessions.submit = async (_target, _text, turnId) => ({ turnId });
- await controller.submit({
- requestId: 'correct-later-reconciled-root',
- text: '不是支付,改成登录',
- explicitTarget: { sessionId: 'login' },
- correction: { from: { sessionId: 'payment' } },
- });
-
- assert.deepEqual(stopped, [['payment', 'later-authoritative-root']]);
-});
-
-test('a partial multi-Host catalog never erases confirmed ownership', async () => {
- const stopped: Array<[string, string]> = [];
- const allSessions = [
- session('login', { sessionName: '登录稳定性' }),
- session('remote-payment', { sessionName: '远端支付稳定性' }),
- ];
- const sessions = port(allSessions);
- let visible = allSessions;
- let paymentCatalogComplete = true;
- sessions.listCatalog = async () => ({
- sessions: visible,
- isCompleteFor: (target) =>
- target.sessionId !== 'remote-payment' || paymentCatalogComplete,
- });
- sessions.submit = async (_target, _text, turnId) => ({ turnId });
- sessions.stop = async (target, turnId) => {
- stopped.push([target.sessionId, turnId]);
- };
- const controller = createWorkHubController({ sessions });
- const owned = await controller.submit({
- requestId: 'remote-root',
- text: '继续远端支付',
- explicitTarget: { sessionId: 'remote-payment' },
- });
- assert.equal(owned.kind, 'submitted');
-
- visible = [allSessions[0]!];
- paymentCatalogComplete = false;
- await controller.submit({
- requestId: 'correct-after-partial-catalog',
- text: '不是支付,改成登录',
- explicitTarget: { sessionId: 'login' },
- correction: { from: { sessionId: 'remote-payment' } },
- });
-
- assert.deepEqual(stopped, [[
- 'remote-payment',
- owned.kind === 'submitted' ? owned.turnId : '',
- ]]);
-});
-
-test('a stale complete catalog never erases newer confirmed ownership', async () => {
- const stopped: Array<[string, string]> = [];
- const allSessions = [
- session('login', { sessionName: '登录稳定性' }),
- session('payment', { sessionName: '支付稳定性' }),
- ];
- const sessions = port(allSessions);
- let signalStaleReadStarted!: () => void;
- const staleReadStarted = new Promise<void>((resolve) => {
- signalStaleReadStarted = resolve;
- });
- let finishStaleRead!: (value: {
- sessions: WorkHubSessionFacts[];
- isCompleteFor(target: { sessionId: string }): boolean;
- }) => void;
- const staleCatalog = new Promise<{
- sessions: WorkHubSessionFacts[];
- isCompleteFor(target: { sessionId: string }): boolean;
- }>((resolve) => {
- finishStaleRead = resolve;
- });
- let catalogReads = 0;
- sessions.listCatalog = async () => {
- catalogReads += 1;
- if (catalogReads === 1) {
- signalStaleReadStarted();
- return staleCatalog;
- }
- return {
- sessions: allSessions,
- isCompleteFor: () => true,
- };
- };
- sessions.submit = async (_target, _text, turnId) => ({ turnId });
- sessions.stop = async (target, turnId) => {
- stopped.push([target.sessionId, turnId]);
- };
- const controller = createWorkHubController({ sessions });
-
- const staleRead = controller.read();
- await staleReadStarted;
- const owned = await controller.submit({
- requestId: 'payment-root-after-stale-read-started',
- text: '继续支付稳定性',
- explicitTarget: { sessionId: 'payment' },
- });
- assert.equal(owned.kind, 'submitted');
-
- finishStaleRead({
- sessions: [allSessions[0]!],
- isCompleteFor: () => true,
- });
- await staleRead;
- await controller.submit({
- requestId: 'correct-after-stale-complete-catalog',
- text: '不是支付,改成登录',
- explicitTarget: { sessionId: 'login' },
- correction: { from: { sessionId: 'payment' } },
- });
-
- assert.deepEqual(stopped, [[
- 'payment',
- owned.kind === 'submitted' ? owned.turnId : '',
- ]]);
-});
-
-test('authoritative Session removal releases uncertain admissions before global backpressure', async () => {
- const stale = Array.from({ length: 32 }, (_, index) =>
- session(`removed-${index}`, { sessionName: `已删除工作 ${index}` }));
- const fresh = session('fresh', { sessionName: '新工作' });
- const sessions = port(stale);
- let visible = stale;
- let removedCatalogComplete = false;
- sessions.listCatalog = async () => ({
- sessions: visible,
- isCompleteFor: (target) =>
- target.sessionId.startsWith('removed-') && removedCatalogComplete,
- });
- sessions.submit = async () => {
- throw new WorkHubSessionSubmitError('reply lost', 'unknown');
- };
- sessions.reconcileSubmission = async () => ({ kind: 'unknown' });
- const controller = createWorkHubController({ sessions });
-
- for (const [index, fact] of stale.entries()) {
- await assert.rejects(controller.submit({
- requestId: `uncertain-${index}`,
- text: `开始已删除工作 ${index}`,
- explicitTarget: fact.target,
- }), /reply lost/u);
- }
-
- visible = [fresh];
- removedCatalogComplete = true;
- sessions.submit = async (_target, _text, turnId) => ({ turnId });
- const admitted = await controller.submit({
- requestId: 'after-authoritative-removal',
- text: '开始新工作',
- explicitTarget: fresh.target,
- });
-
- assert.equal(admitted.kind, 'submitted');
- assert.deepEqual(admitted.kind === 'submitted' ? admitted.target : undefined, fresh.target);
-});
-
-test('a lost reply reconciled as steering never claims the pre-existing root', async () => {
- const stopped: Array<[string, string]> = [];
- const sessions = port([
- session('login', { sessionName: '登录稳定性' }),
- session('payment', { sessionName: '支付稳定性' }),
- ]);
- sessions.submit = async () => {
- throw new WorkHubSessionSubmitError('steering reply lost', 'unknown');
- };
- sessions.reconcileSubmission = async () => ({ kind: 'steered' });
- sessions.stop = async (target, turnId) => {
- stopped.push([target.sessionId, turnId]);
- };
- const controller = createWorkHubController({ sessions });
-
- await assert.rejects(controller.submit({
- requestId: 'steering-reply-lost',
- text: '补充支付测试',
- explicitTarget: { sessionId: 'payment' },
- }), /steering reply lost/u);
- sessions.submit = async (_target, _text, turnId) => ({ turnId });
- await controller.submit({
- requestId: 'correct-after-steering-reply-loss',
- text: '不是支付,改成登录',
- explicitTarget: { sessionId: 'login' },
- correction: { from: { sessionId: 'payment' } },
- });
-
- assert.deepEqual(stopped, []);
-});
-
-test('WorkHub-owned root remains stoppable after navigating away and back', async () => {
- const stopped: Array<[string, string]> = [];
- const sessions = port([
- session('login', { sessionName: '登录稳定性', updatedAt: 20 }),
- session('payment', { sessionName: '支付稳定性', updatedAt: 30 }),
- ]);
- sessions.submit = async (target) => ({ turnId: 'turn-' + target.sessionId });
- sessions.stop = async (target, turnId) => {
- stopped.push([target.sessionId, turnId]);
- };
- const controller = createWorkHubController({ sessions });
- await controller.read();
- await controller.submit({
- requestId: 'request-owned-before-navigation',
- text: '继续这个工作',
- });
- controller.resetVisitContext();
- await controller.read({ focus: { sessionId: 'payment' } });
-
- const corrected = await controller.submit({
- requestId: 'request-correction-after-return',
- text: '不是这个工作,换成登录稳定性',
- });
-
- assert.deepEqual(corrected.kind === 'submitted' ? corrected.target : undefined, {
- sessionId: 'login',
- });
- assert.deepEqual(stopped, [['payment', 'turn-payment']]);
-});
-
-test('natural-language correction never stops a pre-existing focused Session', async () => {
- const stopped: string[] = [];
- const sessions = port([
- session('login', { sessionName: '登录稳定性', updatedAt: 20 }),
- session('payment', { sessionName: '支付稳定性', state: 'running', updatedAt: 30 }),
- ]);
- sessions.submit = async (target) => ({ turnId: `turn-${target.sessionId}` });
- sessions.stop = async (target) => {
- stopped.push(target.sessionId);
- };
- const controller = createWorkHubController({ sessions });
- await controller.read();
-
- const corrected = await controller.submit({
- requestId: 'request-safe-natural-correction',
- text: '不是这个,用登录那个',
- });
-
- assert.deepEqual(corrected.kind === 'submitted' ? corrected.target : undefined, {
- sessionId: 'login',
- });
- assert.deepEqual(corrected.kind === 'submitted' ? corrected.correctedFrom : undefined, {
- sessionId: 'payment',
- });
- assert.deepEqual(stopped, []);
-});
-
-test('English natural-language correction names the replacement Session', async () => {
- const submitted: string[] = [];
- const sessions = port([
- session('login', { sessionName: 'Login Reliability', updatedAt: 20 }),
- session('payment', { sessionName: 'Payment Webhooks', updatedAt: 30 }),
- ]);
- sessions.submit = async (target) => {
- submitted.push(target.sessionId);
- return { turnId: `turn-${submitted.length}` };
- };
- const controller = createWorkHubController({ sessions });
- await controller.read();
-
- const corrected = await controller.submit({
- requestId: 'request-english-natural-correction',
- text: 'Not that work; switch to Login Reliability and add the retry checks',
- });
-
- assert.deepEqual(corrected.kind === 'submitted' ? corrected.target : undefined, {
- sessionId: 'login',
- });
- assert.equal(
- corrected.kind === 'submitted' ? corrected.evidence : undefined,
- 'route_correction',
- );
-});
-
-test('ambiguous natural-language correction preserves correction context through clarification', async () => {
- const submitted: string[] = [];
- const stopped: Array<[string, string]> = [];
- const sessions = port([
- session('login-api', { sessionName: '登录 API 稳定性', updatedAt: 20 }),
- session('login-ui', { sessionName: '登录 UI 稳定性', updatedAt: 10 }),
- session('payment', { sessionName: '支付回调幂等性', updatedAt: 30 }),
- ]);
- sessions.submit = async (target) => {
- submitted.push(target.sessionId);
- return { turnId: `turn-${submitted.length}` };
- };
- sessions.stop = async (target, turnId) => {
- stopped.push([target.sessionId, turnId]);
- };
- const controller = createWorkHubController({ sessions });
- await controller.read();
- await controller.submit({
- requestId: 'request-payment-before-clarification',
- text: '继续这个工作',
- });
-
- const clarification = await controller.submit({
- requestId: 'request-natural-clarification',
- text: '不是这个,换成登录那个',
- });
- assert.equal(clarification.kind, 'clarification');
- if (clarification.kind !== 'clarification') return;
- assert.deepEqual(
- clarification.options.map((option) => option.target.sessionId),
- ['login-api', 'login-ui'],
- );
- assert.deepEqual(clarification.correction, {
- from: { sessionId: 'payment' },
- turnId: 'turn-1',
- });
-
- const corrected = await controller.submit({
- requestId: clarification.requestId,
- text: clarification.text,
- explicitTarget: { sessionId: 'login-api' },
- correction: clarification.correction,
- });
- assert.equal(corrected.kind, 'submitted');
- assert.deepEqual(stopped, [['payment', 'turn-1']]);
- assert.deepEqual(submitted, ['payment', 'login-api']);
-});
-
-test('latest route correction wins for the same expression family', async () => {
- const sessions = port([
- session('login', { sessionName: '登录稳定性' }),
- session('payment', { sessionName: '支付稳定性' }),
- ]);
- sessions.submit = async (_target) => ({ turnId: 'turn' });
- const controller = createWorkHubController({ sessions });
-
- await controller.submit({
- requestId: 'correction-login',
- text: '继续白鹭点,列出验收项。',
- explicitTarget: { sessionId: 'login' },
- correction: { from: { sessionId: 'payment' }, turnId: 'turn' },
- });
- await controller.submit({
- requestId: 'correction-payment',
- text: '继续白鹭点,列出异常项。',
- explicitTarget: { sessionId: 'payment' },
- correction: { from: { sessionId: 'login' }, turnId: 'turn' },
- });
-
- const result = await controller.submit({
- requestId: 'correction-latest',
- text: '继续白鹭点,补充回滚条件。',
- });
-
- assert.deepEqual(result.kind === 'submitted' ? result.target : undefined, {
- sessionId: 'payment',
- });
- assert.equal(result.kind === 'submitted' ? result.evidence : undefined, 'route_correction');
-});
-
-test('user correction order wins when overlapping submissions finish out of order', async () => {
- const sessions = port([
- session('login', { sessionName: '登录稳定性' }),
- session('payment', { sessionName: '支付稳定性' }),
- ]);
- let signalOlderStarted!: () => void;
- const olderStarted = new Promise<void>((resolve) => {
- signalOlderStarted = resolve;
- });
- let finishOlder!: (value: { turnId: string }) => void;
- const olderTurn = new Promise<{ turnId: string }>((resolve) => {
- finishOlder = resolve;
- });
- sessions.submit = async (target) => {
- if (target.sessionId === 'login') {
- signalOlderStarted();
- return olderTurn;
- }
- return { turnId: 'turn-payment' };
- };
- const controller = createWorkHubController({ sessions });
-
- const olderCorrection = controller.submit({
- requestId: 'correction-older-login',
- text: '继续白鹭点,列出验收项。',
- explicitTarget: { sessionId: 'login' },
- correction: { from: { sessionId: 'payment' } },
- });
- await olderStarted;
- controller.resetVisitContext();
- await controller.submit({
- requestId: 'correction-newer-payment',
- text: '继续白鹭点,列出异常项。',
- explicitTarget: { sessionId: 'payment' },
- correction: { from: { sessionId: 'login' } },
- });
- finishOlder({ turnId: 'turn-login' });
- await olderCorrection;
-
- const result = await controller.submit({
- requestId: 'correction-after-overlap',
- text: '继续白鹭点,补充回滚条件。',
- });
-
- assert.deepEqual(result.kind === 'submitted' ? result.target : undefined, {
- sessionId: 'payment',
- });
- assert.equal(result.kind === 'submitted' ? result.evidence : undefined, 'route_correction');
-});
-
-test('waiting Session rejects a second root request without calling submit', async () => {
- let submitted = false;
- const sessions = port([
- session('login', {
- sessionName: '排查令牌过期重复登录问题',
- state: 'waiting_for_user',
- }),
- ]);
- sessions.submit = async () => {
- submitted = true;
- return { turnId: 'unexpected' };
- };
- const controller = createWorkHubController({ sessions });
-
- const result = await controller.submit({
- requestId: 'request-waiting',
- text: '排查令牌过期重复登录问题:补充一条等待状态下的新请求。',
- });
-
- assert.deepEqual(result, {
- kind: 'waiting',
- strategyId: WORKHUB_ROUTING_STRATEGY_ID,
- requestId: 'request-waiting',
- text: '排查令牌过期重复登录问题:补充一条等待状态下的新请求。',
- target: { sessionId: 'login' },
- });
- assert.equal(submitted, false);
-});
-
-test('submit returns to the previous focused Session', async () => {
- const submitted: string[] = [];
- const sessions = port([
- session('login', { sessionName: '登录刷新令牌' }),
- session('payment', { sessionName: '支付回调幂等性' }),
- ]);
- sessions.submit = async (target) => {
- submitted.push(target.sessionId);
- return { turnId: `turn-${submitted.length}` };
- };
- const controller = createWorkHubController({ sessions });
- await controller.submit({
- requestId: 'request-login',
- text: '先看登录',
- explicitTarget: { sessionId: 'login' },
- });
- await controller.submit({
- requestId: 'request-payment',
- text: '再看支付',
- explicitTarget: { sessionId: 'payment' },
- });
-
- const result = await controller.submit({
- requestId: 'request-previous',
- text: '回到上一个工作',
- });
-
- assert.equal(result.kind, 'submitted');
- assert.deepEqual(result.kind === 'submitted' ? result.target : undefined, { sessionId: 'login' });
- assert.deepEqual(submitted, ['login', 'payment', 'login']);
-});
-
-test('submit lets strong foreign core evidence override a vague focus word', async () => {
- const submitted: string[] = [];
- const sessions = port([
- session('login', {
- sessionName: '登录稳定性',
- latestResult: '处理刷新令牌过期导致的重复登录',
- }),
- session('payment', {
- sessionName: '支付稳定性',
- latestResult: '处理支付回调重复投递',
- }),
- ]);
- sessions.submit = async (target) => {
- submitted.push(target.sessionId);
- return { turnId: `turn-${submitted.length}` };
- };
- const controller = createWorkHubController({ sessions });
- await controller.submit({
- requestId: 'request-payment-focus',
- text: '先看支付',
- explicitTarget: { sessionId: 'payment' },
- });
-
- const result = await controller.submit({
- requestId: 'request-foreign-core',
- text: '继续处理刷新令牌过期',
- });
-
- assert.equal(result.kind, 'submitted');
- assert.deepEqual(result.kind === 'submitted' ? result.target : undefined, { sessionId: 'login' });
- assert.deepEqual(submitted, ['payment', 'login']);
-});
-
-test('submit keeps unmatched non-executable conversation in WorkHub', async () => {
- let created = false;
- const actions: unknown[] = [];
- const sessions = port([]);
- sessions.create = async () => {
- created = true;
- return session('unexpected');
- };
- const controller = createGatedWorkHubController({
- sessions,
- coordination: {
- open: async () => ({ close: async () => undefined }),
- answer: async (input) => ({ turnId: input.turnId }),
- record: async (input) => ({ turnId: input.turnId }),
- candidates: async () => ({
- candidateSetId: `sha256:${'a'.repeat(64)}`,
- candidates: [],
- }),
- act: async (input) => {
- actions.push(input);
- return {
- disposition: 'answer_here',
- coordinationTurnId: 'coordination-turn',
- };
- },
- },
- });
-
- const result = await controller.submit({
- requestId: 'request-discussion',
- text: '你觉得统一入口最重要的价值是什么?',
- });
-
- assert.deepEqual(result, {
- kind: 'discussion',
- strategyId: WORKHUB_ROUTING_STRATEGY_ID,
- requestId: 'request-discussion',
- text: '你觉得统一入口最重要的价值是什么?',
- });
- assert.equal(created, false);
- assert.deepEqual(actions, [
- {
- actionId: 'request-discussion',
- userText: '你觉得统一入口最重要的价值是什么?',
- proposal: { disposition: 'answer_here' },
- },
+ assert.equal(created, false);
+ assert.deepEqual(actions, [
+ {
+ actionId: 'request-discussion',
+ userText: '你觉得统一入口最重要的价值是什么?',
+ proposal: { disposition: 'answer_here' },
+ },
]);
});
@@ -2462,14 +1332,10 @@ test('production submission delegates only through the Runtime-owned candidate r
sessions.submit = async () => {
throw new Error('renderer direct submit must not be used');
};
- sessions.stop = async () => {
- throw new Error('renderer direct stop must not be used');
- };
const controller = createGatedWorkHubController({
sessions,
coordination: {
open: async () => ({ close: async () => undefined }),
- answer: async (input) => ({ turnId: input.turnId }),
record: async (input) => ({ turnId: input.turnId }),
candidates: async () => ({
candidateSetId: `sha256:${'b'.repeat(64)}`,
@@ -2524,7 +1390,6 @@ test('production retry reaches durable Action Gate replay while target is waitin
sessions,
coordination: {
open: async () => ({ close: async () => undefined }),
- answer: async (input) => ({ turnId: input.turnId }),
record: async (input) => ({ turnId: input.turnId }),
candidates: async () => ({
candidateSetId: `sha256:${'c'.repeat(64)}`,
@@ -2570,7 +1435,6 @@ test('production defers destructive correction until persistent delegation exist
sessions,
coordination: {
open: async () => ({ close: async () => undefined }),
- answer: async (input) => ({ turnId: input.turnId }),
record: async (input) => ({ turnId: input.turnId }),
candidates: async () => ({
candidateSetId: `sha256:${'d'.repeat(64)}`,
@@ -2676,7 +1540,6 @@ test('production natural-language correction fails closed before a second delega
sessions,
coordination: {
open: async () => ({ close: async () => undefined }),
- answer: async (input) => ({ turnId: input.turnId }),
record: async (input) => ({ turnId: input.turnId }),
candidates: async () => ({ candidateSetId, candidates }),
act: async (input) => {
@@ -2732,7 +1595,6 @@ test('production correction-shaped creation stays create_new without an existing
sessions: port([]),
coordination: {
open: async () => ({ close: async () => undefined }),
- answer: async (input) => ({ turnId: input.turnId }),
record: async (input) => ({ turnId: input.turnId }),
candidates: async () => ({
candidateSetId: `sha256:${'c'.repeat(64)}`,
@@ -2764,7 +1626,6 @@ test('production clarification is persisted through the typed Action Gate dispos
sessions: port([]),
coordination: {
open: async () => ({ close: async () => undefined }),
- answer: async (input) => ({ turnId: input.turnId }),
record: async () => {
throw new Error('legacy summary recording must not persist clarification');
},
@@ -2808,7 +1669,6 @@ test('production creation leaves Session identity and workspace authority to mai
sessions,
coordination: {
open: async () => ({ close: async () => undefined }),
- answer: async (input) => ({ turnId: input.turnId }),
record: async (input) => ({ turnId: input.turnId }),
candidates: async () => ({
candidateSetId: `sha256:${'c'.repeat(64)}`,
diff --git a/apps/desktop/src/main/__tests__/workhub-coordination-host-scope.test.ts b/apps/desktop/src/main/__tests__/workhub-coordination-host-scope.test.ts
index 0a4e014fe2..5feca3844c 100644
--- a/apps/desktop/src/main/__tests__/workhub-coordination-host-scope.test.ts
+++ b/apps/desktop/src/main/__tests__/workhub-coordination-host-scope.test.ts
@@ -19,10 +19,7 @@
import assert from 'node:assert/strict';
import test from 'node:test';
-import {
- desktopSessionKey,
- parseDesktopSessionKey,
-} from '../../shared/runtime-host-identity.js';
+import { desktopSessionKey } from '../../shared/runtime-host-identity.js';
import { scopeWorkHubSessionsToCoordinationHost } from '../../renderer/workhub-coordination-host-scope.js';
import {
startWorkHubCoordinationLifecycle,
@@ -30,7 +27,7 @@ import {
} from '../../renderer/workhub-coordination-lifecycle.js';
import type { WorkHubDesktopSessionBridge } from '../../renderer/workhub-session-port.js';
-test('WorkHub candidates follow the resolved Coordination Session Host only', async () => {
+test('WorkHub projections follow the resolved Coordination Session Host only', async () => {
const sessionA = desktopSessionKey({ hostId: 'host-a', sessionId: 'ordinary-a' });
const sessionB = desktopSessionKey({ hostId: 'host-b', sessionId: 'ordinary-b' });
let coordinationSessionId: string | undefined;
@@ -44,19 +41,9 @@ test('WorkHub candidates follow the resolved Coordination Session Host only', as
completeHostIds: ['host-a', 'host-b'],
}),
listTurns: async () => [],
- create: async () => {
- throw new Error('unscoped create must not be used');
- },
- send: async () => ({ ok: true, turnId: 'turn' }),
- stop: async () => undefined,
+ queryMessageExecutions: async () => ({ resolutions: [] }),
subscribeChanges: () => () => undefined,
};
- const createdFor: string[] = [];
- const createOnCoordinationHost = async (coordinationId: string) => {
- createdFor.push(coordinationId);
- const { hostId } = parseDesktopSessionKey(coordinationId);
- return ordinarySession(hostId === 'host-a' ? sessionA : sessionB);
- };
const scopeSessions = () => {
const generation = coordinationGeneration;
return scopeWorkHubSessionsToCoordinationHost(
@@ -65,7 +52,6 @@ test('WorkHub candidates follow the resolved Coordination Session Host only', as
sessionId: coordinationSessionId,
isCurrent: () => generation === coordinationGeneration,
},
- createOnCoordinationHost,
);
};
let sessions = scopeSessions();
@@ -93,13 +79,11 @@ test('WorkHub candidates follow the resolved Coordination Session Host only', as
});
const unresolvedList = sessions.list();
- const unresolvedCreate = assert.rejects(sessions.create({ name: 'unsafe' }), /unresolved/);
assert.deepEqual(await unresolvedList, []);
- await unresolvedCreate;
await Promise.resolve();
assert.deepEqual((await sessions.list()).map((session) => session.id), [sessionA]);
await assert.rejects(
- () => sessions.send(sessionB, { type: 'send', turnId: 'turn', text: 'wrong Host' }),
+ () => sessions.listTurns(sessionB),
/another Runtime Host/,
);
assert.deepEqual(await sessions.listWithCoverage?.(), {
@@ -113,12 +97,7 @@ test('WorkHub candidates follow the resolved Coordination Session Host only', as
assert.deepEqual(await sessions.list(), []);
await Promise.resolve();
assert.deepEqual((await sessions.list()).map((session) => session.id), [sessionB]);
- await assert.rejects(staleHostAScope.create({ name: 'stale A' }), /scope is revoked/);
- assert.equal((await sessions.create({ name: 'current B' })).id, sessionB);
- assert.deepEqual(
- createdFor.map((sessionId) => parseDesktopSessionKey(sessionId).hostId),
- ['host-b'],
- );
+ await assert.rejects(staleHostAScope.listTurns(sessionA), /scope is revoked/);
stop();
});
diff --git a/apps/desktop/src/main/__tests__/workhub-session-port.test.ts b/apps/desktop/src/main/__tests__/workhub-session-port.test.ts
index 8f3eb7d0c2..fbf60f1c43 100644
--- a/apps/desktop/src/main/__tests__/workhub-session-port.test.ts
+++ b/apps/desktop/src/main/__tests__/workhub-session-port.test.ts
@@ -31,7 +31,6 @@ import {
createDesktopWorkHubCoordinationPort,
projectWorkHubCoordinationTurns,
} from '../../renderer/workhub-coordination-port.js';
-import { WorkHubSessionSubmitError } from '../../renderer/workhub-controller.js';
function desktopSession(
id: string,
@@ -56,6 +55,14 @@ const unusedTranscripts = {
},
};
+const noMessageExecutions = async () => ({
+ resolutions: [] as Array<
+ | { messageId: string; state: 'pending' }
+ | { messageId: string; state: 'cancelled' }
+ | { messageId: string; state: 'owned'; turnId: string; runId: string }
+ >,
+});
+
function transcriptsWith(messages: readonly StoredMessage[]) {
return {
open: async (sessionId: string, handler: (batch: DesktopTranscriptBatch) => void) => {
@@ -148,8 +155,12 @@ test('projects the durable Coordination transcript into the WorkHub conversation
text: 'Continue payments',
state: 'completed',
assignment: {
+ delegationId: 'payments-delegation',
targetSessionId: 'payments',
targetSessionName: 'Payments',
+ targetMessageId: 'payments-message',
+ targetTurnId: 'payments-turn',
+ feedbackState: 'accepted',
},
updatedAt: 20,
}]);
@@ -189,7 +200,6 @@ test('Coordination transcript adapter emits an initial empty ready snapshot and
};
},
},
- answer: async (input) => ({ turnId: input.turnId }),
record: async (input) => ({ turnId: input.turnId }),
candidates: async () => ({
candidateSetId: `sha256:${'a'.repeat(64)}`,
@@ -291,6 +301,7 @@ test('desktop adapter rebuilds recent turns from the Session transcript and clos
sessions: {
list: async () => [],
listTurns: async () => [],
+ queryMessageExecutions: noMessageExecutions,
create: async () => {
throw new Error('not used');
},
@@ -342,7 +353,6 @@ test('desktop adapter rebuilds recent turns from the Session transcript and clos
},
},
projectName: () => 'Maka',
- newTurnId: () => 'unused',
});
assert.deepEqual(await adapter.recentTurns([{ sessionId }]), [{
@@ -366,6 +376,7 @@ test('desktop adapter cancels an unavailable transcript without hiding ready Ses
sessions: {
list: async () => [],
listTurns: async () => [],
+ queryMessageExecutions: noMessageExecutions,
create: async () => { throw new Error('not used'); },
send: async () => { throw new Error('not used'); },
stop: async () => {},
@@ -403,7 +414,6 @@ test('desktop adapter cancels an unavailable transcript without hiding ready Ses
},
},
projectName: () => 'Maka',
- newTurnId: () => 'unused',
});
const turns = adapter.recentTurns([
@@ -448,6 +458,7 @@ test('desktop adapter projects Session catalog facts without owning copies', asy
sessions: {
list: async () => source,
listTurns: async () => [],
+ queryMessageExecutions: noMessageExecutions,
create: async () => {
throw new Error('not used');
},
@@ -458,7 +469,6 @@ test('desktop adapter projects Session catalog facts without owning copies', asy
subscribeChanges: () => () => {},
},
projectName: (projectId) => projectId === 'project-maka' ? 'Maka' : undefined,
- newTurnId: () => 'unused',
});
assert.deepEqual(await adapter.list(), [
@@ -506,266 +516,163 @@ test('desktop adapter projects Session catalog facts without owning copies', asy
]);
});
-test('desktop adapter preserves per-Host catalog coverage for ownership reconciliation', async () => {
- const localSessionId = desktopSessionKey({ hostId: 'local-host', sessionId: 'local' });
+test('desktop adapter rebuilds delegation feedback from the Message-owned execution Turn', async () => {
+ const sessions = [
+ desktopSession('accepted'),
+ desktopSession('running', { status: 'running', runningTurnIds: ['turn-running'] }),
+ desktopSession('stale-running'),
+ desktopSession('recorded-running-only', { runningTurnIds: undefined }),
+ desktopSession('waiting', {
+ status: 'waiting_for_user',
+ runningTurnIds: ['turn-waiting'],
+ }),
+ desktopSession('completed', {
+ status: 'waiting_for_user',
+ runningTurnIds: ['later-turn'],
+ }),
+ desktopSession('failed'),
+ desktopSession('aborted'),
+ desktopSession('cancelled'),
+ desktopSession('recovering'),
+ ];
+ const turns = new Map<string, Array<{
+ turnId: string;
+ status: 'running' | 'completed' | 'failed' | 'aborted';
+ statusSource: 'recorded';
+ }>>([
+ ['running', [{ turnId: 'turn-running', status: 'running', statusSource: 'recorded' }]],
+ ['stale-running', [{
+ turnId: 'turn-stale-running',
+ status: 'running',
+ statusSource: 'recorded',
+ }]],
+ ['recorded-running-only', [{
+ turnId: 'turn-recorded-running-only',
+ status: 'running',
+ statusSource: 'recorded',
+ }]],
+ ['waiting', [{ turnId: 'turn-waiting', status: 'running', statusSource: 'recorded' }]],
+ ['completed', [{ turnId: 'turn-completed', status: 'completed', statusSource: 'recorded' }]],
+ ['failed', [{ turnId: 'turn-failed', status: 'failed', statusSource: 'recorded' }]],
+ ['aborted', [{ turnId: 'turn-aborted', status: 'aborted', statusSource: 'recorded' }]],
+ ]);
const adapter = createDesktopWorkHubSessionPort({
transcripts: unusedTranscripts,
sessions: {
- list: async () => [],
- listWithCoverage: async () => ({
- sessions: [desktopSession(localSessionId)],
- completeHostIds: ['local-host'],
+ list: async () => sessions,
+ listTurns: async (sessionId) => {
+ if (sessionId === 'recovering') throw new Error('Host is recovering');
+ return turns.get(sessionId) ?? [];
+ },
+ queryMessageExecutions: async (sessionId, messageIds) => ({
+ resolutions: sessionId === 'accepted'
+ ? messageIds.map((messageId) => ({ messageId, state: 'pending' as const }))
+ : sessionId === 'cancelled'
+ ? messageIds.map((messageId) => ({ messageId, state: 'cancelled' as const }))
+ : sessionId === 'recovering'
+ ? []
+ : messageIds.map((messageId) => ({
+ messageId,
+ state: 'owned' as const,
+ turnId: `turn-${sessionId}`,
+ runId: `run-${sessionId}`,
+ })),
}),
- listTurns: async () => [],
create: async () => { throw new Error('not used'); },
send: async () => { throw new Error('not used'); },
stop: async () => {},
subscribeChanges: () => () => {},
},
projectName: () => 'Maka',
- newTurnId: () => 'unused',
});
-
- const catalog = await adapter.listCatalog?.();
- assert.ok(catalog);
- assert.equal(catalog.sessions[0]?.target.sessionId, localSessionId);
- assert.equal(catalog.isCompleteFor({ sessionId: localSessionId }), true);
- assert.equal(catalog.isCompleteFor({
- sessionId: desktopSessionKey({ hostId: 'remote-host', sessionId: 'remote' }),
- }), false);
+ const references = [
+ ['accepted', 'turn-accepted'],
+ ['running', 'turn-running'],
+ ['stale-running', 'turn-stale-running'],
+ ['recorded-running-only', 'turn-recorded-running-only'],
+ ['waiting', 'turn-waiting'],
+ ['completed', 'turn-completed'],
+ ['failed', 'turn-failed'],
+ ['aborted', 'turn-aborted'],
+ ['cancelled', 'turn-cancelled'],
+ ['recovering', 'turn-recovering'],
+ ].map(([targetSessionId, targetTurnId]) => ({
+ delegationId: `delegation-${targetSessionId}`,
+ targetSessionId: targetSessionId!,
+ targetMessageId: `message-${targetSessionId}`,
+ targetTurnId: targetTurnId!,
+ }));
+
+ const feedback = await adapter.delegationFeedback(references);
+
+ assert.deepEqual(feedback.map(({ delegationId, state }) => ({ delegationId, state })), [
+ { delegationId: 'delegation-accepted', state: 'accepted' },
+ { delegationId: 'delegation-running', state: 'running' },
+ { delegationId: 'delegation-stale-running', state: 'accepted' },
+ { delegationId: 'delegation-recorded-running-only', state: 'running' },
+ { delegationId: 'delegation-waiting', state: 'waiting_for_user' },
+ { delegationId: 'delegation-completed', state: 'completed' },
+ { delegationId: 'delegation-failed', state: 'failed' },
+ { delegationId: 'delegation-aborted', state: 'aborted' },
+ { delegationId: 'delegation-cancelled', state: 'aborted' },
+ { delegationId: 'delegation-recovering', state: 'recovering' },
+ ]);
});
-test('desktop adapter delegates create, send, and invalidation to Session APIs', async () => {
- const calls: unknown[] = [];
- let onChanged: (() => void) | undefined;
+test('desktop adapter follows a delegated Message into its successor Turn', async () => {
+ const targetSessionId = desktopSessionKey({ hostId: 'local-host', sessionId: 'payments' });
const adapter = createDesktopWorkHubSessionPort({
- transcripts: unusedTranscripts,
+ transcripts: transcriptsWith([{
+ type: 'user',
+ id: 'payment-message',
+ turnId: 'successor-turn',
+ ts: 2,
+ text: 'Continue payment recovery',
+ steeringEventId: 'payment-message',
+ }]),
sessions: {
- list: async () => [desktopSession('created', {
+ list: async () => [desktopSession(targetSessionId, {
status: 'running',
- runningTurnIds: ['turn-new'],
+ runningTurnIds: ['successor-turn'],
})],
- listTurns: async () => [],
- create: async (input) => {
- calls.push(['create', input]);
- return desktopSession('created', { name: input.name });
- },
- send: async (sessionId, command) => {
- calls.push(['send', sessionId, command]);
- return { ok: true, turnId: command.turnId };
- },
- stop: async (sessionId, input) => {
- calls.push(['stop', sessionId, input]);
- },
- subscribeChanges: (handler) => {
- onChanged = handler;
- return () => calls.push(['unsubscribe']);
- },
- },
- projectName: () => 'Maka',
- newTurnId: () => 'turn-new',
- });
-
- const created = await adapter.create({ name: '实现导出发票 PDF 功能' });
- const turnId = adapter.reserveTurnId();
- const turn = await adapter.submit(created.target, '实现导出发票 PDF 功能', turnId);
- await adapter.stop(created.target, 'turn-new');
- let invalidations = 0;
- const unsubscribe = adapter.subscribe(() => {
- invalidations += 1;
- });
- onChanged?.();
- unsubscribe();
-
- assert.equal(created.kind, 'ordinary');
- assert.deepEqual(turn, { turnId: 'turn-new' });
- assert.equal(invalidations, 1);
- assert.deepEqual(calls, [
- ['create', { name: '实现导出发票 PDF 功能' }],
- ['send', 'created', { type: 'send', turnId: 'turn-new', text: '实现导出发票 PDF 功能' }],
- ['stop', 'created', { source: 'stop_button', expectedTurnId: 'turn-new' }],
- ['unsubscribe'],
- ]);
-});
-
-test('desktop adapter preserves when Session delivery steered an existing root Turn', async () => {
- const adapter = createDesktopWorkHubSessionPort({
- transcripts: unusedTranscripts,
- sessions: {
- list: async () => [],
- listTurns: async () => [],
- create: async () => {
- throw new Error('not used');
- },
- send: async (_sessionId, command) => ({
- ok: true,
- turnId: command.turnId,
- steered: true,
+ listTurns: async () => [
+ {
+ turnId: 'admission-turn',
+ status: 'completed',
+ statusSource: 'recorded',
+ },
+ {
+ turnId: 'successor-turn',
+ status: 'running',
+ statusSource: 'recorded',
+ },
+ ],
+ queryMessageExecutions: async (_sessionId, messageIds) => ({
+ resolutions: messageIds.map((messageId) => ({
+ messageId,
+ state: 'owned' as const,
+ turnId: 'successor-turn',
+ runId: 'successor-run',
+ })),
}),
- stop: async () => {},
- subscribeChanges: () => () => {},
- },
- projectName: () => 'Maka',
- newTurnId: () => 'turn-steered',
- });
-
- assert.deepEqual(
- await adapter.submit(
- { sessionId: 'busy' },
- '补充已有执行流',
- adapter.reserveTurnId(),
- ),
- { turnId: 'turn-steered', steered: true },
- );
-});
-
-test('desktop adapter distinguishes definite rejection from an unknown delivery outcome', async () => {
- let outcome: 'throw' | 'unknown' | 'reject' = 'throw';
- const adapter = createDesktopWorkHubSessionPort({
- transcripts: unusedTranscripts,
- sessions: {
- list: async () => [],
- listTurns: async () => [],
create: async () => { throw new Error('not used'); },
- send: async () => {
- if (outcome === 'throw') throw new Error('transport disconnected');
- if (outcome === 'unknown') {
- return {
- ok: false as const,
- reason: 'outcome_unknown' as const,
- messageId: 'reserved-turn',
- skillInvocation: { loaded: [], failed: [], receipts: [] },
- };
- }
- return {
- ok: false as const,
- reason: 'skill_invocation_failed' as const,
- skillInvocation: {
- loaded: [],
- failed: [{ request: 'missing', reason: 'not_found' as const }],
- receipts: [],
- },
- };
- },
+ send: async () => { throw new Error('not used'); },
stop: async () => {},
subscribeChanges: () => () => {},
},
projectName: () => 'Maka',
- newTurnId: () => 'reserved-turn',
- });
-
- await assert.rejects(
- adapter.submit({ sessionId: 'payment' }, '继续支付', 'reserved-turn'),
- (error) => error instanceof WorkHubSessionSubmitError && error.admission === 'unknown',
- );
- // The Host declining to prove the outcome must stay reconcilable; only a
- // Host-owned refusal releases the reserved root.
- outcome = 'unknown';
- await assert.rejects(
- adapter.submit({ sessionId: 'payment' }, '继续支付', 'reserved-turn'),
- (error) => error instanceof WorkHubSessionSubmitError && error.admission === 'unknown',
- );
- outcome = 'reject';
- await assert.rejects(
- adapter.submit({ sessionId: 'payment' }, '继续支付', 'reserved-turn'),
- (error) => error instanceof WorkHubSessionSubmitError && error.admission === 'rejected',
- );
-});
-
-test('desktop adapter reconciles lost replies from authoritative transcript identity', async () => {
- const cases: Array<{
- name: string;
- message: StoredMessage;
- expected: { kind: 'root'; turnId: string } | { kind: 'steered' } | { kind: 'unknown' };
- }> = [
- {
- name: 'direct root',
- message: {
- type: 'user', id: 'user-root', turnId: 'reserved-turn', ts: 1, text: '开始支付',
- },
- expected: { kind: 'root', turnId: 'reserved-turn' },
- },
- {
- name: 'busy-race root',
- message: {
- type: 'user', id: 'reserved-turn', turnId: 'host-root', ts: 1, text: '开始支付',
- },
- expected: { kind: 'root', turnId: 'host-root' },
- },
- {
- name: 'steering',
- message: {
- type: 'user',
- id: 'reserved-turn',
- turnId: 'pre-existing-root',
- steeringEventId: 'steering-event',
- ts: 1,
- text: '补充支付测试',
- },
- expected: { kind: 'steered' },
- },
- {
- name: 'unrelated message',
- message: {
- type: 'user', id: 'other-message', turnId: 'other-root', ts: 1, text: '其他工作',
- },
- expected: { kind: 'unknown' },
- },
- ];
-
- for (const fixture of cases) {
- const adapter = createDesktopWorkHubSessionPort({
- transcripts: transcriptsWith([fixture.message]),
- sessions: {
- list: async () => [],
- listTurns: async () => [],
- create: async () => { throw new Error('not used'); },
- send: async () => { throw new Error('not used'); },
- stop: async () => {},
- subscribeChanges: () => () => {},
- },
- projectName: () => 'Maka',
- newTurnId: () => 'reserved-turn',
- });
-
- assert.deepEqual(
- await adapter.reconcileSubmission({
- sessionId: desktopSessionKey({ hostId: 'local-host', sessionId: fixture.name }),
- }, 'reserved-turn'),
- fixture.expected,
- fixture.name,
- );
- }
-});
-
-test('desktop adapter binds stop to the root Turn owned by the WorkHub submission', async () => {
- const stopped: unknown[] = [];
- const adapter = createDesktopWorkHubSessionPort({
- transcripts: unusedTranscripts,
- sessions: {
- list: async () => [],
- listTurns: async () => [],
- create: async () => {
- throw new Error('not used');
- },
- send: async () => {
- throw new Error('not used');
- },
- stop: async (sessionId, input) => {
- stopped.push([sessionId, input]);
- },
- subscribeChanges: () => () => {},
- },
- projectName: () => 'Maka',
- newTurnId: () => 'unused',
});
- await adapter.stop({ sessionId: 'payment' }, 'turn-workhub');
-
- assert.deepEqual(stopped, [[
- 'payment',
- { source: 'stop_button', expectedTurnId: 'turn-workhub' },
- ]]);
+ const references = [{
+ delegationId: 'payment-delegation',
+ targetSessionId,
+ targetTurnId: 'admission-turn',
+ targetMessageId: 'payment-message',
+ }];
+ assert.deepEqual(await adapter.delegationFeedback(references), [{
+ delegationId: 'payment-delegation',
+ state: 'running',
+ }]);
});
test('desktop adapter derives stable origin evidence from the existing Session log', async () => {
@@ -782,6 +689,7 @@ test('desktop adapter derives stable origin evidence from the existing Session l
{ userPromptPreview: '把风险按高、中、低分组' },
];
},
+ queryMessageExecutions: noMessageExecutions,
create: async () => {
throw new Error('not used');
},
@@ -792,7 +700,6 @@ test('desktop adapter derives stable origin evidence from the existing Session l
subscribeChanges: () => () => {},
},
projectName: () => 'Maka',
- newTurnId: () => 'unused',
});
const first = await adapter.routingEvidence([{ sessionId: 'payment' }]);
diff --git a/apps/desktop/src/main/__tests__/workhub-surface-flow.test.ts b/apps/desktop/src/main/__tests__/workhub-surface-flow.test.ts
index b6f1eb03ec..98af7045e5 100644
--- a/apps/desktop/src/main/__tests__/workhub-surface-flow.test.ts
+++ b/apps/desktop/src/main/__tests__/workhub-surface-flow.test.ts
@@ -24,6 +24,7 @@ import { renderToStaticMarkup } from 'react-dom/server';
import { AstryxLocaleProvider, LocaleProvider } from '@maka/ui';
import {
WorkHubCoordinationStatus,
+ WorkHubCoordinationTurnView,
WorkHubProjectionRefreshGate,
WorkHubSurfaceRouteGate,
submitAndRecordWorkHubSurfaceInput,
@@ -34,9 +35,11 @@ import {
workHubSubmissionClearsDraft,
} from '../../renderer/workhub-surface.js';
import {
- createLegacyWorkHubControllerForTests as createWorkHubController,
+ createWorkHubController,
WORKHUB_ROUTING_STRATEGY_ID,
type WorkHubController,
+ type WorkHubCoordinationTurn,
+ type WorkHubDelegationExecutionState,
type WorkHubSubmitInput,
} from '../../renderer/workhub-controller.js';
import { WorkHubSendLease } from '../../renderer/workhub-send-lease.js';
@@ -107,6 +110,52 @@ test('Coordination lifecycle keeps a visible loading state and exposes failure r
assert.match(failed, />Retry</);
});
+test('durable delegation renders every projected target state as a navigable result', () => {
+ const states: Array<[WorkHubDelegationExecutionState, string]> = [
+ ['accepted', 'Accepted'],
+ ['running', 'Running'],
+ ['waiting_for_user', 'Waiting for you'],
+ ['completed', 'Completed'],
+ ['failed', 'Failed'],
+ ['aborted', 'Aborted'],
+ ['recovering', 'Recovering'],
+ ];
+ for (const [state, label] of states) {
+ const turn: WorkHubCoordinationTurn = {
+ messageId: 'assignment-1',
+ turnId: 'action-1',
+ text: 'Continue payments',
+ state: 'completed',
+ assignment: {
+ delegationId: 'delegation-1',
+ targetSessionId: 'payment',
+ targetSessionName: 'Payments',
+ targetMessageId: 'payment-message',
+ targetTurnId: 'payment-turn',
+ feedbackState: state,
+ },
+ updatedAt: 10,
+ };
+ const markup = renderToStaticMarkup(
+ createElement(LocaleProvider, {
+ locale: 'en',
+ children: createElement(AstryxLocaleProvider, {
+ children: createElement(WorkHubCoordinationTurnView, {
+ turn,
+ projection: { sessions: [], turns: [] },
+ locale: 'en',
+ onOpenSession: () => undefined,
+ }),
+ }),
+ }),
+ );
+ assert.match(markup, /<button/u);
+ assert.match(markup, /Payments/u);
+ assert.match(markup, new RegExp(label, 'u'));
+ assert.match(markup, new RegExp(`data-state="${state}"`, 'u'));
+ }
+});
+
test('surface projection refresh gate rejects older reads after a newer refresh starts', () => {
const gate = new WorkHubProjectionRefreshGate();
const first = gate.begin();
@@ -276,6 +325,34 @@ test('real Session projection creates new guide topics and preserves origin ambi
],
]);
const created: string[] = [];
+ const createSession = async ({ name }: { name: string }) => {
+ const id = name.includes('支付回调') ? 'payment' : 'layout';
+ const createdSession: WorkHubDesktopSession = {
+ id,
+ name: id === 'payment' ? '支付回调幂等性' : '移动端窄屏布局',
+ labels: [],
+ isArchived: false,
+ status: 'active',
+ projectId: 'project-maka',
+ lastMessageAt: ++clock,
+ };
+ created.push(id);
+ sessions.push(createdSession);
+ prompts.set(id, []);
+ return createdSession;
+ };
+ const send = async (
+ sessionId: string,
+ command: { type: 'send'; turnId: string; text: string },
+ ) => {
+ prompts.get(sessionId)?.push(command.text);
+ const target = sessions.find((candidate) => candidate.id === sessionId);
+ if (target) {
+ target.lastMessageAt = ++clock;
+ target.lastMessagePreview = command.text;
+ }
+ return { ok: true as const, turnId: command.turnId };
+ };
const port = createDesktopWorkHubSessionPort({
transcripts: {
open: async () => {
@@ -286,39 +363,68 @@ test('real Session projection creates new guide topics and preserves origin ambi
list: async () => sessions,
listTurns: async (sessionId) =>
(prompts.get(sessionId) ?? []).map((userPromptPreview) => ({ userPromptPreview })),
- create: async ({ name }) => {
- const id = name.includes('支付回调') ? 'payment' : 'layout';
- const session: WorkHubDesktopSession = {
- id,
- name: id === 'payment' ? '支付回调幂等性' : '移动端窄屏布局',
- labels: [],
- isArchived: false,
- status: 'active',
- projectId: 'project-maka',
- lastMessageAt: ++clock,
- };
- created.push(id);
- sessions.push(session);
- prompts.set(id, []);
- return session;
- },
- send: async (sessionId, command) => {
- prompts.get(sessionId)?.push(command.text);
- const session = sessions.find((candidate) => candidate.id === sessionId);
- if (session) {
- session.lastMessageAt = ++clock;
- session.lastMessagePreview = command.text;
- }
- return { ok: true, turnId: command.turnId };
- },
+ queryMessageExecutions: async () => ({ resolutions: [] }),
+ create: createSession,
+ send,
stop: async () => {},
subscribeChanges: () => () => {},
},
projectName: (projectId) =>
projectId === 'project-router' ? 'maka-workhub-session-router' : 'maka-agent',
- newTurnId: () => `turn-${clock + 1}`,
});
- const controller = createWorkHubController({ sessions: port });
+ const controller = createWorkHubController({
+ sessions: port,
+ coordination: {
+ open: async () => ({ close: async () => undefined }),
+ record: async (input) => ({ turnId: input.turnId }),
+ candidates: async () => ({
+ candidateSetId: `sha256:${'a'.repeat(64)}`,
+ candidates: sessions.map((entry) => ({
+ candidateRef: `candidate-${entry.id}`,
+ sessionId: entry.id,
+ sessionName: entry.name,
+ workspace: {
+ target: { kind: 'host_path' as const, path: `/workspace/${entry.id}` },
+ hostCwd: `/workspace/${entry.id}`,
+ },
+ state: entry.status,
+ updatedAt: entry.lastMessageAt ?? 0,
+ })),
+ }),
+ act: async (input) => {
+ if (input.proposal.disposition === 'answer_here') {
+ return { disposition: 'answer_here', coordinationTurnId: input.actionId };
+ }
+ if (input.proposal.disposition === 'clarify') {
+ return { disposition: 'clarify', coordinationTurnId: input.actionId };
+ }
+ if (input.proposal.disposition === 'create_new') {
+ const target = await createSession({ name: input.proposal.title });
+ const admitted = await send(target.id, {
+ type: 'send',
+ turnId: input.actionId,
+ text: input.userText,
+ });
+ return {
+ disposition: 'create_new',
+ targetSessionId: target.id,
+ targetTurnId: admitted.turnId,
+ };
+ }
+ const targetSessionId = input.proposal.candidateRef.replace(/^candidate-/u, '');
+ const admitted = await send(targetSessionId, {
+ type: 'send',
+ turnId: input.actionId,
+ text: input.userText,
+ });
+ return {
+ disposition: 'delegate_existing',
+ targetSessionId,
+ targetTurnId: admitted.turnId,
+ };
+ },
+ },
+ });
const payment = await controller.submit({
requestId: 'setup-payment',
diff --git a/apps/desktop/src/main/runtime-host-client.ts b/apps/desktop/src/main/runtime-host-client.ts
index 81e9775ec0..22aaf6b55a 100644
--- a/apps/desktop/src/main/runtime-host-client.ts
+++ b/apps/desktop/src/main/runtime-host-client.ts
@@ -1084,6 +1084,12 @@ export class DesktopRuntimeHostClient {
return this.request('turn.message.query', input);
}
+ queryMessageExecutions(
+ input: OperationInput<'turn.message.execution.query'>,
+ ): Promise<OperationOutput<'turn.message.execution.query'>> {
+ return this.request('turn.message.execution.query', input);
+ }
+
retractQueueEntry(
input: Omit<QueueEntryRetractInput, "originHostEpoch">,
): Promise<QueueMutationResult> {
diff --git a/apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts b/apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts
index d281173ac0..51e656253e 100644
--- a/apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts
+++ b/apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts
@@ -95,6 +95,7 @@ type RuntimeHostSessionExecutionClient = Pick<
| "interruptTurn"
| 'listSessionTurns'
| 'listSessionTurnLandmarks'
+ | 'queryMessageExecutions'
| 'queryMessages'
| "queryTurnResume"
| "readExecutionBoundary"
@@ -211,6 +212,14 @@ export function registerRuntimeHostSessionExecutionIpc(
},
);
+ ipcMain.handle(
+ 'sessions:queryMessageExecutions',
+ async (_event, sessionId: string, messageIds: unknown) => {
+ if (!Array.isArray(messageIds)) throw new Error('Invalid Message identities');
+ return deps.client.queryMessageExecutions({ sessionId, messageIds });
+ },
+ );
+
handleReconnectableRead(
ipcMain,
"sessions:observe",
diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts
index 92b170ecc2..94dcc8db3b 100644
--- a/apps/desktop/src/preload/bridge-contract.d.ts
+++ b/apps/desktop/src/preload/bridge-contract.d.ts
@@ -1018,6 +1018,10 @@ export interface MakaBridge {
sessionId: string,
messageIds: readonly string[],
): Promise<import('@maka/runtime-host/protocol').TurnMessageQueryResult>;
+ queryMessageExecutions(
+ sessionId: string,
+ messageIds: readonly string[],
+ ): Promise<import('@maka/runtime-host/protocol').TurnMessageExecutionQueryResult>;
retractQueueEntry(sessionId: string, entryId: string): Promise<void>;
promoteQueueEntry(sessionId: string, entryId: string): Promise<void>;
updateQueueEntry(
diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts
index f7b98cefc0..0ad27fe096 100644
--- a/apps/desktop/src/preload/preload.ts
+++ b/apps/desktop/src/preload/preload.ts
@@ -1790,6 +1790,9 @@ const makaBridge = {
queryCancelledMessages(sessionId, messageIds) {
return invokeSessionRuntimeHost('sessions:queryCancelledMessages', sessionId, messageIds);
},
+ queryMessageExecutions(sessionId, messageIds) {
+ return invokeSessionRuntimeHost('sessions:queryMessageExecutions', sessionId, messageIds);
+ },
retractQueueEntry(sessionId: string, entryId: string): Promise<void> {
return invokeSessionRuntimeHost('sessions:retractQueueEntry', sessionId, entryId);
},
diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx
index 67a61a1ea2..e390ab2775 100644
--- a/apps/desktop/src/renderer/app-shell.tsx
+++ b/apps/desktop/src/renderer/app-shell.tsx
@@ -1492,8 +1492,6 @@ function AppShellContent({
coordination: createDesktopWorkHubCoordinationPort({
sessionId: workHubCoordinationSessionId ?? 'workhub-coordination-unresolved',
transcripts: window.maka.transcripts,
- answer: (input) =>
- window.maka.workHub.answer(workHubCoordinationSessionId!, input),
record: (input) =>
window.maka.workHub.record(workHubCoordinationSessionId!, input),
candidates: () =>
@@ -1510,13 +1508,10 @@ function AppShellContent({
workHubCoordinationGenerationRef.current === workHubCoordinationGeneration &&
workHubCoordinationSessionIdRef.current === workHubCoordinationSessionId,
},
- (coordinationSessionId, input) =>
- window.maka.workHub.createSession(coordinationSessionId, input),
),
transcripts: window.maka.transcripts,
projectName: (projectId) =>
workHubProjectsRef.current.find((project) => project.id === projectId)?.name,
- newTurnId: () => crypto.randomUUID(),
}),
}),
[workHubCoordinationGeneration, workHubCoordinationSessionId],
diff --git a/apps/desktop/src/renderer/workhub-controller.ts b/apps/desktop/src/renderer/workhub-controller.ts
index a024b2df29..46a1b89a75 100644
--- a/apps/desktop/src/renderer/workhub-controller.ts
+++ b/apps/desktop/src/renderer/workhub-controller.ts
@@ -62,6 +62,28 @@ export type WorkHubSessionSummary = Omit<WorkHubSessionFacts, 'kind' | 'runningT
export type WorkHubProjectedTurnState = 'running' | 'completed' | 'aborted' | 'failed';
+export type WorkHubDelegationExecutionState =
+ | 'accepted'
+ | 'running'
+ | 'waiting_for_user'
+ | 'completed'
+ | 'failed'
+ | 'aborted'
+ | 'recovering';
+
+export interface WorkHubDelegationReference {
+ readonly delegationId: string;
+ readonly targetSessionId: string;
+ /** Stable delegated work identity; targetTurnId is only its admission location. */
+ readonly targetMessageId: string;
+ readonly targetTurnId: string;
+}
+
+export interface WorkHubDelegationFeedback {
+ readonly delegationId: string;
+ readonly state: WorkHubDelegationExecutionState;
+}
+
export interface WorkHubProjectedTurn {
messageId: string;
target: WorkHubSessionTarget;
@@ -79,8 +101,12 @@ export interface WorkHubCoordinationTurn {
state: WorkHubProjectedTurnState;
result?: string;
assignment?: {
+ readonly delegationId: string;
readonly targetSessionId: string;
readonly targetSessionName: string;
+ readonly targetMessageId: string;
+ readonly targetTurnId: string;
+ readonly feedbackState: WorkHubDelegationExecutionState;
};
updatedAt: number;
}
@@ -157,19 +183,19 @@ export type WorkHubSubmission = (
*/
export interface WorkHubSessionPort {
list(): Promise<WorkHubSessionFacts[]>;
- /**
- * Lists Sessions with per-target catalog coverage. A target missing from a
- * partial multi-Host list is not authoritatively absent.
- */
- listCatalog?(): Promise<{
- sessions: WorkHubSessionFacts[];
- isCompleteFor(target: WorkHubSessionTarget): boolean;
- }>;
/**
* Rebuilds a bounded recent conversation from the authoritative Session
* transcripts. Missing transcripts are omitted rather than copied elsewhere.
*/
recentTurns(targets: readonly WorkHubSessionTarget[]): Promise<WorkHubProjectedTurn[]>;
+ /**
+ * Rebuilds exact target-Turn execution facts for durable delegation links.
+ * The target Session remains authoritative; results are read-only and may
+ * conservatively report `recovering` while that authority is unavailable.
+ */
+ delegationFeedback(
+ references: readonly WorkHubDelegationReference[],
+ ): Promise<readonly WorkHubDelegationFeedback[]>;
/**
* Returns rebuildable routing evidence read from the authoritative Session
* log. Implementations must not persist a second writable copy of it.
@@ -177,22 +203,6 @@ export interface WorkHubSessionPort {
routingEvidence(
targets: readonly WorkHubSessionTarget[],
): Promise<Array<{ target: WorkHubSessionTarget; originPrompt?: string }>>;
- create(input: { name: string }): Promise<WorkHubSessionFacts>;
- reserveTurnId(): string;
- submit(
- target: WorkHubSessionTarget,
- text: string,
- turnId: string,
- ): Promise<{ turnId: string; steered?: true }>;
- reconcileSubmission(
- target: WorkHubSessionTarget,
- reservedTurnId: string,
- ): Promise<
- | { kind: 'root'; turnId: string }
- | { kind: 'steered' }
- | { kind: 'unknown' }
- >;
- stop(target: WorkHubSessionTarget, expectedTurnId: string): Promise<void>;
subscribe(handler: () => void): () => void;
}
@@ -201,7 +211,6 @@ export interface WorkHubCoordinationPort {
handler: (turns: readonly WorkHubCoordinationTurn[]) => void,
onError: (error: unknown) => void,
): Promise<{ close(): Promise<void> }>;
- answer(input: { turnId: string; text: string }): Promise<{ turnId: string }>;
record(input: {
turnId: string;
userText: string;
@@ -211,17 +220,6 @@ export interface WorkHubCoordinationPort {
act(input: Omit<WorkHubCoordinationActInput, 'create'>): Promise<WorkHubCoordinationActResult>;
}
-export class WorkHubSessionSubmitError extends Error {
- constructor(
- message: string,
- readonly admission: 'rejected' | 'unknown',
- options?: ErrorOptions,
- ) {
- super(message, options);
- this.name = 'WorkHubSessionSubmitError';
- }
-}
-
export interface WorkHubController {
read(input?: WorkHubReadInput): Promise<WorkHubProjection>;
submit(input: WorkHubSubmitInput): Promise<WorkHubSubmission>;
@@ -239,50 +237,14 @@ export interface WorkHubController {
resetVisitContext(): void;
}
-const MAX_TRACKED_WORKHUB_ROOTS = 32;
-
-interface WorkHubRootOwnership {
- order: number;
- turnId: string;
-}
-
-interface WorkHubPendingAdmission extends WorkHubRootOwnership {
- state: 'in_flight' | 'uncertain';
-}
-
-interface WorkHubOwnershipTombstone {
- order: number;
- stoppedTurnIds: Set<string>;
-}
-
export function createWorkHubController(deps: {
sessions: WorkHubSessionPort;
coordination: WorkHubCoordinationPort;
}): WorkHubController {
- return createWorkHubControllerImplementation(deps);
-}
-
-/** @internal Transitional R2.4 regression harness; application code must use the Action Gate. */
-export function createLegacyWorkHubControllerForTests(deps: {
- sessions: WorkHubSessionPort;
-}): WorkHubController {
- return createWorkHubControllerImplementation(deps);
-}
-
-function createWorkHubControllerImplementation(deps: {
- sessions: WorkHubSessionPort;
- coordination?: WorkHubCoordinationPort;
-}): WorkHubController {
- const coordination = deps.coordination ?? legacyTestCoordinationPort();
+ const { coordination } = deps;
let routePolicy = createWorkHubRoutePolicy();
let focusReadVersion = 0;
let pendingFocusReadVersion: number | undefined;
- const confirmedOwnershipBySessionId = new Map<string, WorkHubRootOwnership>();
- const pendingAdmissionsBySessionId = new Map<string, WorkHubPendingAdmission[]>();
- const ownershipTombstoneBySessionId = new Map<string, WorkHubOwnershipTombstone>();
- const stopAttemptByTurn = new Map<string, Promise<void>>();
- const stopOperationCountBySessionId = new Map<string, number>();
- let ownershipRevision = 0;
const reconcileFocus = (
policy: ReturnType<typeof createWorkHubRoutePolicy>,
sessions: readonly WorkHubSessionFacts[],
@@ -292,323 +254,77 @@ function createWorkHubControllerImplementation(deps: {
.sort((left, right) => right.updatedAt - left.updatedAt)
.map((session) => session.target));
};
- const correctionFor = (from: WorkHubSessionTarget): WorkHubCorrectionContext => {
- const confirmed = confirmedOwnershipBySessionId.get(from.sessionId);
- const pending = pendingAdmissionsBySessionId.get(from.sessionId);
- const turnId = confirmed?.turnId ?? pending?.at(-1)?.turnId;
- if (!turnId) return { from };
- return {
- from,
- turnId,
- };
- };
- const pendingAdmissions = (sessionId: string): WorkHubPendingAdmission[] =>
- pendingAdmissionsBySessionId.get(sessionId) ?? [];
- const setPendingAdmissions = (
- sessionId: string,
- pending: WorkHubPendingAdmission[],
- ) => {
- ownershipRevision += 1;
- if (pending.length === 0) {
- pendingAdmissionsBySessionId.delete(sessionId);
- return;
- }
- pendingAdmissionsBySessionId.set(
- sessionId,
- [...pending].sort((left, right) => left.order - right.order),
- );
- };
- const trackedRootCount = () => {
- let pendingCount = 0;
- for (const pending of pendingAdmissionsBySessionId.values()) {
- pendingCount += pending.length;
- }
- return confirmedOwnershipBySessionId.size + pendingCount;
- };
- const maybeRetireTombstone = (sessionId: string) => {
- const tombstone = ownershipTombstoneBySessionId.get(sessionId);
- if (!tombstone) return;
- if ((stopOperationCountBySessionId.get(sessionId) ?? 0) > 0) return;
- if (pendingAdmissions(sessionId).some((candidate) => candidate.order <= tombstone.order)) {
- return;
- }
- ownershipTombstoneBySessionId.delete(sessionId);
- };
- const readCatalog = async () => {
- const revisionAtStart = ownershipRevision;
- const catalog = deps.sessions.listCatalog
- ? await deps.sessions.listCatalog()
- : {
- sessions: await deps.sessions.list(),
- isCompleteFor: () => false,
- };
- return {
- catalog,
- // A catalog request that overlapped an ownership mutation may describe
- // the state before that mutation. It remains useful for projection, but
- // it must not authoritatively prune newer ownership or admissions.
- allowAuthoritativePruning: revisionAtStart === ownershipRevision,
- };
- };
- const reconcileConfirmedOwnership = (catalog: {
- sessions: readonly WorkHubSessionFacts[];
- isCompleteFor(target: WorkHubSessionTarget): boolean;
- }, allowAuthoritativePruning: boolean) => {
- if (!allowAuthoritativePruning) return;
- const { sessions } = catalog;
- const sessionById = new Map(sessions.map((session) => [session.target.sessionId, session]));
- for (const [sessionId, ownership] of confirmedOwnershipBySessionId) {
- const session = sessionById.get(sessionId);
- if (
- (!session && catalog.isCompleteFor({ sessionId })) ||
- session?.archived ||
- (session?.runningTurnIds !== undefined &&
- !session.runningTurnIds.includes(ownership.turnId))
- ) {
- if (confirmedOwnershipBySessionId.delete(sessionId)) {
- ownershipRevision += 1;
- }
- }
- }
- };
- const storeOwnershipTombstone = (
- sessionId: string,
- order: number,
- stoppedTurnIds: Iterable<string> = [],
- ) => {
- const existing = ownershipTombstoneBySessionId.get(sessionId);
- if (existing && existing.order > order) return;
- const stopped = new Set(existing?.order === order ? existing.stoppedTurnIds : []);
- for (const turnId of stoppedTurnIds) stopped.add(turnId);
- ownershipTombstoneBySessionId.set(sessionId, {
- order,
- stoppedTurnIds: stopped,
- });
- };
- const reserveOwnedRoot = (
- target: WorkHubSessionTarget,
- turnId: string,
- order: number,
- ) => {
- if (trackedRootCount() >= MAX_TRACKED_WORKHUB_ROOTS) {
- throw new Error('WorkHub has too many unresolved root submissions');
- }
- setPendingAdmissions(target.sessionId, [
- ...pendingAdmissions(target.sessionId),
- { order, turnId, state: 'in_flight' },
- ]);
- };
- const removePendingRoot = (
- target: WorkHubSessionTarget,
- reservedTurnId: string,
- order: number,
- ) => {
- setPendingAdmissions(
- target.sessionId,
- pendingAdmissions(target.sessionId).filter((candidate) =>
- candidate.order !== order || candidate.turnId !== reservedTurnId),
- );
- };
- const markPendingRootUncertain = (
- target: WorkHubSessionTarget,
- reservedTurnId: string,
- order: number,
- ) => {
- setPendingAdmissions(
- target.sessionId,
- pendingAdmissions(target.sessionId).map((candidate) =>
- candidate.order === order && candidate.turnId === reservedTurnId
- ? { ...candidate, state: 'uncertain' }
- : candidate),
- );
- };
- const attemptStop = (
- target: WorkHubSessionTarget,
- turnId: string,
- ): Promise<void> => {
- const key = `${target.sessionId}\0${turnId}`;
- const existing = stopAttemptByTurn.get(key);
- if (existing) return existing;
- stopOperationCountBySessionId.set(
- target.sessionId,
- (stopOperationCountBySessionId.get(target.sessionId) ?? 0) + 1,
- );
- const stopping = deps.sessions.stop(target, turnId).finally(() => {
- stopAttemptByTurn.delete(key);
- const remaining = (stopOperationCountBySessionId.get(target.sessionId) ?? 1) - 1;
- if (remaining === 0) {
- stopOperationCountBySessionId.delete(target.sessionId);
- } else {
- stopOperationCountBySessionId.set(target.sessionId, remaining);
- }
- });
- stopAttemptByTurn.set(key, stopping);
- return stopping;
- };
- const settleOwnedRoot = async (
- target: WorkHubSessionTarget,
- reservedTurnId: string,
- turn: { turnId: string; steered?: true },
- order: number,
- ) => {
- const tombstone = ownershipTombstoneBySessionId.get(target.sessionId);
- let stopped = false;
- let stopFailure: unknown;
- if (!turn.steered && tombstone && tombstone.order >= order) {
- stopped = tombstone.stoppedTurnIds.has(turn.turnId);
- if (!stopped) {
+ return {
+ async openConversation(handler, onError) {
+ let disposed = false;
+ let generation = 0;
+ let latestTurns: readonly WorkHubCoordinationTurn[] = [];
+
+ const refreshFeedback = async () => {
+ const refreshGeneration = ++generation;
+ const turns = latestTurns;
+ const references = turns.flatMap((turn) =>
+ turn.assignment
+ ? [{
+ delegationId: turn.assignment.delegationId,
+ targetSessionId: turn.assignment.targetSessionId,
+ targetMessageId: turn.assignment.targetMessageId,
+ targetTurnId: turn.assignment.targetTurnId,
+ }]
+ : [],
+ );
+ if (references.length === 0) return;
+ let feedback: readonly WorkHubDelegationFeedback[];
try {
- const priorStopAttempt = stopAttemptByTurn.get(
- `${target.sessionId}\0${turn.turnId}`,
- );
- if (priorStopAttempt) {
- try {
- await priorStopAttempt;
- } catch {
- // Admission is new evidence. Retry against the admitted root even
- // when the earlier pre-admission Stop failed or observed nothing.
- }
- }
- await attemptStop(target, turn.turnId);
- const currentBarrier = ownershipTombstoneBySessionId.get(target.sessionId);
- if (currentBarrier && currentBarrier.order >= order) {
- storeOwnershipTombstone(target.sessionId, currentBarrier.order, [turn.turnId]);
- }
- stopped = true;
- } catch (error) {
- stopFailure = error;
+ feedback = await deps.sessions.delegationFeedback(references);
+ } catch {
+ feedback = references.map(({ delegationId }) => ({
+ delegationId,
+ state: 'recovering',
+ }));
}
- }
- }
- removePendingRoot(target, reservedTurnId, order);
- if (!turn.steered && !stopped) {
- const confirmed = confirmedOwnershipBySessionId.get(target.sessionId);
- if (!confirmed || confirmed.order <= order) {
- confirmedOwnershipBySessionId.set(target.sessionId, {
- order,
- turnId: turn.turnId,
- });
- ownershipRevision += 1;
- }
- }
- maybeRetireTombstone(target.sessionId);
- if (stopFailure) throw stopFailure;
- };
- const releasePendingRoot = (
- target: WorkHubSessionTarget,
- reservedTurnId: string,
- order: number,
- ) => {
- removePendingRoot(target, reservedTurnId, order);
- maybeRetireTombstone(target.sessionId);
- };
- const reconcilePendingRoot = async (
- target: WorkHubSessionTarget,
- reservedTurnId: string,
- order: number,
- ): Promise<boolean> => {
- const reconciliation = await deps.sessions.reconcileSubmission(target, reservedTurnId);
- if (reconciliation.kind === 'unknown') return false;
- await settleOwnedRoot(
- target,
- reservedTurnId,
- reconciliation.kind === 'steered'
- ? { turnId: reservedTurnId, steered: true }
- : { turnId: reconciliation.turnId },
- order,
- );
- return true;
- };
- const reconcileUncertainAdmissions = (catalog: {
- sessions: readonly WorkHubSessionFacts[];
- isCompleteFor(target: WorkHubSessionTarget): boolean;
- }, allowAuthoritativePruning: boolean): Promise<void> | undefined => {
- const sessionById = new Map(
- catalog.sessions.map((session) => [session.target.sessionId, session]),
- );
- const uncertain = [...pendingAdmissionsBySessionId.entries()]
- .flatMap(([sessionId, pending]) => pending
- .filter((candidate) => candidate.state === 'uncertain')
- .map((candidate) => ({
- target: { sessionId },
- ...candidate,
- })));
- if (uncertain.length === 0) return undefined;
- return Promise.all(uncertain.map(async ({ target, turnId, order }) => {
- const session = sessionById.get(target.sessionId);
- if (
- allowAuthoritativePruning &&
- (session?.archived || (!session && catalog.isCompleteFor(target)))
- ) {
- releasePendingRoot(target, turnId, order);
- return;
- }
- try {
- await reconcilePendingRoot(target, turnId, order);
- } catch {
- // Failed reconciliation preserves the pending or confirmed ownership.
- }
- })).then(() => undefined);
- };
- const assertSubmissionBarrierOpen = (target: WorkHubSessionTarget) => {
- maybeRetireTombstone(target.sessionId);
- const tombstone = ownershipTombstoneBySessionId.get(target.sessionId);
- const stopCount = stopOperationCountBySessionId.get(target.sessionId) ?? 0;
- const pendingBarrier = tombstone && pendingAdmissions(target.sessionId)
- .some((candidate) => candidate.order <= tombstone.order);
- if (stopCount > 0 || pendingBarrier) {
- throw new Error('WorkHub is still reconciling a correction for this Session');
- }
- };
- const stopOwnedRoots = async (
- correction: WorkHubCorrectionContext,
- order: number,
- ) => {
- if (correction.steered) return;
- const confirmed = confirmedOwnershipBySessionId.get(correction.from.sessionId);
- const pending = pendingAdmissions(correction.from.sessionId);
- const turnIds = new Set<string>();
- const unconfirmedTurnIds = new Set<string>();
- if (correction.turnId) turnIds.add(correction.turnId);
- if (confirmed && confirmed.order < order) {
- turnIds.add(confirmed.turnId);
- }
- for (const candidate of pending) {
- if (candidate.order < order) {
- turnIds.add(candidate.turnId);
- unconfirmedTurnIds.add(candidate.turnId);
- }
- }
- if (turnIds.size === 0) return;
- // Publish only the order barrier before awaiting Host acknowledgements.
- // Individual IDs become tombstoned only after their Stop succeeds.
- storeOwnershipTombstone(correction.from.sessionId, order);
- const failures: unknown[] = [];
- await Promise.all([...turnIds].map(async (turnId) => {
+ if (disposed || refreshGeneration !== generation || turns !== latestTurns) return;
+ const feedbackByDelegationId = new Map(
+ feedback.map((entry) => [entry.delegationId, entry]),
+ );
+ handler(turns.map((turn) => {
+ if (!turn.assignment) return turn;
+ const next = feedbackByDelegationId.get(turn.assignment.delegationId);
+ return next
+ ? { ...turn, assignment: { ...turn.assignment, feedbackState: next.state } }
+ : turn;
+ }));
+ };
+
+ const unsubscribe = deps.sessions.subscribe(() => {
+ void refreshFeedback();
+ });
+ let handle: { close(): Promise<void> } | undefined;
try {
- await attemptStop(correction.from, turnId);
- const barrier = ownershipTombstoneBySessionId.get(correction.from.sessionId);
- if (barrier && barrier.order >= order && !unconfirmedTurnIds.has(turnId)) {
- storeOwnershipTombstone(correction.from.sessionId, barrier.order, [turnId]);
- }
- const owned = confirmedOwnershipBySessionId.get(correction.from.sessionId);
- if (owned && owned.order < order && owned.turnId === turnId) {
- confirmedOwnershipBySessionId.delete(correction.from.sessionId);
- ownershipRevision += 1;
- }
+ handle = await coordination.open((turns) => {
+ if (disposed) return;
+ latestTurns = turns;
+ generation += 1;
+ // The atomic assignment is already durable acknowledgement, so emit
+ // it immediately before enriching it with target-owned lifecycle.
+ handler(turns);
+ void refreshFeedback();
+ }, onError);
} catch (error) {
- failures.push(error);
+ unsubscribe();
+ throw error;
}
- }));
- maybeRetireTombstone(correction.from.sessionId);
- if (failures.length > 0) throw failures[0];
- };
- return {
- openConversation(handler, onError) {
- return coordination.open(handler, onError);
+ return {
+ async close() {
+ disposed = true;
+ generation += 1;
+ unsubscribe();
+ await handle?.close();
+ },
+ };
},
async recordConversationTurn(input) {
- if (deps.coordination && input.disposition === 'clarify') {
+ if (input.disposition === 'clarify') {
const result = await coordination.act({
actionId: input.turnId,
userText: input.userText,
@@ -640,15 +356,7 @@ function createWorkHubControllerImplementation(deps: {
readPolicy.rememberTarget(input.focus);
}
try {
- const { catalog, allowAuthoritativePruning } =
- await readCatalog();
- reconcileConfirmedOwnership(catalog, allowAuthoritativePruning);
- const reconciliation = reconcileUncertainAdmissions(
- catalog,
- allowAuthoritativePruning,
- );
- if (reconciliation) await reconciliation;
- const facts = catalog.sessions;
+ const facts = await deps.sessions.list();
const ordinary = facts
.filter((session) => session.kind === 'ordinary')
.sort((left, right) => right.updatedAt - left.updatedAt);
@@ -674,31 +382,17 @@ function createWorkHubControllerImplementation(deps: {
},
async submit(input) {
const submissionPolicy = routePolicy;
- // Reserve the order synchronously, before any await. Corrections are
- // learned only after successful delivery, but their precedence follows
- // user submission order rather than network completion order.
- const submissionOrder = submissionPolicy.reserveSubmissionOrder();
- if (deps.coordination && input.correction) {
+ if (input.correction) {
throw new Error(
'WorkHub linked correction requires persistent delegation support',
);
}
- const { catalog, allowAuthoritativePruning } =
- await readCatalog();
- reconcileConfirmedOwnership(catalog, allowAuthoritativePruning);
- const reconciliation = reconcileUncertainAdmissions(
- catalog,
- allowAuthoritativePruning,
- );
- if (reconciliation) await reconciliation;
- const sessions = catalog.sessions;
+ const sessions = await deps.sessions.list();
reconcileFocus(submissionPolicy, sessions);
const ordinary = sessions.filter((session) => session.kind === 'ordinary');
- const candidateSet = deps.coordination
- ? await coordination.candidates()
- : undefined;
+ const candidateSet = await coordination.candidates();
const candidateBySessionId = new Map(
- candidateSet?.candidates.map((candidate) => [candidate.sessionId, candidate]),
+ candidateSet.candidates.map((candidate) => [candidate.sessionId, candidate]),
);
// Archived Sessions remain visible as historical work, but Runtime Host
// rejects new root Turns for them. In production the Runtime-owned
@@ -706,7 +400,7 @@ function createWorkHubControllerImplementation(deps: {
const routable = ordinary.filter(
(session) =>
!session.archived &&
- (!candidateSet || candidateBySessionId.has(session.target.sessionId)),
+ candidateBySessionId.has(session.target.sessionId),
);
const routingEvidence = input.explicitTarget
? []
@@ -720,14 +414,11 @@ function createWorkHubControllerImplementation(deps: {
...(input.explicitTarget ? { explicitTarget: input.explicitTarget } : {}),
});
if (decision.kind === 'clarification') {
- if (deps.coordination && decision.correctedFrom) {
+ if (decision.correctedFrom) {
throw new Error(
'WorkHub linked correction requires persistent delegation support',
);
}
- const correction = decision.correctedFrom
- ? correctionFor(decision.correctedFrom)
- : undefined;
return {
kind: 'clarification',
strategyId: WORKHUB_ROUTING_STRATEGY_ID,
@@ -738,22 +429,14 @@ function createWorkHubControllerImplementation(deps: {
projectName: session.projectName,
sessionName: session.sessionName,
})),
- ...(correction ? { correction } : {}),
};
}
if (decision.kind === 'discussion') {
- if (candidateSet) {
- await coordination.act({
- actionId: input.requestId,
- userText: input.text,
- proposal: { disposition: 'answer_here' },
- });
- } else {
- await coordination.answer({
- turnId: input.requestId,
- text: input.text,
- });
- }
+ await coordination.act({
+ actionId: input.requestId,
+ userText: input.text,
+ proposal: { disposition: 'answer_here' },
+ });
return {
kind: 'discussion',
strategyId: WORKHUB_ROUTING_STRATEGY_ID,
@@ -761,17 +444,12 @@ function createWorkHubControllerImplementation(deps: {
text: input.text,
};
}
- let target: WorkHubSessionTarget;
- let evidence: Extract<WorkHubSubmission, { kind: 'submitted' }>['evidence'];
- const correction = input.correction ?? (decision.kind === 'target' && decision.correctedFrom
- ? correctionFor(decision.correctedFrom)
- : undefined);
- if (deps.coordination && correction) {
+ if (decision.kind === 'target' && decision.correctedFrom) {
throw new Error(
'WorkHub linked correction requires persistent delegation support',
);
}
- if (candidateSet && decision.kind === 'new_session') {
+ if (decision.kind === 'new_session') {
const admitted = await coordination.act({
actionId: input.requestId,
userText: input.text,
@@ -783,7 +461,7 @@ function createWorkHubControllerImplementation(deps: {
if (admitted.disposition !== 'create_new') {
throw new Error('WorkHub Action Gate returned an unexpected disposition');
}
- target = { sessionId: admitted.targetSessionId };
+ const target = { sessionId: admitted.targetSessionId };
submissionPolicy.rememberTarget(target);
return {
kind: 'submitted',
@@ -795,21 +473,11 @@ function createWorkHubControllerImplementation(deps: {
evidence: 'new_session',
};
}
- if (decision.kind === 'new_session') {
- const created = await deps.sessions.create({ name: workHubNewSessionName(input.text) });
- if (created.kind !== 'ordinary') {
- throw new Error('WorkHub can only create ordinary Sessions');
- }
- target = created.target;
- evidence = 'new_session';
- } else {
- target = decision.target;
- evidence = correction ? 'route_correction' : decision.evidence;
- }
+ const target = decision.target;
const targetSession = routable.find(
(session) => session.target.sessionId === target.sessionId,
);
- if (!targetSession && evidence !== 'new_session') {
+ if (!targetSession) {
throw new Error('WorkHub target Session is unavailable');
}
if (targetSession?.state === 'waiting_for_user' && !input.retryAction) {
@@ -821,76 +489,33 @@ function createWorkHubControllerImplementation(deps: {
target,
};
}
- if (candidateSet) {
- const candidate = candidateBySessionId.get(target.sessionId);
- if (!candidate) {
- throw new Error('WorkHub target Session is unavailable');
- }
- const action: WorkHubCoordinationActInput = {
- actionId: input.requestId,
- userText: input.text,
- candidateSetId: candidateSet.candidateSetId,
- proposal: {
- disposition: 'delegate_existing',
- candidateRef: candidate.candidateRef,
- },
- };
- const admitted = await coordination.act(action);
- if (admitted.disposition !== 'delegate_existing') {
- throw new Error('WorkHub Action Gate returned an unexpected disposition');
- }
- target = { sessionId: admitted.targetSessionId };
- submissionPolicy.rememberTarget(target);
- return {
- kind: 'submitted',
- strategyId: WORKHUB_ROUTING_STRATEGY_ID,
- requestId: input.requestId,
- target,
- turnId: admitted.targetTurnId,
- ...(admitted.steered ? { steered: true as const } : {}),
- evidence,
- };
- }
- if (correction) {
- await stopOwnedRoots(correction, submissionOrder);
- }
- assertSubmissionBarrierOpen(target);
- const reservedTurnId = deps.sessions.reserveTurnId();
- reserveOwnedRoot(target, reservedTurnId, submissionOrder);
- let turn: { turnId: string; steered?: true };
- try {
- turn = await deps.sessions.submit(target, input.text, reservedTurnId);
- } catch (error) {
- if (
- error instanceof WorkHubSessionSubmitError &&
- error.admission === 'rejected'
- ) {
- releasePendingRoot(target, reservedTurnId, submissionOrder);
- } else {
- markPendingRootUncertain(target, reservedTurnId, submissionOrder);
- try {
- await reconcilePendingRoot(target, reservedTurnId, submissionOrder);
- } catch {
- // The original delivery error remains primary. Reconciliation keeps
- // any unresolved admission reachable for a later read/correction.
- }
- }
- throw error;
+ const candidate = candidateBySessionId.get(target.sessionId);
+ if (!candidate) {
+ throw new Error('WorkHub target Session is unavailable');
}
- await settleOwnedRoot(target, reservedTurnId, turn, submissionOrder);
- submissionPolicy.rememberTarget(target);
- if (correction) {
- submissionPolicy.rememberCorrection(input.text, target, submissionOrder);
+ const action: WorkHubCoordinationActInput = {
+ actionId: input.requestId,
+ userText: input.text,
+ candidateSetId: candidateSet.candidateSetId,
+ proposal: {
+ disposition: 'delegate_existing',
+ candidateRef: candidate.candidateRef,
+ },
+ };
+ const admitted = await coordination.act(action);
+ if (admitted.disposition !== 'delegate_existing') {
+ throw new Error('WorkHub Action Gate returned an unexpected disposition');
}
+ const admittedTarget = { sessionId: admitted.targetSessionId };
+ submissionPolicy.rememberTarget(admittedTarget);
return {
kind: 'submitted',
strategyId: WORKHUB_ROUTING_STRATEGY_ID,
requestId: input.requestId,
- target,
- turnId: turn.turnId,
- ...(turn.steered ? { steered: true as const } : {}),
- evidence,
- ...(correction ? { correctedFrom: correction.from } : {}),
+ target: admittedTarget,
+ turnId: admitted.targetTurnId,
+ ...(admitted.steered ? { steered: true as const } : {}),
+ evidence: decision.evidence,
};
},
resetVisitContext() {
@@ -900,24 +525,3 @@ function createWorkHubControllerImplementation(deps: {
},
};
}
-
-function legacyTestCoordinationPort(): WorkHubCoordinationPort {
- return {
- async open(handler) {
- handler([]);
- return { close: async () => undefined };
- },
- async answer(input) {
- return { turnId: input.turnId };
- },
- async record(input) {
- return { turnId: input.turnId };
- },
- async candidates() {
- throw new Error('The legacy WorkHub test adapter does not expose Action Gate candidates');
- },
- async act() {
- throw new Error('The legacy WorkHub test adapter does not expose Action Gate actions');
- },
- };
-}
diff --git a/apps/desktop/src/renderer/workhub-coordination-host-scope.ts b/apps/desktop/src/renderer/workhub-coordination-host-scope.ts
index 55ebdb1c9e..e6277688ab 100644
--- a/apps/desktop/src/renderer/workhub-coordination-host-scope.ts
+++ b/apps/desktop/src/renderer/workhub-coordination-host-scope.ts
@@ -18,10 +18,7 @@
*/
import { parseDesktopSessionKey } from '../shared/runtime-host-identity.js';
-import type {
- WorkHubCoordinationHostSessionCreator,
- WorkHubDesktopSessionBridge,
-} from './workhub-session-port.js';
+import type { WorkHubDesktopSessionBridge } from './workhub-session-port.js';
export interface WorkHubCoordinationHostAuthority {
readonly sessionId: string | undefined;
@@ -29,11 +26,10 @@ export interface WorkHubCoordinationHostAuthority {
readonly isCurrent: () => boolean;
}
-/** Restricts the transitional WorkHub router to the Coordination Session's Host. */
+/** Restricts WorkHub's read-only Session projection to the Coordination Session's Host. */
export function scopeWorkHubSessionsToCoordinationHost(
sessions: WorkHubDesktopSessionBridge,
coordination: WorkHubCoordinationHostAuthority,
- createOnCoordinationHost: WorkHubCoordinationHostSessionCreator,
): WorkHubDesktopSessionBridge {
const coordinationSessionId = coordination.sessionId;
const hostId = (() => {
@@ -74,20 +70,9 @@ export function scopeWorkHubSessionsToCoordinationHost(
requireTargetHost(sessionId);
return await sessions.listTurns(sessionId);
},
- async create(input: { name: string }) {
- requireHost();
- return await createOnCoordinationHost(coordinationSessionId!, input);
- },
- async send(sessionId: string, command: { type: 'send'; turnId: string; text: string }) {
- requireTargetHost(sessionId);
- return sessions.send(sessionId, command);
- },
- async stop(
- sessionId: string,
- input?: { source?: 'stop_button'; expectedTurnId?: string },
- ) {
+ async queryMessageExecutions(sessionId: string, messageIds: readonly string[]) {
requireTargetHost(sessionId);
- await sessions.stop(sessionId, input);
+ return await sessions.queryMessageExecutions(sessionId, messageIds);
},
subscribeChanges: (handler: () => void) => sessions.subscribeChanges(handler),
} satisfies WorkHubDesktopSessionBridge;
diff --git a/apps/desktop/src/renderer/workhub-coordination-port.ts b/apps/desktop/src/renderer/workhub-coordination-port.ts
index 05e4a1c421..7869470c76 100644
--- a/apps/desktop/src/renderer/workhub-coordination-port.ts
+++ b/apps/desktop/src/renderer/workhub-coordination-port.ts
@@ -54,7 +54,6 @@ export class WorkHubCoordinationFailure extends Error {
export function createDesktopWorkHubCoordinationPort(deps: {
sessionId: string;
transcripts: WorkHubDesktopTranscriptBridge;
- answer(input: { turnId: string; text: string }): Promise<{ turnId: string }>;
record(input: {
turnId: string;
userText: string;
@@ -66,7 +65,6 @@ export function createDesktopWorkHubCoordinationPort(deps: {
): Promise<OperationOutcome<'workhub.coordination.act'>>;
}): WorkHubCoordinationPort {
return {
- answer: deps.answer,
record: deps.record,
candidates: deps.candidates,
async act(input) {
@@ -128,8 +126,12 @@ export function projectWorkHubCoordinationTurns(
text: boundedWorkHubTimelineText(message.userText),
state: 'completed',
assignment: {
+ delegationId: message.delegationId,
targetSessionId: message.targetSessionId,
targetSessionName: message.targetSessionName,
+ targetMessageId: message.targetMessageId,
+ targetTurnId: message.targetTurnId,
+ feedbackState: 'accepted',
},
updatedAt: message.ts,
});
diff --git a/apps/desktop/src/renderer/workhub-route-policy.ts b/apps/desktop/src/renderer/workhub-route-policy.ts
index 907892e730..4dd073bf7a 100644
--- a/apps/desktop/src/renderer/workhub-route-policy.ts
+++ b/apps/desktop/src/renderer/workhub-route-policy.ts
@@ -54,8 +54,6 @@ export interface WorkHubRoutePolicy {
initializeFocus(targets: readonly WorkHubSessionTarget[]): void;
newVisit(): WorkHubRoutePolicy;
rememberTarget(target: WorkHubSessionTarget): void;
- reserveSubmissionOrder(): number;
- rememberCorrection(text: string, target: WorkHubSessionTarget, order: number): void;
}
export function workHubNewSessionName(text: string): string {
@@ -75,20 +73,7 @@ export function workHubNewSessionName(text: string): string {
return firstClause?.slice(0, 48) || '新工作';
}
-interface RouteCorrection {
- text: string;
- target: WorkHubSessionTarget;
- sequence: number;
-}
-
-interface RouteCorrectionMemory {
- corrections: RouteCorrection[];
- sequence: number;
-}
-
-const MAX_ROUTE_CORRECTIONS = 32;
const MIN_EXACT_SESSION_NAME_LENGTH = 2;
-const MIN_CORRECTION_TERM_LENGTH = 3;
// One four-character Han phrase is usually a meaningful entity rather than
// grammar; Latin needs either two whole-word matches or one distinctive word.
const MIN_STRONG_HAN_MATCH_LENGTH = 4;
@@ -104,15 +89,12 @@ const MAX_RELATED_CLARIFICATION_OPTIONS = 4;
* execution state, and recovery continue to come from the Session port.
*/
export function createWorkHubRoutePolicy(): WorkHubRoutePolicy {
- return createWorkHubRoutePolicyVisit({ corrections: [], sequence: 0 });
+ return createWorkHubRoutePolicyVisit();
}
-function createWorkHubRoutePolicyVisit(
- correctionMemory: RouteCorrectionMemory,
-): WorkHubRoutePolicy {
+function createWorkHubRoutePolicyVisit(): WorkHubRoutePolicy {
let currentFocus: WorkHubSessionTarget | undefined;
let previousFocus: WorkHubSessionTarget | undefined;
- const corrections = correctionMemory.corrections;
return {
resolve({ text, sessions, originPromptBySessionId, explicitTarget }) {
@@ -201,11 +183,6 @@ function createWorkHubRoutePolicyVisit(
return { kind: 'clarification', options: options.slice(0, MAX_UNCERTAINTY_OPTIONS) };
}
- const corrected = correctedTarget(text, sessions, corrections);
- if (corrected) {
- return { kind: 'target', target: corrected, evidence: 'route_correction' };
- }
-
const previousReference = looksLikePreviousFocus(text);
const currentReference = !previousReference && looksLikeRecentFocus(text);
const focusCandidate = previousReference
@@ -280,22 +257,13 @@ function createWorkHubRoutePolicyVisit(
}
},
newVisit() {
- return createWorkHubRoutePolicyVisit(correctionMemory);
+ return createWorkHubRoutePolicyVisit();
},
rememberTarget(target) {
if (currentFocus?.sessionId === target.sessionId) return;
previousFocus = currentFocus;
currentFocus = target;
},
- reserveSubmissionOrder() {
- correctionMemory.sequence += 1;
- return correctionMemory.sequence;
- },
- rememberCorrection(text, target, order) {
- corrections.push({ text, target, sequence: order });
- corrections.sort((left, right) => right.sequence - left.sequence);
- corrections.splice(MAX_ROUTE_CORRECTIONS);
- },
};
}
@@ -316,30 +284,6 @@ function rankExactSessions(
.sort((left, right) => right.matchLength - left.matchLength);
}
-function correctedTarget(
- text: string,
- sessions: WorkHubSessionFacts[],
- corrections: readonly RouteCorrection[],
-): WorkHubSessionTarget | undefined {
- const queryTerms = new Set(routingTerms(text));
- if (queryTerms.size === 0) return undefined;
- const available = new Set(sessions.map((session) => session.target.sessionId));
- const ranked = corrections
- .filter((correction) => available.has(correction.target.sessionId))
- .map((correction) => {
- const matches = routingTerms(correction.text).filter((term) => queryTerms.has(term));
- return {
- correction,
- score: matches.reduce((total, term) => total + term.length, 0),
- longestMatch: matches.reduce((longest, term) => Math.max(longest, term.length), 0),
- };
- })
- .filter(({ longestMatch }) => longestMatch >= MIN_CORRECTION_TERM_LENGTH)
- .sort((left, right) =>
- right.score - left.score || right.correction.sequence - left.correction.sequence);
- return ranked[0]?.correction.target;
-}
-
function normalizeIdentityText(value: string): string {
return value.toLocaleLowerCase().replace(/[\s\p{P}\p{S}]+/gu, '');
}
diff --git a/apps/desktop/src/renderer/workhub-session-port.ts b/apps/desktop/src/renderer/workhub-session-port.ts
index 0fa255bc34..9bf586d3bd 100644
--- a/apps/desktop/src/renderer/workhub-session-port.ts
+++ b/apps/desktop/src/renderer/workhub-session-port.ts
@@ -17,7 +17,11 @@
* under the License.
*/
-import { deriveTurnRecords, type StoredMessage } from '@maka/core/session';
+import {
+ deriveTurnRecords,
+ type StoredMessage,
+ type TurnRecord,
+} from '@maka/core/session';
import type {
DesktopTranscriptBatch,
DesktopTranscriptHandle,
@@ -26,15 +30,14 @@ import { parseDesktopSessionKey } from '../shared/runtime-host-identity.js';
import { DesktopTranscriptRangeStore } from './desktop-transcript-range-store.js';
import type {
WorkHubProjectedTurn,
+ WorkHubDelegationFeedback,
+ WorkHubDelegationReference,
WorkHubSessionFacts,
WorkHubSessionPort,
WorkHubSessionState,
WorkHubSessionTarget,
} from './workhub-controller.js';
-import {
- boundedWorkHubTimelineText,
- WorkHubSessionSubmitError,
-} from './workhub-controller.js';
+import { boundedWorkHubTimelineText } from './workhub-controller.js';
export interface WorkHubDesktopSession {
id: string;
@@ -57,26 +60,22 @@ export interface WorkHubDesktopSessionBridge {
sessions: readonly WorkHubDesktopSession[];
completeHostIds: readonly string[];
}>;
- listTurns(sessionId: string): Promise<readonly { userPromptPreview?: string }[]>;
- create(input: { name: string }): Promise<WorkHubDesktopSession>;
- send(
+ listTurns(
sessionId: string,
- command: { type: 'send'; turnId: string; text: string },
- ): Promise<
- { ok: true; turnId: string; steered?: true } | { ok: false; reason: string }
- >;
- stop(
+ ): Promise<readonly Partial<Pick<TurnRecord, 'turnId' | 'status' | 'statusSource' | 'userPromptPreview'>>[]>;
+ queryMessageExecutions(
sessionId: string,
- input?: { source?: 'stop_button'; expectedTurnId?: string },
- ): Promise<unknown>;
+ messageIds: readonly string[],
+ ): Promise<{
+ readonly resolutions: readonly (
+ | { messageId: string; state: 'pending' }
+ | { messageId: string; state: 'cancelled' }
+ | { messageId: string; state: 'owned'; turnId: string; runId: string }
+ )[];
+ }>;
subscribeChanges(handler: () => void): () => void;
}
-export type WorkHubCoordinationHostSessionCreator = (
- coordinationSessionId: string,
- input: { name: string },
-) => Promise<WorkHubDesktopSession>;
-
export interface WorkHubDesktopTranscriptBridge {
open(
sessionId: string,
@@ -89,11 +88,12 @@ const WORKHUB_TIMELINE_SESSION_LIMIT = 10;
const WORKHUB_TIMELINE_TURN_LIMIT = 40;
const WORKHUB_TRANSCRIPT_READY_TIMEOUT_MS = 5_000;
-export function createDesktopWorkHubSessionPort(deps: {
- sessions: WorkHubDesktopSessionBridge;
+export function createDesktopWorkHubSessionPort<
+ Sessions extends WorkHubDesktopSessionBridge,
+>(deps: {
+ sessions: Sessions;
transcripts: WorkHubDesktopTranscriptBridge;
projectName(projectId: string): string | undefined;
- newTurnId(): string;
}): WorkHubSessionPort {
// The first prompt is immutable Session-log evidence. This cache is only a
// rebuildable read optimization; it is never an authority or a write path.
@@ -146,9 +146,6 @@ export function createDesktopWorkHubSessionPort(deps: {
async list() {
return (await projectCatalog()).sessions;
},
- listCatalog() {
- return projectCatalog();
- },
async recentTurns(targets) {
const turnsBySession = await Promise.all(
targets.slice(0, WORKHUB_TIMELINE_SESSION_LIMIT).map(async (target) => {
@@ -171,6 +168,82 @@ export function createDesktopWorkHubSessionPort(deps: {
)
.slice(-WORKHUB_TIMELINE_TURN_LIMIT);
},
+ async delegationFeedback(references) {
+ let catalog: Awaited<ReturnType<typeof projectCatalog>> | undefined;
+ try {
+ catalog = await projectCatalog();
+ } catch {
+ // Exact Turn reads below may still recover terminal or running facts.
+ }
+ const sessionById = new Map(
+ catalog?.sessions.map((session) => [session.target.sessionId, session]) ?? [],
+ );
+ const referencesBySessionId = new Map<string, WorkHubDelegationReference[]>();
+ for (const reference of references) {
+ const grouped = referencesBySessionId.get(reference.targetSessionId) ?? [];
+ grouped.push(reference);
+ referencesBySessionId.set(reference.targetSessionId, grouped);
+ }
+ const projected = await Promise.all(
+ [...referencesBySessionId.entries()].map(async ([sessionId, grouped]) => {
+ let turns: readonly Partial<
+ Pick<TurnRecord, 'turnId' | 'status' | 'statusSource' | 'userPromptPreview'>
+ >[] = [];
+ let turnReadFailed = false;
+ try {
+ turns = await deps.sessions.listTurns(sessionId);
+ } catch {
+ turnReadFailed = true;
+ }
+ let executionReadFailed = false;
+ let resolutions: readonly (
+ | { messageId: string; state: 'pending' }
+ | { messageId: string; state: 'cancelled' }
+ | { messageId: string; state: 'owned'; turnId: string; runId: string }
+ )[] = [];
+ try {
+ const result = await deps.sessions.queryMessageExecutions(
+ sessionId,
+ grouped.map(({ targetMessageId }) => targetMessageId),
+ );
+ resolutions = result.resolutions;
+ } catch {
+ executionReadFailed = true;
+ }
+ const turnById = new Map(
+ turns.flatMap((turn) => turn.turnId ? [[turn.turnId, turn] as const] : []),
+ );
+ const resolutionByMessageId = new Map(
+ resolutions.map((resolution) => [resolution.messageId, resolution]),
+ );
+ const session = sessionById.get(sessionId);
+ return grouped.map((reference): WorkHubDelegationFeedback => {
+ const resolution = resolutionByMessageId.get(reference.targetMessageId);
+ const executionTurnId = resolution?.state === 'owned'
+ ? resolution.turnId
+ : undefined;
+ return {
+ delegationId: reference.delegationId,
+ state: projectDelegationExecutionState({
+ resolutionState: resolution?.state,
+ executionTurnId,
+ session,
+ turn: executionTurnId ? turnById.get(executionTurnId) : undefined,
+ turnReadFailed,
+ executionReadFailed,
+ }),
+ };
+ });
+ }),
+ );
+ const feedbackByDelegationId = new Map(
+ projected.flat().map((feedback) => [feedback.delegationId, feedback]),
+ );
+ return references.flatMap((reference) => {
+ const feedback = feedbackByDelegationId.get(reference.delegationId);
+ return feedback ? [feedback] : [];
+ });
+ },
async routingEvidence(targets) {
return Promise.all(targets.map(async (target) => {
const cached = originPromptCache.get(target.sessionId);
@@ -189,72 +262,6 @@ export function createDesktopWorkHubSessionPort(deps: {
}
}));
},
- async create({ name }) {
- return projectSession(await deps.sessions.create({ name }));
- },
- reserveTurnId() {
- return deps.newTurnId();
- },
- async submit(target: WorkHubSessionTarget, text: string, turnId: string) {
- let result: Awaited<ReturnType<WorkHubDesktopSessionBridge['send']>>;
- try {
- result = await deps.sessions.send(target.sessionId, {
- type: 'send',
- turnId,
- text,
- });
- } catch (cause) {
- throw new WorkHubSessionSubmitError(
- 'WorkHub Session delivery outcome is unknown',
- 'unknown',
- { cause },
- );
- }
- if (!result.ok) {
- // `outcome_unknown` is the Host declining to prove what happened, not a
- // refusal: the Message may already be running, so it stays reachable
- // for reconciliation rather than being released.
- throw new WorkHubSessionSubmitError(
- `WorkHub Session send failed: ${result.reason}`,
- result.reason === 'outcome_unknown' ? 'unknown' : 'rejected',
- );
- }
- return {
- turnId: result.turnId,
- ...(result.steered ? { steered: true as const } : {}),
- };
- },
- async reconcileSubmission(target, reservedTurnId) {
- try {
- const messages = await readWorkHubSessionMessages(deps.transcripts, target);
- let message: Extract<StoredMessage, { type: 'user' }> | undefined;
- for (let index = messages.length - 1; index >= 0; index -= 1) {
- const entry = messages[index];
- if (
- entry?.type === 'user' &&
- (entry.turnId === reservedTurnId || entry.id === reservedTurnId)
- ) {
- message = entry;
- break;
- }
- }
- if (!message) return { kind: 'unknown' };
- if (message.turnId === reservedTurnId) {
- return { kind: 'root', turnId: message.turnId };
- }
- return message.steeringEventId
- ? { kind: 'steered' }
- : { kind: 'root', turnId: message.turnId };
- } catch {
- return { kind: 'unknown' };
- }
- },
- async stop(target, expectedTurnId) {
- await deps.sessions.stop(target.sessionId, {
- source: 'stop_button',
- expectedTurnId,
- });
- },
subscribe(handler) {
return deps.sessions.subscribeChanges(handler);
},
@@ -353,3 +360,31 @@ function projectState(session: WorkHubDesktopSession): WorkHubSessionState {
}
return session.status;
}
+
+function projectDelegationExecutionState(input: {
+ resolutionState: 'pending' | 'cancelled' | 'owned' | undefined;
+ executionTurnId: string | undefined;
+ session: WorkHubSessionFacts | undefined;
+ turn: Partial<Pick<TurnRecord, 'turnId' | 'status' | 'statusSource'>> | undefined;
+ turnReadFailed: boolean;
+ executionReadFailed: boolean;
+}): WorkHubDelegationFeedback['state'] {
+ const { executionTurnId, session, turn } = input;
+ if (input.executionReadFailed) return 'recovering';
+ if (!input.resolutionState) return 'recovering';
+ if (input.resolutionState === 'cancelled') return 'aborted';
+ if (input.resolutionState === 'pending') return 'accepted';
+ if (!executionTurnId) return 'recovering';
+ if (turn?.statusSource === 'recorded' && turn.status && turn.status !== 'running') {
+ return turn.status;
+ }
+ const liveTurnIds = session?.runningTurnIds;
+ const ownsLiveTurn = liveTurnIds?.includes(executionTurnId) === true;
+ if (ownsLiveTurn && session?.state === 'waiting_for_user') return 'waiting_for_user';
+ if (ownsLiveTurn) return 'running';
+ if (liveTurnIds === undefined && turn?.statusSource === 'recorded' && turn.status === 'running') {
+ return 'running';
+ }
+ if (input.turnReadFailed || !session) return 'recovering';
+ return 'accepted';
+}
diff --git a/apps/desktop/src/renderer/workhub-surface.tsx b/apps/desktop/src/renderer/workhub-surface.tsx
index fbaea31fa6..99f78c1948 100644
--- a/apps/desktop/src/renderer/workhub-surface.tsx
+++ b/apps/desktop/src/renderer/workhub-surface.tsx
@@ -405,10 +405,12 @@ export function WorkHubSurface(props: {
<div className="workhub-empty" role="alert">{copy.loadFailed}</div>
) : null}
{visibleCoordinationTurns.map((turn) => (
- <CoordinationTurnView
+ <WorkHubCoordinationTurnView
key={turn.messageId}
turn={turn}
- copy={copy}
+ projection={projection}
+ locale={props.locale}
+ onOpenSession={props.onOpenSession}
/>
))}
{visibleLocalTurns.map((turn) => (
@@ -542,23 +544,44 @@ function WorkHubLoadingState(props: { label: string }) {
);
}
-function CoordinationTurnView(props: {
+/** @internal Presentational seam for durable Coordination turns. */
+export function WorkHubCoordinationTurnView(props: {
turn: WorkHubCoordinationTurn;
- copy: ReturnType<typeof workHubCopy>;
+ projection: WorkHubProjection;
+ locale: UiLocale;
+ onOpenSession(sessionId: string): void;
}) {
+ const copy = workHubCopy(props.locale);
+ const assignment = props.turn.assignment;
+ const session = assignment
+ ? props.projection.sessions.find(
+ (candidate) => candidate.target.sessionId === assignment.targetSessionId,
+ )
+ : undefined;
return (
- <WorkHubMessageFrame text={props.turn.text} state={props.turn.state} projected>
- {props.turn.assignment ? (
- <p className="workhub-result">
- {props.copy.sentTo} {props.turn.assignment.targetSessionName} · {props.copy.accepted}
- </p>
+ <WorkHubMessageFrame
+ text={props.turn.text}
+ state={assignment?.feedbackState ?? props.turn.state}
+ projected
+ >
+ {assignment ? (
+ <SubmittedWorkView
+ session={session}
+ targetSessionId={assignment.targetSessionId}
+ fallbackName={assignment.targetSessionName}
+ heading={copy.sentTo}
+ state={copy.delegationStates[assignment.feedbackState]}
+ result={undefined}
+ copy={copy}
+ onOpenSession={props.onOpenSession}
+ />
) : props.turn.result ? (
<p className="workhub-result">{props.turn.result}</p>
) : props.turn.state === 'running' ? (
- <p className="workhub-status" role="status">{props.copy.answering}</p>
+ <p className="workhub-status" role="status">{copy.answering}</p>
) : (
<p className="workhub-error" role="alert">
- {props.copy.turnStates[props.turn.state]}
+ {copy.turnStates[props.turn.state]}
</p>
)}
</WorkHubMessageFrame>
@@ -689,6 +712,7 @@ function WorkHubMessageFrame(props: {
function SubmittedWorkView(props: {
session: WorkHubSessionSummary | undefined;
targetSessionId: string;
+ fallbackName?: string;
heading: string;
state: string;
result: string | undefined;
@@ -696,17 +720,18 @@ function SubmittedWorkView(props: {
onOpenSession(sessionId: string): void;
}) {
const { session, copy } = props;
+ const sessionName = session?.sessionName ?? props.fallbackName ?? copy.sessionFallback;
return (
<div className="workhub-submitted">
<p>{props.heading}</p>
<Button
- label={`${session?.sessionName ?? copy.sessionFallback}, ${props.state}`}
+ label={`${sessionName}, ${props.state}`}
variant="ghost"
width="100%"
onClick={() => props.onOpenSession(props.targetSessionId)}
endContent={<span className="workhub-submitted-state">{props.state}</span>}>
<span className="workhub-submitted-session">
- <strong>{session?.sessionName ?? copy.sessionFallback}</strong>
+ <strong>{sessionName}</strong>
{session?.projectName ? <small>{session.projectName}</small> : null}
</span>
</Button>
@@ -747,6 +772,15 @@ function workHubCopy(locale: UiLocale) {
delivery_failed: '输入未能送达,请重试。',
}, scrollToBottom: '滚动到底部', archived: '已归档',
states: { active: '活跃', running: '进行中', waiting_for_user: '等待你', blocked: '受阻', aborted: '已中止' },
+ delegationStates: {
+ accepted: '已接收',
+ running: '进行中',
+ waiting_for_user: '等待你',
+ completed: '已完成',
+ failed: '失败',
+ aborted: '已中止',
+ recovering: '正在恢复',
+ },
turnStates: { running: '进行中', completed: '已完成', aborted: '已中止', failed: '失败' },
} as const;
}
@@ -780,6 +814,15 @@ function workHubCopy(locale: UiLocale) {
delivery_failed: 'The input could not be delivered. Try again.',
}, scrollToBottom: 'Scroll to bottom', archived: 'Archived',
states: { active: 'Active', running: 'Running', waiting_for_user: 'Waiting for you', blocked: 'Blocked', aborted: 'Aborted' },
+ delegationStates: {
+ accepted: 'Accepted',
+ running: 'Running',
+ waiting_for_user: 'Waiting for you',
+ completed: 'Completed',
+ failed: 'Failed',
+ aborted: 'Aborted',
+ recovering: 'Recovering',
+ },
turnStates: { running: 'Running', completed: 'Completed', aborted: 'Aborted', failed: 'Failed' },
} as const;
}
diff --git a/docs/architecture/workhub-coordination-session-adr.md b/docs/architecture/workhub-coordination-session-adr.md
index eade3fbfb9..7b49723565 100644
--- a/docs/architecture/workhub-coordination-session-adr.md
+++ b/docs/architecture/workhub-coordination-session-adr.md
@@ -98,6 +98,7 @@ transcripts, such as:
delegationId
coordinationTurnId
targetSessionId
+targetMessageId
targetTurnId
disposition
```
@@ -131,6 +132,22 @@ so WorkHub does not own a second recovery state machine or compensation chain.
The `delegation_assigned` record itself projects the visible WorkHub turn; the
renderer does not append a second summary.
+The first-response contract is hybrid. The atomic `delegation_assigned` record is
+an immediate durable acknowledgement, so WorkHub confirms acceptance without
+waiting for target execution. The target Message is the stable delegation
+identity; `targetTurnId` records only its admission location. WorkHub asks the
+target Message authority which Turn durably consumed or admitted that Message,
+then joins the resolved Turn's recorded lifecycle and the target Session's exact
+live-Turn membership to project `running`, `waiting_for_user`, `completed`,
+`failed`, and `aborted`. This remains correct when an unconsumed steering Message
+is folded into a successor Turn or recovery aggregates several pending Messages
+under one new Turn. A durable cancellation tombstone for a retracted queued
+Message resolves the delegation to `aborted`. If the target authority is
+temporarily unreadable, WorkHub projects `recovering` rather than inventing a
+terminal result. These execution states are never appended as mutable Coordination
+records; Session change notifications invalidate the projection and opening
+WorkHub after restart rebuilds it from the same link and target facts.
+
The renderer persists only a Host-scoped action id until acknowledgement. Composer
draft text uses a separate storage key and lifecycle. A reload therefore preserves
idempotency without freezing old text or coupling draft edits to Host authority.
@@ -156,7 +173,8 @@ been committed.
- Coordination Session role representation, lazy creation, durable lookup,
recovery, per-Host UI resolution, persistent transcript, closed dispositions,
and the Action Gate are implemented. Durable delegation linkage is encoded in
- that transcript; target lifecycle projection, linked correction, and destructive
+ that transcript; target lifecycle projection and the hybrid first-response
+ contract are implemented as rebuildable reads. Linked correction and destructive
replacement/Stop recovery remain later work.
Reevaluate the per-Host decision if supported workflows require one WorkHub
diff --git a/packages/runtime-host/src/__tests__/message-coordinator.test.ts b/packages/runtime-host/src/__tests__/message-coordinator.test.ts
index 48f24ad74a..9b30146285 100644
--- a/packages/runtime-host/src/__tests__/message-coordinator.test.ts
+++ b/packages/runtime-host/src/__tests__/message-coordinator.test.ts
@@ -130,6 +130,58 @@ test('consumes an atomically committed active-target admission exactly once', as
assert.equal(fixture.drainRequests(), 0);
});
+test('idle recovery resolves differently preassigned Messages to their shared successor Turn', async () => {
+ const fixture = createFixture();
+ fixture.setRootState({ kind: 'idle' });
+ for (const [messageId, turnId, runId] of [
+ ['workhub-message-a', 'preassigned-turn-a', 'preassigned-run-a'],
+ ['workhub-message-b', 'preassigned-turn-b', 'preassigned-run-b'],
+ ] as const) {
+ const content = { text: `recover ${messageId}` };
+ await fixture.admissions.commitMessageAdmission({
+ sessionId: ROOT.sessionId,
+ turnId,
+ runId,
+ messageId,
+ content,
+ submittedContentDigest: messageContentDigest(content),
+ submittedPlacement: 'current_turn',
+ placement: 'current_turn',
+ disposition: 'steering',
+ admittedAt: 10,
+ });
+ }
+
+ await fixture.coordinator.consumePendingAdmissions([ROOT.sessionId]);
+ const resolved = await fixture.coordinator.handlers['turn.message.execution.query'](
+ {
+ sessionId: ROOT.sessionId,
+ messageIds: ['workhub-message-a', 'workhub-message-b'],
+ },
+ operationContext(),
+ );
+
+ assert.deepEqual(resolved, {
+ ok: true,
+ result: {
+ resolutions: [
+ {
+ messageId: 'workhub-message-a',
+ state: 'owned',
+ turnId: 'recovered-turn',
+ runId: 'durable-run',
+ },
+ {
+ messageId: 'workhub-message-b',
+ state: 'owned',
+ turnId: 'recovered-turn',
+ runId: 'durable-run',
+ },
+ ],
+ },
+ });
+});
+
test('idle submit starts exactly one root Turn and retry identity is connection-independent', async () => {
const fixture = createFixture();
fixture.setRootState({ kind: 'idle' });
@@ -202,27 +254,75 @@ test('message query reports only durable cancellation proof', async () => {
await submit(fixture, 'cancelled-message', 'discard me', 'next_turn');
await submit(fixture, 'accepted-message', 'waiting', 'next_turn');
await fixture.coordinator.cancelMessages(ROOT.sessionId, ['cancelled-message']);
+ const result = await fixture.coordinator.handlers['turn.message.query'](
+ {
+ sessionId: ROOT.sessionId,
+ messageIds: ['cancelled-message', 'accepted-message', 'unknown-message'],
+ },
+ operationContext(),
+ );
+
+ assert.deepEqual(result, {
+ ok: true,
+ result: { cancelledMessageIds: ['cancelled-message'] },
+ });
+});
+
+test('message execution query reports the Turn that durably owns each Message', async () => {
+ const fixture = createFixture();
+ const pendingContent = { text: 'not handed off yet' };
+ await fixture.admissions.commitMessageAdmission({
+ ...ROOT,
+ messageId: 'pending-message',
+ content: pendingContent,
+ submittedContentDigest: messageContentDigest(pendingContent),
+ submittedPlacement: 'current_turn',
+ placement: 'current_turn',
+ disposition: 'steering',
+ admittedAt: 10,
+ });
fixture.receipts.set(
'handed-off-message',
- sourceReceipt('handed-off-message', 'delivered', 'current_turn', 'steering'),
+ sourceReceipt(
+ 'handed-off-message',
+ 'delivered by successor',
+ 'current_turn',
+ 'steering',
+ 'successor-turn',
+ ),
);
+ fixture.events.push(steeringEvent('steered-message', 'consumed by admission Turn'));
- const result = await fixture.coordinator.handlers['turn.message.query'](
+ const result = await fixture.coordinator.handlers['turn.message.execution.query'](
{
sessionId: ROOT.sessionId,
- messageIds: [
- 'cancelled-message',
- 'accepted-message',
- 'handed-off-message',
- 'unknown-message',
- ],
+ messageIds: ['pending-message', 'handed-off-message', 'steered-message', 'unknown-message'],
},
operationContext(),
);
assert.deepEqual(result, {
ok: true,
- result: { cancelledMessageIds: ['cancelled-message'] },
+ result: {
+ resolutions: [
+ {
+ messageId: 'pending-message',
+ state: 'pending',
+ },
+ {
+ messageId: 'handed-off-message',
+ state: 'owned',
+ turnId: 'successor-turn',
+ runId: 'durable-run',
+ },
+ {
+ messageId: 'steered-message',
+ state: 'owned',
+ turnId: ROOT.turnId,
+ runId: ROOT.runId,
+ },
+ ],
+ },
});
});
@@ -743,6 +843,18 @@ test('entry retract removes one queued entry, replays its outcome, and rejects s
fixture.coordinator.projection(ROOT.sessionId).steering.map((entry) => entry.messageId),
['steer-1'],
);
+ assert.deepEqual(
+ await fixture.coordinator.handlers['turn.message.execution.query'](
+ { sessionId: ROOT.sessionId, messageIds: ['follow-1'] },
+ operationContext(),
+ ),
+ {
+ ok: true,
+ result: {
+ resolutions: [{ messageId: 'follow-1', state: 'cancelled' }],
+ },
+ },
+ );
const retry = await fixture.coordinator.handlers['queue.entry.retract'](
{
diff --git a/packages/runtime-host/src/__tests__/protocol.test.ts b/packages/runtime-host/src/__tests__/protocol.test.ts
index 8c4b8e3f2b..1c5241a5e9 100644
--- a/packages/runtime-host/src/__tests__/protocol.test.ts
+++ b/packages/runtime-host/src/__tests__/protocol.test.ts
@@ -333,6 +333,10 @@ describe('Runtime Host bootstrap protocol', () => {
assert.ok(RUNTIME_HOST_COMPATIBILITY_EPOCH > 50);
});
+ test('publishes a new compatibility epoch for Message execution ownership', () => {
+ assert.ok(RUNTIME_HOST_COMPATIBILITY_EPOCH > 61);
+ });
+
test('publishes a new compatibility epoch for exact Session Connection identity', () => {
assert.ok(RUNTIME_HOST_COMPATIBILITY_EPOCH > 56);
});
@@ -1185,9 +1189,14 @@ describe('Runtime Host bootstrap protocol', () => {
operation: 'turn.message.query' as const,
input: {
sessionId: 'session-1',
- messageIds: ['message-1', 'message-2'],
+ messageIds: ['message-1', 'message-2', 'message-3'],
},
};
+ const executionQuery = {
+ requestId: 'execution-query-request-1',
+ operation: 'turn.message.execution.query' as const,
+ input: query.input,
+ };
const submit = {
requestId: 'submit-request-1',
operation: 'turn.message.submit' as const,
@@ -1216,6 +1225,36 @@ describe('Runtime Host bootstrap protocol', () => {
},
};
assert.deepEqual(decodeClientFrame(query), query);
+ assert.deepEqual(decodeClientFrame(executionQuery), executionQuery);
+ const queried = {
+ requestId: executionQuery.requestId,
+ operation: executionQuery.operation,
+ ok: true as const,
+ result: {
+ resolutions: [
+ { messageId: 'message-1', state: 'pending' as const },
+ {
+ messageId: 'message-2',
+ state: 'owned' as const,
+ turnId: 'turn-2',
+ runId: 'run-2',
+ },
+ { messageId: 'message-3', state: 'cancelled' as const },
+ ],
+ },
+ };
+ assert.deepEqual(decodeHostFrame(queried), queried);
+ assert.throws(
+ () =>
+ decodeHostFrame({
+ ...queried,
+ result: {
+ ...queried.result,
+ resolutions: [...queried.result.resolutions, ...queried.result.resolutions],
+ },
+ }),
+ isInvalidFrame,
+ );
assert.deepEqual(decodeClientFrame(submit), submit);
assert.deepEqual(decodeClientFrame(retract), retract);
assert.deepEqual(decodeClientFrame(interrupt), interrupt);
diff --git a/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts b/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts
index 08eb929d6c..a0f2f62842 100644
--- a/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts
+++ b/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts
@@ -55,7 +55,7 @@ import type {
BackendCompactHistoryInput,
BackendSendInput,
} from '@maka/core/backend-types';
-import type { SessionEvent } from '@maka/core/events';
+import { messageContentDigest, type SessionEvent } from '@maka/core/events';
import {
WORKHUB_COORDINATION_SESSION_ID,
WORKHUB_COORDINATION_SESSION_ROLE,
diff --git a/packages/runtime-host/src/protocol/index.ts b/packages/runtime-host/src/protocol/index.ts
index 253c42fb20..a359bcdefd 100644
--- a/packages/runtime-host/src/protocol/index.ts
+++ b/packages/runtime-host/src/protocol/index.ts
@@ -94,7 +94,9 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1 as const;
export const RUNTIME_HOST_PROTOCOL_VERSION = 0 as const;
// Increment when the same protocol version no longer guarantees safe Client-Host
// interoperability. Mismatches are rejected before domain commands are admitted.
-export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 66 as const;
+export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 67 as const;
+// 67: Message lifecycle queries expose durable execution ownership and
+// cancellation. Older peers cannot decode or provide the closed proof list.
// 66: Peer Mesh queries expose one canonical transit selection and runtime metrics.
// 65: live `tool_start` frames may carry optional `intent` / `argsPreview`
// keys. Older Clients decode the event with a strict allowed-key list and tear
diff --git a/packages/runtime-host/src/protocol/message.ts b/packages/runtime-host/src/protocol/message.ts
index ea4df35d27..c30546427c 100644
--- a/packages/runtime-host/src/protocol/message.ts
+++ b/packages/runtime-host/src/protocol/message.ts
@@ -123,6 +123,25 @@ export interface TurnMessageQueryResult {
readonly cancelledMessageIds: readonly string[];
}
+export interface TurnMessageExecutionQueryInput {
+ readonly sessionId: string;
+ readonly messageIds: readonly string[];
+}
+
+export interface TurnMessageExecutionQueryResult {
+ readonly resolutions: readonly TurnMessageExecutionResolution[];
+}
+
+export type TurnMessageExecutionResolution =
+ | { readonly messageId: string; readonly state: 'pending' }
+ | { readonly messageId: string; readonly state: 'cancelled' }
+ | {
+ readonly messageId: string;
+ readonly state: 'owned';
+ readonly turnId: string;
+ readonly runId: string;
+ };
+
export interface QueueRetractInput {
readonly originHostEpoch: string;
readonly sessionId: string;
@@ -202,6 +221,13 @@ export const MESSAGE_OPERATION_SPECS = {
decodeInput: decodeTurnMessageQueryInput,
decodeOutput: decodeTurnMessageQueryResult,
}),
+ 'turn.message.execution.query': defineOperation({
+ mode: 'query',
+ availability: 'ready',
+ errors: MESSAGE_OPERATION_ERRORS,
+ decodeInput: decodeTurnMessageExecutionQueryInput,
+ decodeOutput: decodeTurnMessageExecutionQueryResult,
+ }),
'turn.message.submit': defineOperation({
mode: 'command',
availability: 'ready',
@@ -341,6 +367,59 @@ function decodeTurnMessageQueryResult(value: unknown): TurnMessageQueryResult {
return { cancelledMessageIds };
}
+function decodeTurnMessageExecutionQueryInput(value: unknown): TurnMessageExecutionQueryInput {
+ return decodeTurnMessageQueryInput(value);
+}
+
+function decodeTurnMessageExecutionQueryResult(value: unknown): TurnMessageExecutionQueryResult {
+ const record = requireExactRecord(value, 'turn.message.execution.query result', ['resolutions']);
+ if (!Array.isArray(record.resolutions) || record.resolutions.length > MESSAGE_QUEUE_MAX_ENTRIES) {
+ throw invalidProtocolFrame('Invalid turn.message.execution.query resolutions');
+ }
+ const resolutions = record.resolutions.map((value): TurnMessageExecutionResolution => {
+ const resolution = requireRecord(value, 'turn.message.execution.query resolution');
+ if (resolution.state === 'pending') {
+ assertExactKeys(resolution, 'turn.message.execution.query pending resolution', [
+ 'messageId',
+ 'state',
+ ]);
+ return {
+ messageId: requireEntityId(resolution.messageId, 'messageId'),
+ state: 'pending',
+ };
+ }
+ if (resolution.state === 'cancelled') {
+ assertExactKeys(resolution, 'turn.message.execution.query cancelled resolution', [
+ 'messageId',
+ 'state',
+ ]);
+ return {
+ messageId: requireEntityId(resolution.messageId, 'messageId'),
+ state: 'cancelled',
+ };
+ }
+ if (resolution.state === 'owned') {
+ assertExactKeys(resolution, 'turn.message.execution.query owned resolution', [
+ 'messageId',
+ 'state',
+ 'turnId',
+ 'runId',
+ ]);
+ return {
+ messageId: requireEntityId(resolution.messageId, 'messageId'),
+ state: 'owned',
+ turnId: requireEntityId(resolution.turnId, 'turnId'),
+ runId: requireEntityId(resolution.runId, 'runId'),
+ };
+ }
+ throw invalidProtocolFrame('Invalid turn.message.execution.query resolution state');
+ });
+ if (new Set(resolutions.map(({ messageId }) => messageId)).size !== resolutions.length) {
+ throw invalidProtocolFrame('Duplicate turn.message.execution.query messageId');
+ }
+ return { resolutions };
+}
+
function decodeTurnMessageSubmitResult(value: unknown): TurnMessageSubmitResult {
const record = requireRecord(value, 'turn.message.submit result');
if (record.disposition === 'turn_started') {
diff --git a/packages/runtime-host/src/protocol/operations.ts b/packages/runtime-host/src/protocol/operations.ts
index d9176c2657..392347d4f7 100644
--- a/packages/runtime-host/src/protocol/operations.ts
+++ b/packages/runtime-host/src/protocol/operations.ts
@@ -317,6 +317,7 @@ export const REMOTE_OWNER_OPERATION_GRANTS = Object.freeze([
'subscription.open',
'task.ledger.query',
'turn.interrupt',
+ 'turn.message.execution.query',
'turn.message.query',
'turn.message.submit',
'turn.query',
diff --git a/packages/runtime-host/src/server/message-coordinator.ts b/packages/runtime-host/src/server/message-coordinator.ts
index 1db629328d..0f3ec3f083 100644
--- a/packages/runtime-host/src/server/message-coordinator.ts
+++ b/packages/runtime-host/src/server/message-coordinator.ts
@@ -338,6 +338,7 @@ const HOST_EPOCH_PATTERN = /^[A-Za-z0-9_-]{1,128}$/u;
export class HostMessageCoordinator implements RuntimeMessageAuthority {
readonly handlers: MessageOperationHandlerMap = {
'turn.message.query': (input) => this.queryMessages(input),
+ 'turn.message.execution.query': (input) => this.queryMessageExecutions(input),
'turn.message.submit': (input, context) => this.submit(input, context),
'queue.retract': (input) => this.retract(input),
'queue.entry.retract': (input) => this.retractQueuedEntry(input),
@@ -411,6 +412,71 @@ export class HostMessageCoordinator implements RuntimeMessageAuthority {
return success({ cancelledMessageIds });
}
+ async queryMessageExecutions(input: {
+ sessionId: string;
+ messageIds: readonly string[];
+ }): Promise<
+ MessageOutcome<{
+ resolutions: Array<
+ | { messageId: string; state: 'pending' }
+ | { messageId: string; state: 'cancelled' }
+ | { messageId: string; state: 'owned'; turnId: string; runId: string }
+ >;
+ }>
+ > {
+ const resolutions: Array<
+ | { messageId: string; state: 'pending' }
+ | { messageId: string; state: 'cancelled' }
+ | { messageId: string; state: 'owned'; turnId: string; runId: string }
+ > = [];
+ for (const messageId of input.messageIds) {
+ const receipt = await this.#durableProof.readRootTurnSourceMessageReceipt(
+ input.sessionId,
+ messageId,
+ );
+ if (
+ receipt?.admission.sessionId === input.sessionId &&
+ receipt.sourceMessage.messageId === messageId
+ ) {
+ // A root source receipt is the latest durable ownership proof and
+ // therefore outranks the steering location from which a Message may
+ // have been folded into this successor.
+ resolutions.push({
+ messageId,
+ state: 'owned',
+ turnId: receipt.admission.turnId,
+ runId: receipt.admission.runId,
+ });
+ continue;
+ }
+ const steering = await this.#durableProof.readImmutableSteeringMessageProof(
+ input.sessionId,
+ messageId,
+ );
+ if (
+ steering?.event.sessionId === input.sessionId &&
+ steering.event.refs?.providerEventId === messageId
+ ) {
+ resolutions.push({
+ messageId,
+ state: 'owned',
+ turnId: steering.event.turnId,
+ runId: steering.event.runId,
+ });
+ continue;
+ }
+ if (await this.#admissions.hasCancelledMessageAdmission(input.sessionId, messageId)) {
+ resolutions.push({ messageId, state: 'cancelled' });
+ continue;
+ }
+ const pending = await this.#admissions.readMessageAdmission(input.sessionId, messageId);
+ if (pending?.sessionId === input.sessionId && pending.messageId === messageId) {
+ resolutions.push({ messageId, state: 'pending' });
+ }
+ }
+ return success({ resolutions });
+ }
+
retireSessions(sessionIds: readonly string[]): void {
for (const sessionId of new Set(sessionIds)) {
const state = this.#sessions.get(sessionId);
diff --git a/packages/runtime-host/src/server/operation-dispatcher.ts b/packages/runtime-host/src/server/operation-dispatcher.ts
index 8208c5ad08..77af9e17a5 100644
--- a/packages/runtime-host/src/server/operation-dispatcher.ts
+++ b/packages/runtime-host/src/server/operation-dispatcher.ts
@@ -90,6 +90,7 @@ export type ConnectionEffectOperationKey = Extract<
export type MessageOperationKey = Extract<
OperationKey,
| 'turn.message.query'
+ | 'turn.message.execution.query'
| 'turn.message.submit'
| 'queue.retract'
| 'queue.entry.retract'
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org