From f055193b16fb505262b19aacac6c6e0d29075063 Mon Sep 17 00:00:00 2001 From: 777genius Date: Thu, 14 May 2026 03:39:07 +0300 Subject: [PATCH] fix(team): observe opencode deliveries by prompt id --- .../services/team/TeamProvisioningService.ts | 11 +++- .../bridge/OpenCodeBridgeCommandContract.ts | 6 ++ .../bridge/OpenCodeReadinessBridge.ts | 3 +- .../delivery/OpenCodePromptDeliveryLedger.ts | 6 ++ .../runtime/OpenCodeTeamRuntimeAdapter.ts | 12 +++- .../team/OpenCodePromptDeliveryLedger.test.ts | 4 ++ .../team/OpenCodeTeamRuntimeAdapter.test.ts | 62 +++++++++++++++++++ .../team/TeamProvisioningService.test.ts | 8 +++ 8 files changed, 109 insertions(+), 3 deletions(-) diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index e41cca39..607b000f 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -368,7 +368,11 @@ type OpenCodeRuntimeMessageAdapter = TeamLaunchRuntimeAdapter & { input: OpenCodeTeamRuntimeMessageInput ): Promise; observeMessageDelivery?( - input: OpenCodeTeamRuntimeMessageInput & { prePromptCursor?: string | null } + input: OpenCodeTeamRuntimeMessageInput & { + prePromptCursor?: string | null; + sessionId?: string; + runtimePromptMessageId?: string; + } ): Promise; }; @@ -8358,6 +8362,8 @@ export class TeamProvisioningService { workSyncReviewRequestEventIds: input.workSyncReviewRequestEventIds, taskRefs: input.taskRefs, prePromptCursor: ledgerRecord.prePromptCursor, + sessionId: ledgerRecord.runtimeSessionId ?? undefined, + runtimePromptMessageId: ledgerRecord.runtimePromptMessageId ?? undefined, }); } catch (error) { const reason = `opencode_direct_user_delivery_inline_observe_failed: ${getErrorMessage( @@ -9808,6 +9814,8 @@ export class TeamProvisioningService { workSyncReviewRequestEventIds: input.workSyncReviewRequestEventIds, taskRefs: input.taskRefs, prePromptCursor: ledgerRecord.prePromptCursor, + sessionId: ledgerRecord.runtimeSessionId ?? undefined, + runtimePromptMessageId: ledgerRecord.runtimePromptMessageId ?? undefined, }); await this.rememberOpenCodeRuntimePidFromBridge({ teamName, @@ -9983,6 +9991,7 @@ export class TeamProvisioningService { attempted: true, responseObservation, sessionId: result.sessionId, + runtimePromptMessageId: result.runtimePromptMessageId, prePromptCursor: result.prePromptCursor, diagnostics: result.diagnostics, reason: promptAccepted ? responseObservation?.reason : result.diagnostics[0], diff --git a/src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract.ts b/src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract.ts index b067b9ab..d201367d 100644 --- a/src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract.ts +++ b/src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract.ts @@ -9,6 +9,7 @@ import type { export const OPEN_CODE_BRIDGE_SCHEMA_VERSION = 1 as const; export const OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION = 1 as const; export const OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION = 1 as const; +export const OPEN_CODE_DELIVERY_ACCEPTANCE_CONTRACT_VERSION = 1 as const; export type OpenCodeBridgeCommandName = | 'opencode.handshake' @@ -172,6 +173,7 @@ export interface OpenCodeSendMessageCommandBody { messageId?: string; deliveryAttemptId?: string; payloadHash?: string; + settlementMode?: 'observed' | 'acceptance'; fileParts?: { type: 'file'; mime: 'image/png' | 'image/jpeg' | 'image/webp'; @@ -233,6 +235,7 @@ export interface OpenCodeSendMessageCommandData { memberName: string; runtimePid?: number; prePromptCursor?: string | null; + runtimePromptMessageId?: string; responseObservation?: OpenCodeDeliveryResponseObservation; diagnostics: OpenCodeTeamBridgeDiagnostic[]; } @@ -284,6 +287,8 @@ export interface OpenCodeObserveMessageDeliveryCommandBody { projectPath: string; memberName: string; messageId: string; + sessionId?: string; + runtimePromptMessageId?: string; prePromptCursor?: string | null; } @@ -292,6 +297,7 @@ export interface OpenCodeObserveMessageDeliveryCommandData { sessionId?: string; memberName: string; runtimePid?: number; + runtimePromptMessageId?: string; responseObservation: OpenCodeDeliveryResponseObservation; diagnostics: OpenCodeTeamBridgeDiagnostic[]; } diff --git a/src/main/services/team/opencode/bridge/OpenCodeReadinessBridge.ts b/src/main/services/team/opencode/bridge/OpenCodeReadinessBridge.ts index 7a3d4946..8a439f52 100644 --- a/src/main/services/team/opencode/bridge/OpenCodeReadinessBridge.ts +++ b/src/main/services/team/opencode/bridge/OpenCodeReadinessBridge.ts @@ -81,7 +81,7 @@ const DEFAULT_BACKFILL_TIMEOUT_MS = 45_000; const DEFAULT_COMMAND_STATUS_TIMEOUT_MS = 5_000; function buildSendPayloadHash(input: OpenCodeSendMessageCommandBody): string { - const { payloadHash: _payloadHash, ...hashable } = input; + const { payloadHash: _payloadHash, settlementMode: _settlementMode, ...hashable } = input; return stableHash(hashable); } @@ -334,6 +334,7 @@ export class OpenCodeReadinessBridge implements OpenCodeTeamRuntimeBridgePort { memberName: input.body.memberName, sessionId: status.sessionId, runtimePid: status.runtimePid, + runtimePromptMessageId: status.runtimePromptMessageId, prePromptCursor: status.prePromptCursor, diagnostics, }; diff --git a/src/main/services/team/opencode/delivery/OpenCodePromptDeliveryLedger.ts b/src/main/services/team/opencode/delivery/OpenCodePromptDeliveryLedger.ts index 0208ba5a..03b87095 100644 --- a/src/main/services/team/opencode/delivery/OpenCodePromptDeliveryLedger.ts +++ b/src/main/services/team/opencode/delivery/OpenCodePromptDeliveryLedger.ts @@ -29,6 +29,7 @@ export interface OpenCodePromptDeliveryLedgerRecord { laneId: string; runId: string | null; runtimeSessionId: string | null; + runtimePromptMessageId?: string | null; inboxMessageId: string; inboxTimestamp: string; source: 'watcher' | 'ui-send' | 'manual' | 'watchdog' | 'member-work-sync-review-pickup'; @@ -136,6 +137,7 @@ export interface ApplyOpenCodePromptDeliveryResultInput { attempted?: boolean; responseObservation?: OpenCodeDeliveryResponseObservation; sessionId?: string | null; + runtimePromptMessageId?: string | null; runtimePid?: number; prePromptCursor?: string | null; diagnostics?: string[]; @@ -210,6 +212,7 @@ export class OpenCodePromptDeliveryLedgerStore { laneId: input.laneId, runId: input.runId ?? null, runtimeSessionId: null, + runtimePromptMessageId: null, inboxMessageId: input.inboxMessageId, inboxTimestamp: input.inboxTimestamp, source: input.source, @@ -315,6 +318,8 @@ export class OpenCodePromptDeliveryLedgerStore { attempts: input.accepted || input.attempted === true ? record.attempts + 1 : record.attempts, runtimeSessionId: input.sessionId ?? record.runtimeSessionId, + runtimePromptMessageId: + input.runtimePromptMessageId ?? record.runtimePromptMessageId ?? null, acceptanceUnknown: input.accepted ? false : record.acceptanceUnknown, lastAttemptAt: input.now, lastObservedAt: observation ? input.now : record.lastObservedAt, @@ -714,6 +719,7 @@ function isOpenCodePromptDeliveryLedgerRecord( typeof record.laneId === 'string' && isOptionalNullableString(record.runId) && isOptionalNullableString(record.runtimeSessionId) && + isOptionalNullableString(record.runtimePromptMessageId) && typeof record.inboxMessageId === 'string' && typeof record.inboxTimestamp === 'string' && isOpenCodePromptDeliverySource(record.source) && diff --git a/src/main/services/team/runtime/OpenCodeTeamRuntimeAdapter.ts b/src/main/services/team/runtime/OpenCodeTeamRuntimeAdapter.ts index d73dba62..82cf20ee 100644 --- a/src/main/services/team/runtime/OpenCodeTeamRuntimeAdapter.ts +++ b/src/main/services/team/runtime/OpenCodeTeamRuntimeAdapter.ts @@ -83,6 +83,7 @@ export interface OpenCodeTeamRuntimeMessageResult { sessionId?: string; runtimePid?: number; prePromptCursor?: string | null; + runtimePromptMessageId?: string; responseObservation?: OpenCodeSendMessageCommandData['responseObservation']; diagnostics: string[]; } @@ -333,6 +334,7 @@ export class OpenCodeTeamRuntimeAdapter implements TeamLaunchRuntimeAdapter { text: buildOpenCodeRuntimeMessageText(input), messageId: input.messageId, ...(input.deliveryAttemptId ? { deliveryAttemptId: input.deliveryAttemptId } : {}), + settlementMode: 'acceptance', fileParts: input.fileParts, actionMode: input.actionMode, messageKind: input.messageKind, @@ -347,13 +349,18 @@ export class OpenCodeTeamRuntimeAdapter implements TeamLaunchRuntimeAdapter { sessionId: data.sessionId, runtimePid: data.runtimePid, prePromptCursor: data.prePromptCursor, + runtimePromptMessageId: data.runtimePromptMessageId, responseObservation: data.responseObservation, diagnostics: data.diagnostics.map((diagnostic) => diagnostic.message), }; } async observeMessageDelivery( - input: OpenCodeTeamRuntimeMessageInput & { prePromptCursor?: string | null } + input: OpenCodeTeamRuntimeMessageInput & { + prePromptCursor?: string | null; + sessionId?: string; + runtimePromptMessageId?: string; + } ): Promise { if (!this.bridge.observeOpenCodeTeamMessageDelivery) { return { @@ -380,6 +387,8 @@ export class OpenCodeTeamRuntimeAdapter implements TeamLaunchRuntimeAdapter { projectPath: input.cwd, memberName: input.memberName, messageId: input.messageId, + sessionId: input.sessionId, + runtimePromptMessageId: input.runtimePromptMessageId, prePromptCursor: input.prePromptCursor ?? null, }); @@ -389,6 +398,7 @@ export class OpenCodeTeamRuntimeAdapter implements TeamLaunchRuntimeAdapter { memberName: input.memberName, sessionId: data.sessionId, runtimePid: data.runtimePid, + runtimePromptMessageId: data.runtimePromptMessageId, responseObservation: data.responseObservation, diagnostics: data.diagnostics.map((diagnostic) => diagnostic.message), }; diff --git a/test/main/services/team/OpenCodePromptDeliveryLedger.test.ts b/test/main/services/team/OpenCodePromptDeliveryLedger.test.ts index ab3b87a4..2d5e8bc6 100644 --- a/test/main/services/team/OpenCodePromptDeliveryLedger.test.ts +++ b/test/main/services/team/OpenCodePromptDeliveryLedger.test.ts @@ -315,6 +315,8 @@ describe('OpenCodePromptDeliveryLedger', () => { id: unanswered.id, accepted: true, attempted: true, + sessionId: 'oc-session-1', + runtimePromptMessageId: 'msg_prompt_1', responseObservation: { state: 'empty_assistant_turn', deliveredUserMessageId: 'oc-user-1', @@ -332,6 +334,8 @@ describe('OpenCodePromptDeliveryLedger', () => { expect(emptyResult.status).toBe('unanswered'); expect(emptyResult.responseState).toBe('empty_assistant_turn'); expect(emptyResult.attempts).toBe(1); + expect(emptyResult.runtimeSessionId).toBe('oc-session-1'); + expect(emptyResult.runtimePromptMessageId).toBe('msg_prompt_1'); const noAssistant = await store.ensurePending({ teamName: 'team-a', diff --git a/test/main/services/team/OpenCodeTeamRuntimeAdapter.test.ts b/test/main/services/team/OpenCodeTeamRuntimeAdapter.test.ts index 5cfe3aa2..308a6c8b 100644 --- a/test/main/services/team/OpenCodeTeamRuntimeAdapter.test.ts +++ b/test/main/services/team/OpenCodeTeamRuntimeAdapter.test.ts @@ -484,6 +484,7 @@ describe('OpenCodeTeamRuntimeAdapter', () => { sessionId: 'oc-session-bob', memberName: 'bob', runtimePid: 456, + runtimePromptMessageId: 'msg_prompt_1', diagnostics: [], })); const adapter = new OpenCodeTeamRuntimeAdapter( @@ -511,6 +512,7 @@ describe('OpenCodeTeamRuntimeAdapter', () => { memberName: 'bob', sessionId: 'oc-session-bob', runtimePid: 456, + runtimePromptMessageId: 'msg_prompt_1', diagnostics: [], }); expect(sendOpenCodeTeamMessage).toHaveBeenCalledWith({ @@ -522,6 +524,7 @@ describe('OpenCodeTeamRuntimeAdapter', () => { memberName: 'bob', text: expect.stringContaining('agent-teams_message_send'), messageId: 'msg-1', + settlementMode: 'acceptance', actionMode: 'delegate', taskRefs: [{ taskId: 'task-1', displayId: 'abcd1234', teamName: 'team-a' }], agent: 'teammate', @@ -542,6 +545,65 @@ describe('OpenCodeTeamRuntimeAdapter', () => { expect(sentText).toContain('never use #00000000'); }); + it('observes direct teammate messages by exact accepted runtime prompt id', async () => { + const observeOpenCodeTeamMessageDelivery = vi.fn< + NonNullable + >(async () => ({ + observed: true, + sessionId: 'oc-session-bob', + memberName: 'bob', + runtimePid: 456, + runtimePromptMessageId: 'msg_prompt_1', + responseObservation: { + state: 'responded_plain_text', + deliveredUserMessageId: 'msg_prompt_1', + assistantMessageId: 'oc-assistant-1', + toolCallNames: [], + visibleMessageToolCallId: null, + visibleReplyMessageId: null, + visibleReplyCorrelation: null, + latestAssistantPreview: 'done', + reason: null, + }, + diagnostics: [], + })); + const adapter = new OpenCodeTeamRuntimeAdapter( + bridgePort(readiness({ state: 'ready', launchAllowed: true }), { + observeOpenCodeTeamMessageDelivery, + }) + ); + + await expect( + adapter.observeMessageDelivery({ + runId: 'run-1', + teamName: 'team-a', + laneId: 'secondary:opencode:bob', + memberName: 'bob', + cwd: '/repo', + text: 'hello bob', + messageId: 'msg-1', + sessionId: 'oc-session-bob', + runtimePromptMessageId: 'msg_prompt_1', + prePromptCursor: 'cursor-before', + }) + ).resolves.toMatchObject({ + ok: true, + sessionId: 'oc-session-bob', + runtimePromptMessageId: 'msg_prompt_1', + responseObservation: { + deliveredUserMessageId: 'msg_prompt_1', + }, + }); + + expect(observeOpenCodeTeamMessageDelivery).toHaveBeenCalledWith( + expect.objectContaining({ + sessionId: 'oc-session-bob', + runtimePromptMessageId: 'msg_prompt_1', + prePromptCursor: 'cursor-before', + }) + ); + }); + it('sends member work sync nudges with report-oriented response instructions', async () => { const sendOpenCodeTeamMessage = vi.fn< NonNullable diff --git a/test/main/services/team/TeamProvisioningService.test.ts b/test/main/services/team/TeamProvisioningService.test.ts index 1463f425..713ed9cb 100644 --- a/test/main/services/team/TeamProvisioningService.test.ts +++ b/test/main/services/team/TeamProvisioningService.test.ts @@ -6161,6 +6161,7 @@ describe('TeamProvisioningService', () => { providerId: 'opencode', memberName: String(input.memberName), sessionId: 'oc-session-bob', + runtimePromptMessageId: `msg_prompt_${sendMessageToMember.mock.calls.length}`, prePromptCursor: `cursor-${sendMessageToMember.mock.calls.length}`, responseObservation: { state: 'pending', @@ -6293,6 +6294,13 @@ describe('TeamProvisioningService', () => { }); expect(observeMessageDelivery).toHaveBeenCalledTimes(1); + expect(observeMessageDelivery).toHaveBeenCalledWith( + expect.objectContaining({ + sessionId: 'oc-session-bob', + runtimePromptMessageId: 'msg_prompt_1', + prePromptCursor: 'cursor-1', + }) + ); expect(sendMessageToMember).toHaveBeenCalledTimes(2); expect(sendMessageToMember.mock.calls[1]?.[0]).toMatchObject({ runId: 'opencode-run-bob',