fix(team): observe opencode deliveries by prompt id

This commit is contained in:
777genius 2026-05-14 03:39:07 +03:00
parent 565362a911
commit f055193b16
8 changed files with 109 additions and 3 deletions

View file

@ -368,7 +368,11 @@ type OpenCodeRuntimeMessageAdapter = TeamLaunchRuntimeAdapter & {
input: OpenCodeTeamRuntimeMessageInput
): Promise<OpenCodeTeamRuntimeMessageResult>;
observeMessageDelivery?(
input: OpenCodeTeamRuntimeMessageInput & { prePromptCursor?: string | null }
input: OpenCodeTeamRuntimeMessageInput & {
prePromptCursor?: string | null;
sessionId?: string;
runtimePromptMessageId?: string;
}
): Promise<OpenCodeTeamRuntimeMessageResult>;
};
@ -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],

View file

@ -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[];
}

View file

@ -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,
};

View file

@ -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) &&

View file

@ -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<OpenCodeTeamRuntimeMessageResult> {
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),
};

View file

@ -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',

View file

@ -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<OpenCodeTeamRuntimeBridgePort['observeOpenCodeTeamMessageDelivery']>
>(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<OpenCodeTeamRuntimeBridgePort['sendOpenCodeTeamMessage']>

View file

@ -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',