fix(team): observe opencode deliveries by prompt id
This commit is contained in:
parent
565362a911
commit
f055193b16
8 changed files with 109 additions and 3 deletions
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) &&
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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']>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue