fix(team): harden opencode delivery prompt evidence

This commit is contained in:
777genius 2026-05-14 04:25:48 +03:00
parent f055193b16
commit 55e1c8f3c4
16 changed files with 820 additions and 33 deletions

View file

@ -142,6 +142,7 @@ export class TeamInboxReader {
row.messageKind === 'slash_command' || row.messageKind === 'slash_command' ||
row.messageKind === 'slash_command_result' || row.messageKind === 'slash_command_result' ||
row.messageKind === 'task_comment_notification' || row.messageKind === 'task_comment_notification' ||
row.messageKind === 'task_stall_remediation' ||
row.messageKind === 'member_work_sync_nudge' || row.messageKind === 'member_work_sync_nudge' ||
row.messageKind === 'agent_error' row.messageKind === 'agent_error'
? row.messageKind ? row.messageKind

View file

@ -8363,7 +8363,10 @@ export class TeamProvisioningService {
taskRefs: input.taskRefs, taskRefs: input.taskRefs,
prePromptCursor: ledgerRecord.prePromptCursor, prePromptCursor: ledgerRecord.prePromptCursor,
sessionId: ledgerRecord.runtimeSessionId ?? undefined, sessionId: ledgerRecord.runtimeSessionId ?? undefined,
runtimePromptMessageId: ledgerRecord.runtimePromptMessageId ?? undefined, runtimePromptMessageId:
ledgerRecord.lastRuntimePromptMessageId ??
ledgerRecord.runtimePromptMessageId ??
undefined,
}); });
} catch (error) { } catch (error) {
const reason = `opencode_direct_user_delivery_inline_observe_failed: ${getErrorMessage( const reason = `opencode_direct_user_delivery_inline_observe_failed: ${getErrorMessage(
@ -8416,6 +8419,8 @@ export class TeamProvisioningService {
latestAssistantPreview: null, latestAssistantPreview: null,
reason: observed.diagnostics[0] ?? null, reason: observed.diagnostics[0] ?? null,
}, },
sessionId: observed.sessionId,
runtimePromptMessageId: observed.runtimePromptMessageId,
diagnostics: [ diagnostics: [
`opencode_direct_user_delivery_inline_observe_attempt_${inlineObserveAttempt}`, `opencode_direct_user_delivery_inline_observe_attempt_${inlineObserveAttempt}`,
...(hadMessageSendToolError ? ['opencode_message_send_tool_error_inline_observe'] : []), ...(hadMessageSendToolError ? ['opencode_message_send_tool_error_inline_observe'] : []),
@ -9815,7 +9820,10 @@ export class TeamProvisioningService {
taskRefs: input.taskRefs, taskRefs: input.taskRefs,
prePromptCursor: ledgerRecord.prePromptCursor, prePromptCursor: ledgerRecord.prePromptCursor,
sessionId: ledgerRecord.runtimeSessionId ?? undefined, sessionId: ledgerRecord.runtimeSessionId ?? undefined,
runtimePromptMessageId: ledgerRecord.runtimePromptMessageId ?? undefined, runtimePromptMessageId:
ledgerRecord.lastRuntimePromptMessageId ??
ledgerRecord.runtimePromptMessageId ??
undefined,
}); });
await this.rememberOpenCodeRuntimePidFromBridge({ await this.rememberOpenCodeRuntimePidFromBridge({
teamName, teamName,
@ -9842,6 +9850,8 @@ export class TeamProvisioningService {
latestAssistantPreview: null, latestAssistantPreview: null,
reason: observed.diagnostics[0] ?? null, reason: observed.diagnostics[0] ?? null,
}, },
sessionId: observed.sessionId,
runtimePromptMessageId: observed.runtimePromptMessageId,
diagnostics: observed.diagnostics, diagnostics: observed.diagnostics,
observedAt: nowIso(), observedAt: nowIso(),
}); });
@ -9982,8 +9992,17 @@ export class TeamProvisioningService {
const responseObservation = this.normalizeOpenCodeDeliveryResponseObservation( const responseObservation = this.normalizeOpenCodeDeliveryResponseObservation(
result.responseObservation result.responseObservation
); );
const promptAccepted = const promptAcceptedByRuntimeIdentity = Boolean(
result.ok || this.isOpenCodePromptAcceptedByObservation(responseObservation); result.ok && result.runtimePromptMessageId?.trim()
);
const promptAcceptedByObservation =
this.isOpenCodePromptAcceptedByObservation(responseObservation);
const promptAccepted = promptAcceptedByRuntimeIdentity || promptAcceptedByObservation;
const promptAcceptanceMissingRuntimePromptId =
result.ok && !promptAcceptedByRuntimeIdentity && !promptAcceptedByObservation;
const deliveryDiagnostics = promptAcceptanceMissingRuntimePromptId
? [...result.diagnostics, 'opencode_prompt_acceptance_missing_runtime_prompt_id']
: result.diagnostics;
if (ledgerRecord && ledger) { if (ledgerRecord && ledger) {
ledgerRecord = await ledger.applyDeliveryResult({ ledgerRecord = await ledger.applyDeliveryResult({
id: ledgerRecord.id, id: ledgerRecord.id,
@ -9992,9 +10011,10 @@ export class TeamProvisioningService {
responseObservation, responseObservation,
sessionId: result.sessionId, sessionId: result.sessionId,
runtimePromptMessageId: result.runtimePromptMessageId, runtimePromptMessageId: result.runtimePromptMessageId,
deliveryAttemptId,
prePromptCursor: result.prePromptCursor, prePromptCursor: result.prePromptCursor,
diagnostics: result.diagnostics, diagnostics: deliveryDiagnostics,
reason: promptAccepted ? responseObservation?.reason : result.diagnostics[0], reason: promptAccepted ? responseObservation?.reason : deliveryDiagnostics[0],
now: nowIso(), now: nowIso(),
}); });
this.emitOpenCodePromptDeliveryTaskLogChange( this.emitOpenCodePromptDeliveryTaskLogChange(
@ -10049,7 +10069,7 @@ export class TeamProvisioningService {
ledgerRecord, ledgerRecord,
{ {
accepted: promptAccepted, accepted: promptAccepted,
reason: ledgerRecord.lastReason ?? result.diagnostics[0] ?? null, reason: ledgerRecord.lastReason ?? deliveryDiagnostics[0] ?? null,
} }
); );
} }
@ -10112,16 +10132,21 @@ export class TeamProvisioningService {
} }
} }
if (ledgerRecord && !promptAccepted) { if (ledgerRecord && !promptAccepted) {
const reason = this.isOpenCodePromptAcceptanceUnknownFailure(result.diagnostics) const reason = promptAcceptanceMissingRuntimePromptId
? 'opencode_prompt_acceptance_unknown_after_bridge_timeout' ? 'opencode_prompt_acceptance_unknown_missing_runtime_prompt_id'
: (result.diagnostics[0] ?? 'opencode_message_delivery_failed'); : this.isOpenCodePromptAcceptanceUnknownFailure(deliveryDiagnostics)
if (reason === 'opencode_prompt_acceptance_unknown_after_bridge_timeout') { ? 'opencode_prompt_acceptance_unknown_after_bridge_timeout'
: (deliveryDiagnostics[0] ?? 'opencode_message_delivery_failed');
if (
reason === 'opencode_prompt_acceptance_unknown_after_bridge_timeout' ||
reason === 'opencode_prompt_acceptance_unknown_missing_runtime_prompt_id'
) {
const delayMs = OPENCODE_PROMPT_DELIVERY_OBSERVE_DELAY_MS; const delayMs = OPENCODE_PROMPT_DELIVERY_OBSERVE_DELAY_MS;
ledgerRecord = await ledger!.markAcceptanceUnknown({ ledgerRecord = await ledger!.markAcceptanceUnknown({
id: ledgerRecord.id, id: ledgerRecord.id,
reason, reason,
nextAttemptAt: new Date(Date.now() + delayMs).toISOString(), nextAttemptAt: new Date(Date.now() + delayMs).toISOString(),
diagnostics: result.diagnostics, diagnostics: deliveryDiagnostics,
markedAt: nowIso(), markedAt: nowIso(),
}); });
this.scheduleOpenCodePromptDeliveryWatchdog({ this.scheduleOpenCodePromptDeliveryWatchdog({

View file

@ -438,6 +438,7 @@ export interface OpenCodeBridgePeerIdentity {
supportedCommands: OpenCodeBridgeCommandName[]; supportedCommands: OpenCodeBridgeCommandName[];
opencodeTaskLedgerEvidenceContractVersion?: number; opencodeTaskLedgerEvidenceContractVersion?: number;
opencodeAppManagedBootstrapContractVersion?: number; opencodeAppManagedBootstrapContractVersion?: number;
opencodeDeliveryAcceptanceContractVersion?: number;
}; };
runtime: { runtime: {
providerId: 'opencode'; providerId: 'opencode';
@ -618,6 +619,7 @@ export function validateOpenCodeBridgeHandshake(input: {
expectedCapabilitySnapshotId: string | null; expectedCapabilitySnapshotId: string | null;
expectedManifestHighWatermark: number | null; expectedManifestHighWatermark: number | null;
expectedRunId: string | null; expectedRunId: string | null;
requiresDeliveryAcceptanceContract?: boolean;
}): { ok: true } | { ok: false; reason: string } { }): { ok: true } | { ok: false; reason: string } {
const shape = validateOpenCodeBridgeHandshakeShape(input.handshake); const shape = validateOpenCodeBridgeHandshakeShape(input.handshake);
if (!shape.ok) { if (!shape.ok) {
@ -676,6 +678,19 @@ export function validateOpenCodeBridgeHandshake(input: {
} }
} }
if (
input.requiredCommand === 'opencode.sendMessage' &&
input.requiresDeliveryAcceptanceContract === true &&
input.handshake.server.bridgeProtocol.opencodeDeliveryAcceptanceContractVersion !==
OPEN_CODE_DELIVERY_ACCEPTANCE_CONTRACT_VERSION
) {
return {
ok: false,
reason:
'OpenCode delivery acceptance mode is required, but the orchestrator does not advertise contract version 1. Falling back to observed delivery mode is required.',
};
}
if ( if (
input.expectedCapabilitySnapshotId && input.expectedCapabilitySnapshotId &&
input.handshake.server.runtime.capabilitySnapshotId !== input.expectedCapabilitySnapshotId input.handshake.server.runtime.capabilitySnapshotId !== input.expectedCapabilitySnapshotId
@ -948,7 +963,10 @@ function isPeerIdentity(value: unknown): value is OpenCodeBridgePeerIdentity {
(bridgeProtocol.opencodeTaskLedgerEvidenceContractVersion as number) < 1)) || (bridgeProtocol.opencodeTaskLedgerEvidenceContractVersion as number) < 1)) ||
(bridgeProtocol.opencodeAppManagedBootstrapContractVersion !== undefined && (bridgeProtocol.opencodeAppManagedBootstrapContractVersion !== undefined &&
(!Number.isInteger(bridgeProtocol.opencodeAppManagedBootstrapContractVersion) || (!Number.isInteger(bridgeProtocol.opencodeAppManagedBootstrapContractVersion) ||
(bridgeProtocol.opencodeAppManagedBootstrapContractVersion as number) < 1)) (bridgeProtocol.opencodeAppManagedBootstrapContractVersion as number) < 1)) ||
(bridgeProtocol.opencodeDeliveryAcceptanceContractVersion !== undefined &&
(!Number.isInteger(bridgeProtocol.opencodeDeliveryAcceptanceContractVersion) ||
(bridgeProtocol.opencodeDeliveryAcceptanceContractVersion as number) < 1))
) { ) {
return false; return false;
} }

View file

@ -1,5 +1,6 @@
import { import {
OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION, OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
OPEN_CODE_DELIVERY_ACCEPTANCE_CONTRACT_VERSION,
OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION, OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION,
} from './OpenCodeBridgeCommandContract'; } from './OpenCodeBridgeCommandContract';
@ -103,6 +104,7 @@ export function createOpenCodeBridgeClientIdentity(input: {
], ],
opencodeTaskLedgerEvidenceContractVersion: OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION, opencodeTaskLedgerEvidenceContractVersion: OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION,
opencodeAppManagedBootstrapContractVersion: OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION, opencodeAppManagedBootstrapContractVersion: OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
opencodeDeliveryAcceptanceContractVersion: OPEN_CODE_DELIVERY_ACCEPTANCE_CONTRACT_VERSION,
}, },
runtime: { runtime: {
providerId: 'opencode', providerId: 'opencode',

View file

@ -232,29 +232,56 @@ export class OpenCodeReadinessBridge implements OpenCodeTeamRuntimeBridgePort {
...input, ...input,
payloadHash: input.payloadHash ?? buildSendPayloadHash(input), payloadHash: input.payloadHash ?? buildSendPayloadHash(input),
}; };
const result = await this.bridge.execute< let activeRequestId = commandRequestId;
OpenCodeSendMessageCommandBody, let activeBody = body;
OpenCodeSendMessageCommandData let usedObservedFallback = false;
>('opencode.sendMessage', body, { const executeSend = (nextBody: OpenCodeSendMessageCommandBody, requestId: string) =>
cwd: body.projectPath, this.bridge.execute<OpenCodeSendMessageCommandBody, OpenCodeSendMessageCommandData>(
timeoutMs: this.options.sendTimeoutMs ?? DEFAULT_SEND_TIMEOUT_MS, 'opencode.sendMessage',
requestId: commandRequestId, nextBody,
}); {
cwd: nextBody.projectPath,
timeoutMs: this.options.sendTimeoutMs ?? DEFAULT_SEND_TIMEOUT_MS,
requestId,
}
);
let result: OpenCodeBridgeResult<OpenCodeSendMessageCommandData>;
try {
result = await executeSend(activeBody, activeRequestId);
} catch (error) {
if (
body.settlementMode !== 'acceptance' ||
!isOpenCodeAcceptanceContractMissingError(error)
) {
throw error;
}
activeRequestId = `${commandRequestId}-observed`;
activeBody = {
...body,
settlementMode: 'observed',
};
usedObservedFallback = true;
result = await executeSend(activeBody, activeRequestId);
}
if (result.ok) { if (result.ok) {
return result.data; return usedObservedFallback
? withOpenCodeObservedFallbackDiagnostic(result.data)
: result.data;
} }
if (result.error.kind === 'timeout') { if (result.error.kind === 'timeout') {
const recovered = await this.recoverTimedOutSendMessage({ const recovered = await this.recoverTimedOutSendMessage({
originalRequestId: commandRequestId, originalRequestId: activeRequestId,
body, body: activeBody,
}); });
if (recovered) { if (recovered) {
return recovered; return usedObservedFallback ? withOpenCodeObservedFallbackDiagnostic(recovered) : recovered;
} }
} }
return { return {
accepted: false, accepted: false,
memberName: body.memberName, memberName: activeBody.memberName,
diagnostics: [ diagnostics: [
{ {
code: result.error.kind, code: result.error.kind,
@ -543,6 +570,28 @@ function formatDiagnosticEvent(event: OpenCodeBridgeDiagnosticEvent): string {
return `${event.type}: ${event.message}`; return `${event.type}: ${event.message}`;
} }
function isOpenCodeAcceptanceContractMissingError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
return message.includes('OpenCode delivery acceptance mode is required');
}
function withOpenCodeObservedFallbackDiagnostic(
data: OpenCodeSendMessageCommandData
): OpenCodeSendMessageCommandData {
return {
...data,
diagnostics: [
{
code: 'opencode_accept_fast_capability_missing',
severity: 'warning',
message:
'OpenCode delivery acceptance capability was not advertised by the orchestrator; used observed delivery mode.',
},
...data.diagnostics,
],
};
}
function thrownBridgeFailure<TData>( function thrownBridgeFailure<TData>(
command: OpenCodeBridgeCommandName, command: OpenCodeBridgeCommandName,
runId: string, runId: string,

View file

@ -118,6 +118,10 @@ export class OpenCodeStateChangingBridgeCommandService {
expectedCapabilitySnapshotId: input.capabilitySnapshotId, expectedCapabilitySnapshotId: input.capabilitySnapshotId,
expectedManifestHighWatermark: manifest.highWatermark, expectedManifestHighWatermark: manifest.highWatermark,
expectedRunId: input.runId, expectedRunId: input.runId,
requiresDeliveryAcceptanceContract: requiresOpenCodeDeliveryAcceptanceContract(
input.command,
input.body
),
}); });
if (!handshakeValidation.ok) { if (!handshakeValidation.ok) {
@ -299,6 +303,16 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value); return typeof value === 'object' && value !== null && !Array.isArray(value);
} }
function requiresOpenCodeDeliveryAcceptanceContract(
command: OpenCodeBridgeCommandName,
body: unknown
): boolean {
if (command !== 'opencode.sendMessage' || !isRecord(body)) {
return false;
}
return body.settlementMode === 'acceptance';
}
function stringifyError(error: unknown): string { function stringifyError(error: unknown): string {
return error instanceof Error ? error.message : String(error); return error instanceof Error ? error.message : String(error);
} }

View file

@ -30,6 +30,9 @@ export interface OpenCodePromptDeliveryLedgerRecord {
runId: string | null; runId: string | null;
runtimeSessionId: string | null; runtimeSessionId: string | null;
runtimePromptMessageId?: string | null; runtimePromptMessageId?: string | null;
runtimePromptMessageIds?: string[];
lastRuntimePromptMessageId?: string | null;
lastDeliveryAttemptIdWithAcceptedPrompt?: string | null;
inboxMessageId: string; inboxMessageId: string;
inboxTimestamp: string; inboxTimestamp: string;
source: 'watcher' | 'ui-send' | 'manual' | 'watchdog' | 'member-work-sync-review-pickup'; source: 'watcher' | 'ui-send' | 'manual' | 'watchdog' | 'member-work-sync-review-pickup';
@ -138,6 +141,7 @@ export interface ApplyOpenCodePromptDeliveryResultInput {
responseObservation?: OpenCodeDeliveryResponseObservation; responseObservation?: OpenCodeDeliveryResponseObservation;
sessionId?: string | null; sessionId?: string | null;
runtimePromptMessageId?: string | null; runtimePromptMessageId?: string | null;
deliveryAttemptId?: string | null;
runtimePid?: number; runtimePid?: number;
prePromptCursor?: string | null; prePromptCursor?: string | null;
diagnostics?: string[]; diagnostics?: string[];
@ -213,6 +217,9 @@ export class OpenCodePromptDeliveryLedgerStore {
runId: input.runId ?? null, runId: input.runId ?? null,
runtimeSessionId: null, runtimeSessionId: null,
runtimePromptMessageId: null, runtimePromptMessageId: null,
runtimePromptMessageIds: [],
lastRuntimePromptMessageId: null,
lastDeliveryAttemptIdWithAcceptedPrompt: null,
inboxMessageId: input.inboxMessageId, inboxMessageId: input.inboxMessageId,
inboxTimestamp: input.inboxTimestamp, inboxTimestamp: input.inboxTimestamp,
source: input.source, source: input.source,
@ -305,6 +312,37 @@ export class OpenCodePromptDeliveryLedgerStore {
observation?.state ?? (input.accepted ? record.responseState : 'not_observed'); observation?.state ?? (input.accepted ? record.responseState : 'not_observed');
const responded = isOpenCodePromptResponseStateResponded(responseState); const responded = isOpenCodePromptResponseStateResponded(responseState);
const unanswered = isOpenCodePromptDeliveryUnansweredResponseState(responseState); const unanswered = isOpenCodePromptDeliveryUnansweredResponseState(responseState);
const acceptedRuntimePromptMessageId =
input.accepted && input.runtimePromptMessageId?.trim()
? input.runtimePromptMessageId.trim()
: null;
const previousRuntimePromptMessageIds = getOpenCodeRuntimePromptMessageIds(record);
const runtimePromptMessageIds =
acceptedRuntimePromptMessageId &&
!previousRuntimePromptMessageIds.includes(acceptedRuntimePromptMessageId)
? [...previousRuntimePromptMessageIds, acceptedRuntimePromptMessageId]
: previousRuntimePromptMessageIds;
const acceptedDeliveryAttemptId = input.deliveryAttemptId?.trim() || null;
const acceptedAttemptAlreadyRecorded = Boolean(
input.accepted &&
acceptedDeliveryAttemptId &&
record.lastDeliveryAttemptIdWithAcceptedPrompt === acceptedDeliveryAttemptId
);
const acceptedPromptAlreadyRecorded = Boolean(
input.accepted &&
acceptedRuntimePromptMessageId &&
previousRuntimePromptMessageIds.includes(acceptedRuntimePromptMessageId)
);
const shouldIncrementAttempts =
(input.accepted || input.attempted === true) &&
!acceptedAttemptAlreadyRecorded &&
!acceptedPromptAlreadyRecorded;
const lastRuntimePromptMessageId =
acceptedRuntimePromptMessageId ??
record.lastRuntimePromptMessageId ??
record.runtimePromptMessageId ??
runtimePromptMessageIds[runtimePromptMessageIds.length - 1] ??
null;
return { return {
...record, ...record,
status: input.accepted status: input.accepted
@ -315,11 +353,15 @@ export class OpenCodePromptDeliveryLedgerStore {
: 'accepted' : 'accepted'
: 'failed_retryable', : 'failed_retryable',
responseState, responseState,
attempts: attempts: shouldIncrementAttempts ? record.attempts + 1 : record.attempts,
input.accepted || input.attempted === true ? record.attempts + 1 : record.attempts,
runtimeSessionId: input.sessionId ?? record.runtimeSessionId, runtimeSessionId: input.sessionId ?? record.runtimeSessionId,
runtimePromptMessageId: runtimePromptMessageId: lastRuntimePromptMessageId,
input.runtimePromptMessageId ?? record.runtimePromptMessageId ?? null, runtimePromptMessageIds,
lastRuntimePromptMessageId,
lastDeliveryAttemptIdWithAcceptedPrompt:
input.accepted && acceptedDeliveryAttemptId
? acceptedDeliveryAttemptId
: (record.lastDeliveryAttemptIdWithAcceptedPrompt ?? null),
acceptanceUnknown: input.accepted ? false : record.acceptanceUnknown, acceptanceUnknown: input.accepted ? false : record.acceptanceUnknown,
lastAttemptAt: input.now, lastAttemptAt: input.now,
lastObservedAt: observation ? input.now : record.lastObservedAt, lastObservedAt: observation ? input.now : record.lastObservedAt,
@ -348,6 +390,8 @@ export class OpenCodePromptDeliveryLedgerStore {
async applyObservation(input: { async applyObservation(input: {
id: string; id: string;
responseObservation: OpenCodeDeliveryResponseObservation; responseObservation: OpenCodeDeliveryResponseObservation;
sessionId?: string | null;
runtimePromptMessageId?: string | null;
diagnostics?: string[]; diagnostics?: string[];
observedAt: string; observedAt: string;
}): Promise<OpenCodePromptDeliveryLedgerRecord> { }): Promise<OpenCodePromptDeliveryLedgerRecord> {
@ -356,17 +400,48 @@ export class OpenCodePromptDeliveryLedgerStore {
const unanswered = isOpenCodePromptDeliveryUnansweredResponseState( const unanswered = isOpenCodePromptDeliveryUnansweredResponseState(
input.responseObservation.state input.responseObservation.state
); );
const previousRuntimePromptMessageIds = getOpenCodeRuntimePromptMessageIds(record);
const deliveredRuntimePromptMessageId =
input.responseObservation.deliveredUserMessageId?.trim() || null;
const requestedRuntimePromptMessageId = input.runtimePromptMessageId?.trim() || null;
const requestedRuntimePromptMessageIdIsKnown = Boolean(
requestedRuntimePromptMessageId &&
previousRuntimePromptMessageIds.includes(requestedRuntimePromptMessageId)
);
const observedRuntimePromptMessageId =
deliveredRuntimePromptMessageId ||
(requestedRuntimePromptMessageIdIsKnown ? requestedRuntimePromptMessageId : null);
const runtimePromptMessageIds =
observedRuntimePromptMessageId &&
!previousRuntimePromptMessageIds.includes(observedRuntimePromptMessageId)
? [...previousRuntimePromptMessageIds, observedRuntimePromptMessageId]
: previousRuntimePromptMessageIds;
const promptAcceptedByObservation = Boolean(deliveredRuntimePromptMessageId);
const lastRuntimePromptMessageId =
observedRuntimePromptMessageId ??
record.lastRuntimePromptMessageId ??
record.runtimePromptMessageId ??
runtimePromptMessageIds[runtimePromptMessageIds.length - 1] ??
null;
return { return {
...record, ...record,
status: responded status: responded
? 'responded' ? 'responded'
: unanswered : unanswered
? 'unanswered' ? 'unanswered'
: record.status === 'pending' : record.status === 'pending' || promptAcceptedByObservation
? 'accepted' ? 'accepted'
: record.status, : record.status,
responseState: input.responseObservation.state, responseState: input.responseObservation.state,
runtimeSessionId: input.sessionId ?? record.runtimeSessionId,
runtimePromptMessageId: lastRuntimePromptMessageId,
runtimePromptMessageIds,
lastRuntimePromptMessageId,
acceptanceUnknown: promptAcceptedByObservation ? false : record.acceptanceUnknown,
lastObservedAt: input.observedAt, lastObservedAt: input.observedAt,
acceptedAt: promptAcceptedByObservation
? (record.acceptedAt ?? input.observedAt)
: record.acceptedAt,
respondedAt: responded ? (record.respondedAt ?? input.observedAt) : record.respondedAt, respondedAt: responded ? (record.respondedAt ?? input.observedAt) : record.respondedAt,
deliveredUserMessageId: deliveredUserMessageId:
input.responseObservation.deliveredUserMessageId ?? record.deliveredUserMessageId, input.responseObservation.deliveredUserMessageId ?? record.deliveredUserMessageId,
@ -660,6 +735,41 @@ export function hashOpenCodePromptDeliveryPayload(input: {
})}`; })}`;
} }
export function getOpenCodeRuntimePromptMessageIds(
record: Pick<
OpenCodePromptDeliveryLedgerRecord,
'runtimePromptMessageId' | 'runtimePromptMessageIds' | 'lastRuntimePromptMessageId'
>
): string[] {
const ids: string[] = [];
for (const value of [
...(Array.isArray(record.runtimePromptMessageIds) ? record.runtimePromptMessageIds : []),
record.runtimePromptMessageId,
record.lastRuntimePromptMessageId,
]) {
const id = typeof value === 'string' ? value.trim() : '';
if (id && !ids.includes(id)) {
ids.push(id);
}
}
return ids;
}
export function getLatestOpenCodeRuntimePromptMessageId(
record: Pick<
OpenCodePromptDeliveryLedgerRecord,
'runtimePromptMessageId' | 'runtimePromptMessageIds' | 'lastRuntimePromptMessageId'
>
): string | null {
const explicit =
record.lastRuntimePromptMessageId?.trim() || record.runtimePromptMessageId?.trim();
if (explicit) {
return explicit;
}
const ids = getOpenCodeRuntimePromptMessageIds(record);
return ids[ids.length - 1] ?? null;
}
export function isOpenCodePromptResponseStateResponded( export function isOpenCodePromptResponseStateResponded(
state: OpenCodeDeliveryResponseState state: OpenCodeDeliveryResponseState
): boolean { ): boolean {
@ -720,6 +830,9 @@ function isOpenCodePromptDeliveryLedgerRecord(
isOptionalNullableString(record.runId) && isOptionalNullableString(record.runId) &&
isOptionalNullableString(record.runtimeSessionId) && isOptionalNullableString(record.runtimeSessionId) &&
isOptionalNullableString(record.runtimePromptMessageId) && isOptionalNullableString(record.runtimePromptMessageId) &&
isOptionalStringArray(record.runtimePromptMessageIds) &&
isOptionalNullableString(record.lastRuntimePromptMessageId) &&
isOptionalNullableString(record.lastDeliveryAttemptIdWithAcceptedPrompt) &&
typeof record.inboxMessageId === 'string' && typeof record.inboxMessageId === 'string' &&
typeof record.inboxTimestamp === 'string' && typeof record.inboxTimestamp === 'string' &&
isOpenCodePromptDeliverySource(record.source) && isOpenCodePromptDeliverySource(record.source) &&
@ -812,6 +925,7 @@ function isOptionalNullableInboxMessageKind(
value === 'slash_command' || value === 'slash_command' ||
value === 'slash_command_result' || value === 'slash_command_result' ||
value === 'task_comment_notification' || value === 'task_comment_notification' ||
value === 'task_stall_remediation' ||
value === 'member_work_sync_nudge' || value === 'member_work_sync_nudge' ||
value === 'agent_error' value === 'agent_error'
); );
@ -825,6 +939,10 @@ function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((item) => typeof item === 'string'); return Array.isArray(value) && value.every((item) => typeof item === 'string');
} }
function isOptionalStringArray(value: unknown): value is string[] | undefined {
return value === undefined || isStringArray(value);
}
function isNonNegativeInteger(value: unknown): value is number { function isNonNegativeInteger(value: unknown): value is number {
return Number.isInteger(value) && (value as number) >= 0; return Number.isInteger(value) && (value as number) >= 0;
} }

View file

@ -3,6 +3,7 @@ import * as path from 'node:path';
import { import {
createOpenCodePromptDeliveryLedgerStore, createOpenCodePromptDeliveryLedgerStore,
getLatestOpenCodeRuntimePromptMessageId,
type OpenCodePromptDeliveryLedgerRecord, type OpenCodePromptDeliveryLedgerRecord,
} from '../../opencode/delivery/OpenCodePromptDeliveryLedger'; } from '../../opencode/delivery/OpenCodePromptDeliveryLedger';
import { import {
@ -158,7 +159,10 @@ function toAttributionRecord(
]) ])
: undefined; : undefined;
const until = addMsToIso(terminalUntil ?? fallbackUntil, TERMINAL_EVIDENCE_GRACE_MS); const until = addMsToIso(terminalUntil ?? fallbackUntil, TERMINAL_EVIDENCE_GRACE_MS);
const startMessageUuid = record.deliveredUserMessageId?.trim() || undefined; const startMessageUuid =
record.deliveredUserMessageId?.trim() ||
getLatestOpenCodeRuntimePromptMessageId(record) ||
undefined;
return { return {
taskId: task.id, taskId: task.id,
@ -241,7 +245,9 @@ export class TaskLogOpenCodeSessionEvidenceSource implements OpenCodeTaskLogSess
record.memberName.trim().toLowerCase(), record.memberName.trim().toLowerCase(),
record.laneId.trim(), record.laneId.trim(),
sessionId, sessionId,
record.deliveredUserMessageId ?? record.inboxMessageId, record.deliveredUserMessageId ??
getLatestOpenCodeRuntimePromptMessageId(record) ??
record.inboxMessageId,
].join('::'); ].join('::');
if (seen.has(key)) { if (seen.has(key)) {
continue; continue;

View file

@ -7,6 +7,7 @@ import {
createOpenCodeBridgeIdempotencyKey, createOpenCodeBridgeIdempotencyKey,
isOpenCodeBridgeCommandName, isOpenCodeBridgeCommandName,
OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION, OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
OPEN_CODE_DELIVERY_ACCEPTANCE_CONTRACT_VERSION,
OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION, OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION,
parseSingleBridgeJsonResult, parseSingleBridgeJsonResult,
stableHash, stableHash,
@ -240,6 +241,66 @@ describe('OpenCodeBridgeCommandContract', () => {
}); });
}); });
it('requires the delivery acceptance contract only for acceptance-mode sendMessage', () => {
const client = peerIdentity('claude_team');
const server = peerIdentity('agent_teams_orchestrator');
client.bridgeProtocol.supportedCommands.push('opencode.sendMessage');
server.bridgeProtocol.supportedCommands.push('opencode.sendMessage');
let handshake = withAcceptedCommands(buildHandshake({ client, server }), [
'opencode.launchTeam',
'opencode.stopTeam',
'opencode.sendMessage',
]);
expect(
validateOpenCodeBridgeHandshake({
handshake,
expectedClient: client,
requiredCommand: 'opencode.sendMessage',
expectedCapabilitySnapshotId: null,
expectedManifestHighWatermark: 10,
expectedRunId: 'run-1',
requiresDeliveryAcceptanceContract: false,
})
).toEqual({ ok: true });
expect(
validateOpenCodeBridgeHandshake({
handshake,
expectedClient: client,
requiredCommand: 'opencode.sendMessage',
expectedCapabilitySnapshotId: null,
expectedManifestHighWatermark: 10,
expectedRunId: 'run-1',
requiresDeliveryAcceptanceContract: true,
})
).toEqual({
ok: false,
reason:
'OpenCode delivery acceptance mode is required, but the orchestrator does not advertise contract version 1. Falling back to observed delivery mode is required.',
});
server.bridgeProtocol.opencodeDeliveryAcceptanceContractVersion =
OPEN_CODE_DELIVERY_ACCEPTANCE_CONTRACT_VERSION;
handshake = withAcceptedCommands(buildHandshake({ client, server }), [
'opencode.launchTeam',
'opencode.stopTeam',
'opencode.sendMessage',
]);
expect(
validateOpenCodeBridgeHandshake({
handshake,
expectedClient: client,
requiredCommand: 'opencode.sendMessage',
expectedCapabilitySnapshotId: null,
expectedManifestHighWatermark: 10,
expectedRunId: 'run-1',
requiresDeliveryAcceptanceContract: true,
})
).toEqual({ ok: true });
});
it('creates deterministic idempotency keys for equivalent JSON bodies', () => { it('creates deterministic idempotency keys for equivalent JSON bodies', () => {
const first = createOpenCodeBridgeIdempotencyKey({ const first = createOpenCodeBridgeIdempotencyKey({
command: 'opencode.launchTeam', command: 'opencode.launchTeam',
@ -352,3 +413,18 @@ function buildHandshake(input: {
identityHash: createOpenCodeBridgeHandshakeIdentityHash(withoutHash), identityHash: createOpenCodeBridgeHandshakeIdentityHash(withoutHash),
}; };
} }
function withAcceptedCommands(
handshake: OpenCodeBridgeHandshake,
acceptedCommands: OpenCodeBridgeHandshake['acceptedCommands']
): OpenCodeBridgeHandshake {
const { identityHash: _identityHash, ...rest } = handshake;
const withoutHash: Omit<OpenCodeBridgeHandshake, 'identityHash'> = {
...rest,
acceptedCommands,
};
return {
...withoutHash,
identityHash: createOpenCodeBridgeHandshakeIdentityHash(withoutHash),
};
}

View file

@ -400,6 +400,169 @@ describe('OpenCodePromptDeliveryLedger', () => {
expect(observed.observedAssistantPreview).toBe('Понял'); expect(observed.observedAssistantPreview).toBe('Понял');
}); });
it('tracks accepted runtime prompt ids without double-counting recovered command status', async () => {
const store = createStore();
const record = await store.ensurePending({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
inboxMessageId: 'msg-accepted',
inboxTimestamp: '2026-04-25T09:59:00.000Z',
source: 'watcher',
replyRecipient: 'user',
payloadHash: 'sha256:accepted',
now: '2026-04-25T10:00:00.000Z',
});
const firstAccepted = await store.applyDeliveryResult({
id: record.id,
accepted: true,
attempted: true,
sessionId: 'oc-session-1',
runtimePromptMessageId: 'msg_prompt_1',
deliveryAttemptId: 'attempt-1',
now: '2026-04-25T10:00:05.000Z',
});
expect(firstAccepted).toMatchObject({
status: 'accepted',
attempts: 1,
runtimePromptMessageId: 'msg_prompt_1',
lastRuntimePromptMessageId: 'msg_prompt_1',
lastDeliveryAttemptIdWithAcceptedPrompt: 'attempt-1',
});
expect(firstAccepted.runtimePromptMessageIds).toEqual(['msg_prompt_1']);
const recoveredSamePrompt = await store.applyDeliveryResult({
id: record.id,
accepted: true,
attempted: true,
sessionId: 'oc-session-1',
runtimePromptMessageId: 'msg_prompt_1',
deliveryAttemptId: 'attempt-1',
now: '2026-04-25T10:00:06.000Z',
});
expect(recoveredSamePrompt.attempts).toBe(1);
expect(recoveredSamePrompt.runtimePromptMessageIds).toEqual(['msg_prompt_1']);
const retryAccepted = await store.applyDeliveryResult({
id: record.id,
accepted: true,
attempted: true,
sessionId: 'oc-session-2',
runtimePromptMessageId: 'msg_prompt_2',
deliveryAttemptId: 'attempt-2',
now: '2026-04-25T10:01:00.000Z',
});
expect(retryAccepted.attempts).toBe(2);
expect(retryAccepted.runtimePromptMessageIds).toEqual(['msg_prompt_1', 'msg_prompt_2']);
expect(retryAccepted.lastRuntimePromptMessageId).toBe('msg_prompt_2');
});
it('keeps schema-1 legacy prompt-id fields compatible and normalizes when touched', async () => {
const store = createStore();
const legacy = await store.ensurePending({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
inboxMessageId: 'msg-legacy-runtime-prompt',
inboxTimestamp: '2026-04-25T09:59:00.000Z',
source: 'watcher',
replyRecipient: 'user',
payloadHash: 'sha256:legacy-runtime-prompt',
now: '2026-04-25T10:00:00.000Z',
});
const envelope = JSON.parse(await fs.readFile(ledgerPath(), 'utf8')) as {
data: Record<string, unknown>[];
};
delete envelope.data[0].runtimePromptMessageIds;
delete envelope.data[0].lastRuntimePromptMessageId;
delete envelope.data[0].lastDeliveryAttemptIdWithAcceptedPrompt;
await fs.writeFile(ledgerPath(), `${JSON.stringify(envelope, null, 2)}\n`, 'utf8');
await expect(store.list()).resolves.toHaveLength(1);
const touched = await store.applyDeliveryResult({
id: legacy.id,
accepted: true,
attempted: true,
runtimePromptMessageId: 'msg_prompt_legacy_touch',
deliveryAttemptId: 'attempt-legacy-touch',
now: '2026-04-25T10:00:05.000Z',
});
expect(touched.runtimePromptMessageIds).toEqual(['msg_prompt_legacy_touch']);
expect(touched.lastRuntimePromptMessageId).toBe('msg_prompt_legacy_touch');
expect(touched.lastDeliveryAttemptIdWithAcceptedPrompt).toBe('attempt-legacy-touch');
});
it('accepts task stall remediation message kind across ledger validation', async () => {
const store = createStore();
const record = await store.ensurePending({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
inboxMessageId: 'task-stall:team-a:jack:task-a',
inboxTimestamp: '2026-04-25T09:59:00.000Z',
source: 'watchdog',
messageKind: 'task_stall_remediation',
replyRecipient: 'team-lead',
actionMode: 'do',
payloadHash: 'sha256:task-stall',
now: '2026-04-25T10:00:00.000Z',
});
expect(record.messageKind).toBe('task_stall_remediation');
await expect(store.list()).resolves.toMatchObject([
{ messageKind: 'task_stall_remediation' },
]);
});
it('upgrades acceptance-unknown records when exact observation finds the prompt', async () => {
const store = createStore();
const record = await store.ensurePending({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
inboxMessageId: 'msg-observed-later',
inboxTimestamp: '2026-04-25T09:59:00.000Z',
source: 'watcher',
replyRecipient: 'user',
payloadHash: 'sha256:observed-later',
now: '2026-04-25T10:00:00.000Z',
});
const unknown = await store.markAcceptanceUnknown({
id: record.id,
reason: 'opencode_prompt_acceptance_unknown_after_bridge_timeout',
nextAttemptAt: '2026-04-25T10:01:00.000Z',
markedAt: '2026-04-25T10:00:45.000Z',
});
expect(unknown.acceptanceUnknown).toBe(true);
const observed = await store.applyObservation({
id: record.id,
sessionId: 'oc-session-recovered',
runtimePromptMessageId: 'msg_prompt_recovered',
responseObservation: {
state: 'pending',
deliveredUserMessageId: 'msg_prompt_recovered',
assistantMessageId: null,
toolCallNames: [],
visibleMessageToolCallId: null,
visibleReplyMessageId: null,
visibleReplyCorrelation: null,
latestAssistantPreview: null,
reason: 'assistant_response_pending',
},
observedAt: '2026-04-25T10:00:50.000Z',
});
expect(observed.status).toBe('accepted');
expect(observed.acceptanceUnknown).toBe(false);
expect(observed.acceptedAt).toBe('2026-04-25T10:00:50.000Z');
expect(observed.runtimeSessionId).toBe('oc-session-recovered');
expect(observed.runtimePromptMessageIds).toEqual(['msg_prompt_recovered']);
});
it('keeps plain-text responses active until their visible inbox reply is materialized', async () => { it('keeps plain-text responses active until their visible inbox reply is materialized', async () => {
const store = createStore(); const store = createStore();
const record = await store.ensurePending({ const record = await store.ensurePending({

View file

@ -308,6 +308,63 @@ describe('OpenCodeReadinessBridge', () => {
); );
}); });
it('falls back to observed sendMessage when acceptance capability is missing', async () => {
const execute = vi
.fn()
.mockRejectedValueOnce(
new Error(
'OpenCode delivery acceptance mode is required, but the orchestrator does not advertise contract version 1.'
)
)
.mockResolvedValueOnce(
bridgeCommandSuccess({
command: 'opencode.sendMessage',
requestId: 'send-req-observed',
data: {
accepted: true,
memberName: 'bob',
sessionId: 'session-bob',
diagnostics: [],
},
})
);
const executor = {
execute: execute as unknown as OpenCodeReadinessBridgeCommandExecutor['execute'] &
ReturnType<typeof vi.fn>,
};
const bridge = new OpenCodeReadinessBridge(executor);
await expect(
bridge.sendOpenCodeTeamMessage({
teamId: 'team-a',
teamName: 'team-a',
laneId: 'secondary:opencode:bob',
projectPath: '/repo',
memberName: 'bob',
text: 'hello',
messageId: 'message-1',
deliveryAttemptId: 'ledger-1:1:payload',
settlementMode: 'acceptance',
})
).resolves.toMatchObject({
accepted: true,
sessionId: 'session-bob',
diagnostics: [
expect.objectContaining({
code: 'opencode_accept_fast_capability_missing',
severity: 'warning',
}),
],
});
expect(execute).toHaveBeenCalledTimes(2);
expect(execute.mock.calls[0]?.[1]).toMatchObject({ settlementMode: 'acceptance' });
expect(execute.mock.calls[1]?.[1]).toMatchObject({ settlementMode: 'observed' });
expect(execute.mock.calls[1]?.[2]).toMatchObject({
requestId: expect.stringMatching(/-observed$/),
});
});
it('recovers accepted OpenCode sendMessage after bridge timeout through commandStatus by default', async () => { it('recovers accepted OpenCode sendMessage after bridge timeout through commandStatus by default', async () => {
const executor = fakeSequenceExecutor([ const executor = fakeSequenceExecutor([
bridgeFailure('timeout', 'OpenCode bridge command timed out', []), bridgeFailure('timeout', 'OpenCode bridge command timed out', []),

View file

@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { import {
OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION, OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
OPEN_CODE_DELIVERY_ACCEPTANCE_CONTRACT_VERSION,
createOpenCodeBridgeHandshakeIdentityHash, createOpenCodeBridgeHandshakeIdentityHash,
type OpenCodeBridgeCommandName, type OpenCodeBridgeCommandName,
type OpenCodeBridgeHandshake, type OpenCodeBridgeHandshake,
@ -85,6 +86,45 @@ describe('OpenCodeStateChangingBridgeCommandService', () => {
await expect(leaseStore.getActive('team-a')).resolves.toBeNull(); await expect(leaseStore.getActive('team-a')).resolves.toBeNull();
}); });
it('requires delivery acceptance contract only for acceptance-mode sendMessage', async () => {
clientIdentity.bridgeProtocol.supportedCommands.push('opencode.sendMessage');
const server = peerIdentity('agent_teams_orchestrator');
server.bridgeProtocol.supportedCommands.push('opencode.sendMessage');
handshakePort.nextHandshake = buildHandshakeWithAcceptedCommands(
{ client: clientIdentity, server },
['opencode.launchTeam', 'opencode.stopTeam', 'opencode.sendMessage']
);
const service = createService();
await expect(service.execute(buildSendInput('acceptance'))).rejects.toThrow(
'OpenCode delivery acceptance mode is required'
);
expect(bridge.calls).toHaveLength(0);
await expect(ledger.list()).resolves.toEqual([]);
await expect(leaseStore.getActive('team-a')).resolves.toBeNull();
server.bridgeProtocol.opencodeDeliveryAcceptanceContractVersion =
OPEN_CODE_DELIVERY_ACCEPTANCE_CONTRACT_VERSION;
handshakePort.nextHandshake = buildHandshakeWithAcceptedCommands(
{ client: clientIdentity, server },
['opencode.launchTeam', 'opencode.stopTeam', 'opencode.sendMessage']
);
bridge.resultFactory = ({ body, command, options }) =>
bridgeSuccess({
requestId: options.requestId,
command,
data: {
runId: 'run-1',
idempotencyKey: body.preconditions.idempotencyKey,
runtimeStoreManifestHighWatermark: 10,
},
});
await expect(service.execute(buildSendInput('acceptance'))).resolves.toMatchObject({
ok: true,
});
expect(bridge.calls).toHaveLength(1);
});
it('adds preconditions, commits ledger, and releases lease on success', async () => { it('adds preconditions, commits ledger, and releases lease on success', async () => {
bridge.resultFactory = ({ body, options }) => bridge.resultFactory = ({ body, options }) =>
bridgeSuccess({ bridgeSuccess({
@ -227,6 +267,32 @@ function buildLaunchInput(): Parameters<OpenCodeStateChangingBridgeCommandServic
}; };
} }
function buildSendInput(
settlementMode: 'observed' | 'acceptance'
): Parameters<OpenCodeStateChangingBridgeCommandService['execute']>[0] {
return {
command: 'opencode.sendMessage',
teamName: 'team-a',
laneId: 'secondary:opencode:bob',
runId: 'run-1',
capabilitySnapshotId: null,
behaviorFingerprint: null,
body: {
runId: 'run-1',
laneId: 'secondary:opencode:bob',
teamId: 'team-a',
teamName: 'team-a',
projectPath: '/tmp/project',
memberName: 'bob',
text: 'hello',
messageId: 'msg-1',
settlementMode,
},
cwd: '/tmp/project',
timeoutMs: 10_000,
};
}
function bridgeSuccess( function bridgeSuccess(
overrides: Partial<OpenCodeBridgeSuccess<unknown>> = {} overrides: Partial<OpenCodeBridgeSuccess<unknown>> = {}
): OpenCodeBridgeSuccess<unknown> { ): OpenCodeBridgeSuccess<unknown> {
@ -313,6 +379,29 @@ function buildHandshake(input: {
}; };
} }
function buildHandshakeWithAcceptedCommands(
input: {
client: OpenCodeBridgePeerIdentity;
server: OpenCodeBridgePeerIdentity;
},
acceptedCommands: OpenCodeBridgeHandshake['acceptedCommands']
): OpenCodeBridgeHandshake {
const withoutHash: Omit<OpenCodeBridgeHandshake, 'identityHash'> = {
schemaVersion: 1,
requestId: 'handshake-1',
client: input.client,
server: input.server,
agreedProtocolVersion: 1,
acceptedCommands,
serverTime: '2026-04-21T12:00:00.000Z',
};
return {
...withoutHash,
identityHash: createOpenCodeBridgeHandshakeIdentityHash(withoutHash),
};
}
class FakeBridgeExecutor implements OpenCodeBridgeCommandExecutor { class FakeBridgeExecutor implements OpenCodeBridgeCommandExecutor {
calls: Array<{ calls: Array<{
command: OpenCodeBridgeCommandName; command: OpenCodeBridgeCommandName;

View file

@ -291,6 +291,7 @@ describe('OpenCodeTaskLogStreamSource real OpenCode fixture e2e', () => {
expect(bridge.getOpenCodeTranscript).toHaveBeenCalledWith('/tmp/agent_teams_orchestrator', { expect(bridge.getOpenCodeTranscript).toHaveBeenCalledWith('/tmp/agent_teams_orchestrator', {
teamId: 'relay-works-10', teamId: 'relay-works-10',
memberName: 'jack', memberName: 'jack',
sessionId: 'ses_23edf9243ffeSNYPWObDloBJyQ',
limit: 500, limit: 500,
}); });
}); });
@ -361,6 +362,7 @@ describe('OpenCodeTaskLogStreamSource real OpenCode fixture e2e', () => {
{ {
teamId: 'relay-works-10', teamId: 'relay-works-10',
memberName: 'jack', memberName: 'jack',
sessionId: 'stale-session-id',
limit: 500, limit: 500,
} }
); );

View file

@ -37,6 +37,9 @@ function createLedgerRecord(
laneId: 'lane-a', laneId: 'lane-a',
runId: 'run-a', runId: 'run-a',
runtimeSessionId: 'session-a', runtimeSessionId: 'session-a',
runtimePromptMessageIds: [],
lastRuntimePromptMessageId: null,
lastDeliveryAttemptIdWithAcceptedPrompt: null,
inboxMessageId: 'inbox-a', inboxMessageId: 'inbox-a',
inboxTimestamp: '2026-04-21T10:00:00.000Z', inboxTimestamp: '2026-04-21T10:00:00.000Z',
source: 'watcher', source: 'watcher',
@ -203,4 +206,33 @@ describe('TaskLogOpenCodeSessionEvidenceSource', () => {
await expect(source.readTaskRecords('team-a', createTask())).resolves.toEqual([]); await expect(source.readTaskRecords('team-a', createTask())).resolves.toEqual([]);
}); });
it('uses accepted runtime prompt id as task-log start anchor before observation catches up', async () => {
const teamsBasePath = await createTempTeamsBasePath();
await writeLedger({
teamsBasePath,
teamName: 'team-a',
laneId: 'lane-a',
records: [
createLedgerRecord({
id: 'record-accepted-only',
runtimeSessionId: 'session-accepted-only',
runtimePromptMessageId: 'msg_prompt_current',
runtimePromptMessageIds: ['msg_prompt_previous', 'msg_prompt_current'],
lastRuntimePromptMessageId: 'msg_prompt_current',
deliveredUserMessageId: null,
}),
],
});
const source = new TaskLogOpenCodeSessionEvidenceSource({ teamsBasePath });
const records = await source.readTaskRecords('team-a', createTask());
expect(records).toEqual([
expect.objectContaining({
sessionId: 'session-accepted-only',
startMessageUuid: 'msg_prompt_current',
}),
]);
});
}); });

View file

@ -206,6 +206,31 @@ describe('TeamInboxReader', () => {
}); });
}); });
it('preserves task-stall remediation semantic kind', async () => {
hoisted.files.set(
'/mock/teams/my-team/inboxes/alice.json',
JSON.stringify([
{
from: 'system',
to: 'alice',
text: 'Please continue the stalled task or report a blocker.',
timestamp: '2026-01-01T02:45:00.000Z',
read: false,
messageId: 'task-stall:my-team:alice:task-a',
source: 'system_notification',
messageKind: 'task_stall_remediation',
},
])
);
const messages = await reader.getMessagesFor('my-team', 'alice');
expect(messages).toHaveLength(1);
expect(messages[0]).toMatchObject({
messageId: 'task-stall:my-team:alice:task-a',
messageKind: 'task_stall_remediation',
});
});
it('preserves agent error semantic kind from the team lead inbox', async () => { it('preserves agent error semantic kind from the team lead inbox', async () => {
hoisted.files.set( hoisted.files.set(
'/mock/teams/my-team/inboxes/team-lead.json', '/mock/teams/my-team/inboxes/team-lead.json',

View file

@ -5426,6 +5426,7 @@ describe('TeamProvisioningService', () => {
providerId: 'opencode', providerId: 'opencode',
memberName: String(input.memberName), memberName: String(input.memberName),
sessionId: 'oc-session-bob', sessionId: 'oc-session-bob',
runtimePromptMessageId: 'msg_prompt_direct_lane',
runtimePid: 456, runtimePid: 456,
diagnostics: [], diagnostics: [],
})); }));
@ -5507,6 +5508,7 @@ describe('TeamProvisioningService', () => {
providerId: 'opencode', providerId: 'opencode',
memberName: String(input.memberName), memberName: String(input.memberName),
sessionId: 'oc-session-bob', sessionId: 'oc-session-bob',
runtimePromptMessageId: 'msg_prompt_verified_pid',
runtimePid: 456, runtimePid: 456,
diagnostics: [], diagnostics: [],
})); }));
@ -5632,6 +5634,7 @@ describe('TeamProvisioningService', () => {
providerId: 'opencode', providerId: 'opencode',
memberName: String(input.memberName), memberName: String(input.memberName),
sessionId: 'oc-session-bob', sessionId: 'oc-session-bob',
runtimePromptMessageId: 'msg_prompt_unverified_pid',
runtimePid: 456, runtimePid: 456,
diagnostics: [], diagnostics: [],
})); }));
@ -5734,6 +5737,7 @@ describe('TeamProvisioningService', () => {
providerId: 'opencode', providerId: 'opencode',
memberName: String(input.memberName), memberName: String(input.memberName),
sessionId: 'oc-session-bob', sessionId: 'oc-session-bob',
runtimePromptMessageId: 'msg_prompt_snapshot_config',
diagnostics: [], diagnostics: [],
})); }));
svc.setRuntimeAdapterRegistry( svc.setRuntimeAdapterRegistry(
@ -5824,6 +5828,7 @@ describe('TeamProvisioningService', () => {
providerId: 'opencode', providerId: 'opencode',
memberName: String(input.memberName), memberName: String(input.memberName),
sessionId: 'oc-session-bob', sessionId: 'oc-session-bob',
runtimePromptMessageId: 'msg_prompt_worktree_cwd',
diagnostics: [], diagnostics: [],
})); }));
svc.setRuntimeAdapterRegistry( svc.setRuntimeAdapterRegistry(
@ -7071,6 +7076,109 @@ describe('TeamProvisioningService', () => {
expect(ledgerEnvelope.data[0].nextAttemptAt).toBeTruthy(); expect(ledgerEnvelope.data[0].nextAttemptAt).toBeTruthy();
}); });
it('keeps accepted OpenCode responses without exact prompt identity acceptance-unknown', async () => {
const svc = new TeamProvisioningService();
const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({
ok: true,
providerId: 'opencode',
memberName: String(input.memberName),
sessionId: 'oc-session-bob',
prePromptCursor: 'cursor-before',
diagnostics: [],
}));
const registry = new TeamRuntimeAdapterRegistry([
{
providerId: 'opencode',
prepare: vi.fn(),
launch: vi.fn(),
reconcile: vi.fn(),
stop: vi.fn(),
sendMessageToMember,
observeMessageDelivery: vi.fn(),
} as any,
]);
svc.setRuntimeAdapterRegistry(registry);
(svc as any).getTrackedRunId = vi.fn(() => 'run-1');
(svc as any).provisioningRunByTeam.set('team-a', 'run-1');
(svc as any).setSecondaryRuntimeRun({
teamName: 'team-a',
runId: 'opencode-run-bob',
providerId: 'opencode',
laneId: 'secondary:opencode:bob',
memberName: 'bob',
cwd: '/repo',
});
await writeDefaultBobOpenCodeBootstrapEvidence();
(svc as any).configReader = {
getConfig: vi.fn(async () => ({
projectPath: '/repo',
members: [
{ name: 'team-lead', providerId: 'codex', model: 'gpt-5.4' },
{ name: 'bob', providerId: 'opencode', model: 'minimax-m2.5-free' },
],
})),
};
(svc as any).teamMetaStore = {
getMeta: vi.fn(async () => ({
launchIdentity: { providerId: 'codex' },
providerId: 'codex',
})),
};
(svc as any).membersMetaStore = {
getMembers: vi.fn(async () => [
{
name: 'bob',
providerId: 'opencode',
model: 'opencode/minimax-m2.5-free',
},
]),
};
await expect(
svc.deliverOpenCodeMemberMessage('team-a', {
memberName: 'bob',
text: 'Please handle this.',
messageId: 'msg-accepted-missing-prompt-id',
replyRecipient: 'user',
actionMode: 'ask',
source: 'watcher',
inboxTimestamp: '2026-04-25T10:00:00.000Z',
})
).resolves.toMatchObject({
delivered: true,
accepted: false,
responsePending: true,
acceptanceUnknown: true,
reason: 'opencode_prompt_acceptance_unknown_missing_runtime_prompt_id',
});
const ledgerPath = getOpenCodeLaneScopedRuntimeFilePath({
teamsBasePath: tempTeamsBase,
teamName: 'team-a',
laneId: 'secondary:opencode:bob',
fileName: 'opencode-prompt-delivery-ledger.json',
});
const ledgerEnvelope = JSON.parse(await fsPromises.readFile(ledgerPath, 'utf8')) as {
data: Array<{
acceptanceUnknown: boolean;
status: string;
runtimePromptMessageId: string | null;
lastReason: string | null;
diagnostics: string[];
}>;
};
expect(ledgerEnvelope.data[0]).toMatchObject({
acceptanceUnknown: true,
status: 'failed_retryable',
runtimePromptMessageId: null,
lastReason: 'opencode_prompt_acceptance_unknown_missing_runtime_prompt_id',
});
expect(ledgerEnvelope.data[0].diagnostics).toContain(
'opencode_prompt_acceptance_missing_runtime_prompt_id'
);
});
it('marks OpenCode payload hash mismatch terminal without sending a duplicate prompt', async () => { it('marks OpenCode payload hash mismatch terminal without sending a duplicate prompt', async () => {
const svc = new TeamProvisioningService(); const svc = new TeamProvisioningService();
const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({ const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({
@ -8923,6 +9031,7 @@ describe('TeamProvisioningService', () => {
providerId: 'opencode', providerId: 'opencode',
memberName: String(input.memberName), memberName: String(input.memberName),
sessionId: 'oc-session-bob', sessionId: 'oc-session-bob',
runtimePromptMessageId: 'msg_prompt_after_restart',
diagnostics: [], diagnostics: [],
})); }));
const registry = new TeamRuntimeAdapterRegistry([ const registry = new TeamRuntimeAdapterRegistry([
@ -9015,6 +9124,7 @@ describe('TeamProvisioningService', () => {
providerId: 'opencode', providerId: 'opencode',
memberName: String(input.memberName), memberName: String(input.memberName),
sessionId: 'oc-session-bob', sessionId: 'oc-session-bob',
runtimePromptMessageId: 'msg_prompt_live_lane',
diagnostics: [], diagnostics: [],
})); }));
svc.setRuntimeAdapterRegistry( svc.setRuntimeAdapterRegistry(