Skip to content

Commit bf55aab

Browse files
committed
fix(workhub): follow delegated message ownership
Generated-by: Codex
1 parent db50370 commit bf55aab

21 files changed

Lines changed: 502 additions & 30 deletions

apps/desktop/src/main/__tests__/runtime-host-session-execution-ipc-main.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,37 @@ test('returns Host-owned cancellation proof to the renderer', async () => {
630630
);
631631
});
632632

633+
test('returns Host-owned Message execution proof to the renderer', async () => {
634+
const ipc = ipcHarness();
635+
registerExecutionIpc(
636+
{
637+
client: executionClient({
638+
queryMessageExecutions: async (input) => ({
639+
resolutions: input.messageIds.map((messageId) => ({
640+
messageId,
641+
state: 'owned' as const,
642+
turnId: 'successor-turn',
643+
runId: 'successor-run',
644+
})),
645+
}),
646+
}),
647+
},
648+
ipc,
649+
);
650+
651+
assert.deepEqual(
652+
await ipc.invoke('sessions:queryMessageExecutions', 'session-1', ['message-delegated']),
653+
{
654+
resolutions: [{
655+
messageId: 'message-delegated',
656+
state: 'owned',
657+
turnId: 'successor-turn',
658+
runId: 'successor-run',
659+
}],
660+
},
661+
);
662+
});
663+
633664
test('submits a slash Skill message and reports the Host Skill outcome', async () => {
634665
const submits: unknown[] = [];
635666
const ipc = ipcHarness();
@@ -1502,6 +1533,7 @@ function executionClient(overrides: Partial<ExecutionClient>): ExecutionClient {
15021533
interruptTurn: unavailable,
15031534
listSessionTurnLandmarks: unavailable,
15041535
listSessionTurns: unavailable,
1536+
queryMessageExecutions: unavailable,
15051537
queryMessages: unavailable,
15061538
queryTurnResume: unavailable,
15071539
readExecutionBoundary: unavailable,

apps/desktop/src/main/__tests__/workhub-controller.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ function coordinationAssignmentTurn(): WorkHubCoordinationTurn {
103103
delegationId: 'delegation-1',
104104
targetSessionId: 'payment',
105105
targetSessionName: 'Payments',
106+
targetMessageId: 'payment-message',
106107
targetTurnId: 'payment-turn',
107108
feedbackState: 'accepted',
108109
},

apps/desktop/src/main/__tests__/workhub-coordination-host-scope.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ test('WorkHub candidates follow the resolved Coordination Session Host only', as
4444
completeHostIds: ['host-a', 'host-b'],
4545
}),
4646
listTurns: async () => [],
47+
queryMessageExecutions: async () => ({ resolutions: [] }),
4748
create: async () => {
4849
throw new Error('unscoped create must not be used');
4950
},

apps/desktop/src/main/__tests__/workhub-session-port.test.ts

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ const unusedTranscripts = {
5656
},
5757
};
5858

59+
const noMessageExecutions = async () => ({
60+
resolutions: [] as Array<
61+
| { messageId: string; state: 'pending' }
62+
| { messageId: string; state: 'owned'; turnId: string; runId: string }
63+
>,
64+
});
65+
5966
function transcriptsWith(messages: readonly StoredMessage[]) {
6067
return {
6168
open: async (sessionId: string, handler: (batch: DesktopTranscriptBatch) => void) => {
@@ -151,6 +158,7 @@ test('projects the durable Coordination transcript into the WorkHub conversation
151158
delegationId: 'payments-delegation',
152159
targetSessionId: 'payments',
153160
targetSessionName: 'Payments',
161+
targetMessageId: 'payments-message',
154162
targetTurnId: 'payments-turn',
155163
feedbackState: 'accepted',
156164
},
@@ -294,6 +302,7 @@ test('desktop adapter rebuilds recent turns from the Session transcript and clos
294302
sessions: {
295303
list: async () => [],
296304
listTurns: async () => [],
305+
queryMessageExecutions: noMessageExecutions,
297306
create: async () => {
298307
throw new Error('not used');
299308
},
@@ -369,6 +378,7 @@ test('desktop adapter cancels an unavailable transcript without hiding ready Ses
369378
sessions: {
370379
list: async () => [],
371380
listTurns: async () => [],
381+
queryMessageExecutions: noMessageExecutions,
372382
create: async () => { throw new Error('not used'); },
373383
send: async () => { throw new Error('not used'); },
374384
stop: async () => {},
@@ -451,6 +461,7 @@ test('desktop adapter projects Session catalog facts without owning copies', asy
451461
sessions: {
452462
list: async () => source,
453463
listTurns: async () => [],
464+
queryMessageExecutions: noMessageExecutions,
454465
create: async () => {
455466
throw new Error('not used');
456467
},
@@ -509,7 +520,7 @@ test('desktop adapter projects Session catalog facts without owning copies', asy
509520
]);
510521
});
511522

512-
test('desktop adapter rebuilds delegation feedback from the exact authoritative Turn', async () => {
523+
test('desktop adapter rebuilds delegation feedback from the Message-owned execution Turn', async () => {
513524
const sessions = [
514525
desktopSession('accepted'),
515526
desktopSession('running', { status: 'running', runningTurnIds: ['turn-running'] }),
@@ -544,6 +555,18 @@ test('desktop adapter rebuilds delegation feedback from the exact authoritative
544555
if (sessionId === 'recovering') throw new Error('Host is recovering');
545556
return turns.get(sessionId) ?? [];
546557
},
558+
queryMessageExecutions: async (sessionId, messageIds) => ({
559+
resolutions: sessionId === 'accepted'
560+
? messageIds.map((messageId) => ({ messageId, state: 'pending' as const }))
561+
: sessionId === 'recovering'
562+
? []
563+
: messageIds.map((messageId) => ({
564+
messageId,
565+
state: 'owned' as const,
566+
turnId: `turn-${sessionId}`,
567+
runId: `run-${sessionId}`,
568+
})),
569+
}),
547570
create: async () => { throw new Error('not used'); },
548571
send: async () => { throw new Error('not used'); },
549572
stop: async () => {},
@@ -563,6 +586,7 @@ test('desktop adapter rebuilds delegation feedback from the exact authoritative
563586
].map(([targetSessionId, targetTurnId]) => ({
564587
delegationId: `delegation-${targetSessionId}`,
565588
targetSessionId: targetSessionId!,
589+
targetMessageId: `message-${targetSessionId}`,
566590
targetTurnId: targetTurnId!,
567591
}));
568592

@@ -579,6 +603,63 @@ test('desktop adapter rebuilds delegation feedback from the exact authoritative
579603
]);
580604
});
581605

606+
test('desktop adapter follows a delegated Message into its successor Turn', async () => {
607+
const targetSessionId = desktopSessionKey({ hostId: 'local-host', sessionId: 'payments' });
608+
const adapter = createDesktopWorkHubSessionPort({
609+
transcripts: transcriptsWith([{
610+
type: 'user',
611+
id: 'payment-message',
612+
turnId: 'successor-turn',
613+
ts: 2,
614+
text: 'Continue payment recovery',
615+
steeringEventId: 'payment-message',
616+
}]),
617+
sessions: {
618+
list: async () => [desktopSession(targetSessionId, {
619+
status: 'running',
620+
runningTurnIds: ['successor-turn'],
621+
})],
622+
listTurns: async () => [
623+
{
624+
turnId: 'admission-turn',
625+
status: 'completed',
626+
statusSource: 'recorded',
627+
},
628+
{
629+
turnId: 'successor-turn',
630+
status: 'running',
631+
statusSource: 'recorded',
632+
},
633+
],
634+
queryMessageExecutions: async (_sessionId, messageIds) => ({
635+
resolutions: messageIds.map((messageId) => ({
636+
messageId,
637+
state: 'owned' as const,
638+
turnId: 'successor-turn',
639+
runId: 'successor-run',
640+
})),
641+
}),
642+
create: async () => { throw new Error('not used'); },
643+
send: async () => { throw new Error('not used'); },
644+
stop: async () => {},
645+
subscribeChanges: () => () => {},
646+
},
647+
projectName: () => 'Maka',
648+
newTurnId: () => 'unused',
649+
});
650+
651+
const references = [{
652+
delegationId: 'payment-delegation',
653+
targetSessionId,
654+
targetTurnId: 'admission-turn',
655+
targetMessageId: 'payment-message',
656+
}];
657+
assert.deepEqual(await adapter.delegationFeedback(references), [{
658+
delegationId: 'payment-delegation',
659+
state: 'running',
660+
}]);
661+
});
662+
582663
test('desktop adapter preserves per-Host catalog coverage for ownership reconciliation', async () => {
583664
const localSessionId = desktopSessionKey({ hostId: 'local-host', sessionId: 'local' });
584665
const adapter = createDesktopWorkHubSessionPort({
@@ -590,6 +671,7 @@ test('desktop adapter preserves per-Host catalog coverage for ownership reconcil
590671
completeHostIds: ['local-host'],
591672
}),
592673
listTurns: async () => [],
674+
queryMessageExecutions: noMessageExecutions,
593675
create: async () => { throw new Error('not used'); },
594676
send: async () => { throw new Error('not used'); },
595677
stop: async () => {},
@@ -619,6 +701,7 @@ test('desktop adapter delegates create, send, and invalidation to Session APIs',
619701
runningTurnIds: ['turn-new'],
620702
})],
621703
listTurns: async () => [],
704+
queryMessageExecutions: noMessageExecutions,
622705
create: async (input) => {
623706
calls.push(['create', input]);
624707
return desktopSession('created', { name: input.name });
@@ -667,6 +750,7 @@ test('desktop adapter preserves when Session delivery steered an existing root T
667750
sessions: {
668751
list: async () => [],
669752
listTurns: async () => [],
753+
queryMessageExecutions: noMessageExecutions,
670754
create: async () => {
671755
throw new Error('not used');
672756
},
@@ -699,6 +783,7 @@ test('desktop adapter distinguishes definite rejection from an unknown delivery
699783
sessions: {
700784
list: async () => [],
701785
listTurns: async () => [],
786+
queryMessageExecutions: noMessageExecutions,
702787
create: async () => { throw new Error('not used'); },
703788
send: async () => {
704789
if (outcome === 'throw') throw new Error('transport disconnected');
@@ -792,6 +877,7 @@ test('desktop adapter reconciles lost replies from authoritative transcript iden
792877
sessions: {
793878
list: async () => [],
794879
listTurns: async () => [],
880+
queryMessageExecutions: noMessageExecutions,
795881
create: async () => { throw new Error('not used'); },
796882
send: async () => { throw new Error('not used'); },
797883
stop: async () => {},
@@ -818,6 +904,7 @@ test('desktop adapter binds stop to the root Turn owned by the WorkHub submissio
818904
sessions: {
819905
list: async () => [],
820906
listTurns: async () => [],
907+
queryMessageExecutions: noMessageExecutions,
821908
create: async () => {
822909
throw new Error('not used');
823910
},
@@ -855,6 +942,7 @@ test('desktop adapter derives stable origin evidence from the existing Session l
855942
{ userPromptPreview: '把风险按高、中、低分组' },
856943
];
857944
},
945+
queryMessageExecutions: noMessageExecutions,
858946
create: async () => {
859947
throw new Error('not used');
860948
},

apps/desktop/src/main/__tests__/workhub-surface-flow.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ test('durable delegation renders every projected target state as a navigable res
130130
delegationId: 'delegation-1',
131131
targetSessionId: 'payment',
132132
targetSessionName: 'Payments',
133+
targetMessageId: 'payment-message',
133134
targetTurnId: 'payment-turn',
134135
feedbackState: state,
135136
},
@@ -334,6 +335,7 @@ test('real Session projection creates new guide topics and preserves origin ambi
334335
list: async () => sessions,
335336
listTurns: async (sessionId) =>
336337
(prompts.get(sessionId) ?? []).map((userPromptPreview) => ({ userPromptPreview })),
338+
queryMessageExecutions: async () => ({ resolutions: [] }),
337339
create: async ({ name }) => {
338340
const id = name.includes('支付回调') ? 'payment' : 'layout';
339341
const session: WorkHubDesktopSession = {

apps/desktop/src/main/runtime-host-client.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,6 +1084,12 @@ export class DesktopRuntimeHostClient {
10841084
return this.request('turn.message.query', input);
10851085
}
10861086

1087+
queryMessageExecutions(
1088+
input: OperationInput<'turn.message.execution.query'>,
1089+
): Promise<OperationOutput<'turn.message.execution.query'>> {
1090+
return this.request('turn.message.execution.query', input);
1091+
}
1092+
10871093
retractQueueEntry(
10881094
input: Omit<QueueEntryRetractInput, "originHostEpoch">,
10891095
): Promise<QueueMutationResult> {

apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ type RuntimeHostSessionExecutionClient = Pick<
9595
| "interruptTurn"
9696
| 'listSessionTurns'
9797
| 'listSessionTurnLandmarks'
98+
| 'queryMessageExecutions'
9899
| 'queryMessages'
99100
| "queryTurnResume"
100101
| "readExecutionBoundary"
@@ -211,6 +212,14 @@ export function registerRuntimeHostSessionExecutionIpc(
211212
},
212213
);
213214

215+
ipcMain.handle(
216+
'sessions:queryMessageExecutions',
217+
async (_event, sessionId: string, messageIds: unknown) => {
218+
if (!Array.isArray(messageIds)) throw new Error('Invalid Message identities');
219+
return deps.client.queryMessageExecutions({ sessionId, messageIds });
220+
},
221+
);
222+
214223
handleReconnectableRead(
215224
ipcMain,
216225
"sessions:observe",

apps/desktop/src/preload/bridge-contract.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,6 +1009,10 @@ export interface MakaBridge {
10091009
sessionId: string,
10101010
messageIds: readonly string[],
10111011
): Promise<import('@maka/runtime-host/protocol').TurnMessageQueryResult>;
1012+
queryMessageExecutions(
1013+
sessionId: string,
1014+
messageIds: readonly string[],
1015+
): Promise<import('@maka/runtime-host/protocol').TurnMessageExecutionQueryResult>;
10121016
retractQueueEntry(sessionId: string, entryId: string): Promise<void>;
10131017
promoteQueueEntry(sessionId: string, entryId: string): Promise<void>;
10141018
updateQueueEntry(

apps/desktop/src/preload/preload.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1781,6 +1781,9 @@ const makaBridge = {
17811781
queryCancelledMessages(sessionId, messageIds) {
17821782
return invokeSessionRuntimeHost('sessions:queryCancelledMessages', sessionId, messageIds);
17831783
},
1784+
queryMessageExecutions(sessionId, messageIds) {
1785+
return invokeSessionRuntimeHost('sessions:queryMessageExecutions', sessionId, messageIds);
1786+
},
17841787
retractQueueEntry(sessionId: string, entryId: string): Promise<void> {
17851788
return invokeSessionRuntimeHost('sessions:retractQueueEntry', sessionId, entryId);
17861789
},

apps/desktop/src/renderer/workhub-controller.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ export type WorkHubDelegationExecutionState =
7474
export interface WorkHubDelegationReference {
7575
readonly delegationId: string;
7676
readonly targetSessionId: string;
77+
/** Stable delegated work identity; targetTurnId is only its admission location. */
78+
readonly targetMessageId: string;
7779
readonly targetTurnId: string;
7880
}
7981

@@ -102,6 +104,7 @@ export interface WorkHubCoordinationTurn {
102104
readonly delegationId: string;
103105
readonly targetSessionId: string;
104106
readonly targetSessionName: string;
107+
readonly targetMessageId: string;
105108
readonly targetTurnId: string;
106109
readonly feedbackState: WorkHubDelegationExecutionState;
107110
};
@@ -648,6 +651,7 @@ function createWorkHubControllerImplementation(deps: {
648651
? [{
649652
delegationId: turn.assignment.delegationId,
650653
targetSessionId: turn.assignment.targetSessionId,
654+
targetMessageId: turn.assignment.targetMessageId,
651655
targetTurnId: turn.assignment.targetTurnId,
652656
}]
653657
: [],

0 commit comments

Comments
 (0)