fix(opencode): harden team delivery reconciliation

This commit is contained in:
777genius 2026-05-09 01:52:29 +03:00
parent 3ed6dd3159
commit bcbcb621c2
5 changed files with 579 additions and 122 deletions

View file

@ -5,6 +5,7 @@ import {
type CodexNativeImageArgPart,
type OpenCodeFilePart,
} from '@features/agent-attachments/main';
import { AgentAttachmentError } from '@features/agent-attachments/core/domain';
import {
resolveAnthropicFastMode,
resolveAnthropicRuntimeSelection,
@ -322,6 +323,15 @@ import type {
} from './runtime';
import type { RuntimeTurnSettledProvider } from '@features/member-work-sync/main';
type OpenCodeRuntimeMessageAdapter = TeamLaunchRuntimeAdapter & {
sendMessageToMember(
input: OpenCodeTeamRuntimeMessageInput
): Promise<OpenCodeTeamRuntimeMessageResult>;
observeMessageDelivery?(
input: OpenCodeTeamRuntimeMessageInput & { prePromptCursor?: string | null }
): Promise<OpenCodeTeamRuntimeMessageResult>;
};
/**
* Kill a team CLI process using SIGKILL (uncatchable).
*
@ -6739,28 +6749,12 @@ export class TeamProvisioningService {
return this.runtimeAdapterRegistry.get('opencode');
}
private getOpenCodeRuntimeMessageAdapter():
| (TeamLaunchRuntimeAdapter & {
sendMessageToMember(
input: OpenCodeTeamRuntimeMessageInput
): Promise<OpenCodeTeamRuntimeMessageResult>;
observeMessageDelivery?(
input: OpenCodeTeamRuntimeMessageInput & { prePromptCursor?: string | null }
): Promise<OpenCodeTeamRuntimeMessageResult>;
})
| null {
private getOpenCodeRuntimeMessageAdapter(): OpenCodeRuntimeMessageAdapter | null {
const adapter = this.getOpenCodeRuntimeAdapter();
if (!adapter || !('sendMessageToMember' in adapter)) {
return null;
}
return adapter as TeamLaunchRuntimeAdapter & {
sendMessageToMember(
input: OpenCodeTeamRuntimeMessageInput
): Promise<OpenCodeTeamRuntimeMessageResult>;
observeMessageDelivery?(
input: OpenCodeTeamRuntimeMessageInput & { prePromptCursor?: string | null }
): Promise<OpenCodeTeamRuntimeMessageResult>;
};
return adapter as OpenCodeRuntimeMessageAdapter;
}
private resolveRuntimeRecipientProviderIdFromSources(
@ -7844,6 +7838,161 @@ export class TeamProvisioningService {
}
}
private async observeOpenCodeDirectUserDeliveryInlineIfNeeded(input: {
adapter: OpenCodeRuntimeMessageAdapter;
ledger: OpenCodePromptDeliveryLedgerStore;
ledgerRecord: OpenCodePromptDeliveryLedgerRecord;
teamName: string;
memberName: string;
laneId: string;
cwd: string;
text: string;
messageId: string;
runtimeRunId?: string | null;
replyRecipient?: string | null;
actionMode?: AgentActionMode;
messageKind?: OpenCodeTeamRuntimeMessageInput['messageKind'];
taskRefs?: TaskRef[];
promptAccepted: boolean;
visibleReply?: OpenCodeVisibleReplyProof | null;
}): Promise<{
ledgerRecord: OpenCodePromptDeliveryLedgerRecord;
visibleReply: OpenCodeVisibleReplyProof | null;
}> {
let ledgerRecord = input.ledgerRecord;
let visibleReply = input.visibleReply ?? null;
const observeMessageDelivery = input.adapter.observeMessageDelivery;
const readAllowed = this.isOpenCodeDeliveryResponseReadCommitAllowed({
responseState: ledgerRecord.responseState,
actionMode: ledgerRecord.actionMode ?? undefined,
taskRefs: ledgerRecord.taskRefs,
visibleReply,
ledgerRecord,
});
const shouldObserveInline =
observeMessageDelivery &&
input.promptAccepted &&
this.isOpenCodeDirectUserPromptDelivery(ledgerRecord) &&
(ledgerRecord.source === 'manual' ||
(ledgerRecord.responseState === 'tool_error' &&
this.hasOpenCodeObservedMessageSendToolCall(ledgerRecord))) &&
!readAllowed &&
!visibleReply &&
!ledgerRecord.visibleReplyMessageId;
if (!shouldObserveInline || !observeMessageDelivery) {
return { ledgerRecord, visibleReply };
}
for (let inlineObserveAttempt = 1; inlineObserveAttempt <= 4; inlineObserveAttempt += 1) {
await sleep(OPENCODE_PROMPT_DELIVERY_OBSERVE_DELAY_MS);
let observed: OpenCodeTeamRuntimeMessageResult;
try {
observed = await observeMessageDelivery.call(input.adapter, {
...(input.runtimeRunId ? { runId: input.runtimeRunId } : {}),
teamName: input.teamName,
laneId: input.laneId,
memberName: input.memberName,
cwd: input.cwd,
text: input.text,
messageId: input.messageId,
replyRecipient: input.replyRecipient ?? undefined,
actionMode: input.actionMode,
messageKind: input.messageKind,
taskRefs: input.taskRefs,
prePromptCursor: ledgerRecord.prePromptCursor,
});
} catch (error) {
const reason = `opencode_direct_user_delivery_inline_observe_failed: ${getErrorMessage(
error
)}`;
ledgerRecord = await input.ledger.applyObservation({
id: ledgerRecord.id,
responseObservation: {
state: 'reconcile_failed',
deliveredUserMessageId: null,
assistantMessageId: null,
toolCallNames: [],
visibleMessageToolCallId: null,
visibleReplyMessageId: null,
visibleReplyCorrelation: null,
latestAssistantPreview: null,
reason,
},
diagnostics: [
`opencode_direct_user_delivery_inline_observe_attempt_${inlineObserveAttempt}`,
reason,
],
observedAt: nowIso(),
});
break;
}
await this.rememberOpenCodeRuntimePidFromBridge({
teamName: input.teamName,
memberName: input.memberName,
laneId: input.laneId,
runId: input.runtimeRunId,
runtimeSessionId: observed.sessionId,
runtimePid: observed.runtimePid,
reason: 'opencode_delivery_inline_observe_runtime_pid_observed',
});
const observedResponse = this.normalizeOpenCodeDeliveryResponseObservation(
observed.responseObservation
);
const hadMessageSendToolError = this.hasOpenCodeObservedMessageSendToolCall(ledgerRecord);
ledgerRecord = await input.ledger.applyObservation({
id: ledgerRecord.id,
responseObservation: observedResponse ?? {
state: observed.ok ? 'not_observed' : 'reconcile_failed',
deliveredUserMessageId: null,
assistantMessageId: null,
toolCallNames: [],
visibleMessageToolCallId: null,
visibleReplyMessageId: null,
visibleReplyCorrelation: null,
latestAssistantPreview: null,
reason: observed.diagnostics[0] ?? null,
},
diagnostics: [
`opencode_direct_user_delivery_inline_observe_attempt_${inlineObserveAttempt}`,
...(hadMessageSendToolError ? ['opencode_message_send_tool_error_inline_observe'] : []),
...observed.diagnostics,
],
observedAt: nowIso(),
});
const proof = await this.applyOpenCodeVisibleDestinationProof({
ledger: input.ledger,
ledgerRecord,
teamName: input.teamName,
replyRecipient: input.replyRecipient,
memberName: input.memberName,
});
ledgerRecord = proof.ledgerRecord;
visibleReply = proof.visibleReply;
const materialized = await this.materializeOpenCodePlainTextReplyIfNeeded({
ledger: input.ledger,
ledgerRecord,
teamName: input.teamName,
memberName: input.memberName,
visibleReply,
});
ledgerRecord = materialized.ledgerRecord;
visibleReply = materialized.visibleReply;
const observedReadAllowed = this.isOpenCodeDeliveryResponseReadCommitAllowed({
responseState: ledgerRecord.responseState,
actionMode: ledgerRecord.actionMode ?? undefined,
taskRefs: ledgerRecord.taskRefs,
visibleReply,
ledgerRecord,
});
if (observedReadAllowed) {
break;
}
}
return { ledgerRecord, visibleReply };
}
private getOpenCodeDeliveryWatchdogKey(input: {
teamName: string;
memberName: string;
@ -8788,14 +8937,27 @@ export class TeamProvisioningService {
}
}
const openCodeFileParts: OpenCodeFilePart[] =
input.attachments?.length && laneIdentity.laneOwnerProviderId === 'opencode'
? buildOpenCodeAttachmentDeliveryParts({
text: input.text,
model: metaMember?.model ?? configMember?.model ?? '',
attachments: input.attachments,
}).fileParts
: [];
let openCodeFileParts: OpenCodeFilePart[] = [];
if (input.attachments?.length && laneIdentity.laneOwnerProviderId === 'opencode') {
try {
openCodeFileParts = buildOpenCodeAttachmentDeliveryParts({
text: input.text,
model: metaMember?.model ?? configMember?.model ?? '',
attachments: input.attachments,
}).fileParts;
} catch (error) {
const reason =
error instanceof AgentAttachmentError
? error.code
: 'opencode_attachment_delivery_prepare_failed';
const diagnostic = `opencode_attachment_delivery_prepare_failed: ${getErrorMessage(error)}`;
return {
delivered: false,
reason,
diagnostics: [diagnostic],
};
}
}
if (!this.isOpenCodePromptDeliveryWatchdogEnabled()) {
const result = await adapter.sendMessageToMember({
@ -9251,6 +9413,25 @@ export class TeamProvisioningService {
visibleReply: proof.visibleReply,
});
ledgerRecord = proof.ledgerRecord;
proof = await this.observeOpenCodeDirectUserDeliveryInlineIfNeeded({
adapter,
ledger,
ledgerRecord,
teamName,
memberName: canonicalMemberName,
laneId: laneIdentity.laneId,
cwd,
text: input.text,
messageId: ledgerRecord.inboxMessageId,
runtimeRunId,
replyRecipient: input.replyRecipient,
actionMode: input.actionMode,
messageKind: input.messageKind,
taskRefs: input.taskRefs,
promptAccepted,
visibleReply: proof.visibleReply,
});
ledgerRecord = proof.ledgerRecord;
this.logOpenCodePromptDeliveryEvent(
promptAccepted
? ledgerRecord.status === 'unanswered'
@ -20040,7 +20221,7 @@ export class TeamProvisioningService {
private asOpenCodeAttachmentPayload(meta: AttachmentMeta): AttachmentPayload | null {
const data = (meta as Partial<AttachmentPayload>).data;
return typeof data === 'string' && data.length > 0
return typeof data === 'string'
? {
...meta,
data,
@ -20284,6 +20465,7 @@ export class TeamProvisioningService {
const effectiveTaskRefs =
existingRecord?.taskRefs ?? options.deliveryMetadata?.taskRefs ?? message.taskRefs ?? [];
const effectiveSource = existingRecord?.source ?? options.source ?? 'watcher';
result.attempted += 1;
const attachmentPayloads = await this.resolveOpenCodeInboxAttachmentPayloads({
teamName,
message,
@ -20298,7 +20480,6 @@ export class TeamProvisioningService {
};
break;
}
result.attempted += 1;
const delivery = await this.deliverOpenCodeMemberMessage(teamName, {
memberName,
text: message.text,

View file

@ -1,5 +1,5 @@
{
"generatedAt": "2026-05-08T21:28:15.433Z",
"generatedAt": "2026-05-08T22:48:31.416Z",
"runsPerModel": 1,
"qualification": {
"minimumAverageScore": 80,
@ -25,8 +25,8 @@
"runtimeTransportFailures": 0,
"modelBehaviorFailures": 0,
"harnessFailures": 0,
"p50DurationMs": 173403,
"p95DurationMs": 173403,
"p50DurationMs": 118757,
"p95DurationMs": 118757,
"stagePassRates": {
"launchBootstrap": {
"passed": 1,
@ -217,17 +217,17 @@
"outcome": "passed",
"failureCategory": "none",
"primaryFailure": null,
"durationMs": 173403,
"durationMs": 118757,
"hardFailure": false,
"stageDurationsMs": {
"setup": 191,
"launchBootstrap": 24869,
"materializeTasks": 33,
"directReply": 11982,
"peerRelayAB": 24984,
"peerRelayBC": 24320,
"concurrentReplies": 77988,
"hygiene": 2
"setup": 225,
"launchBootstrap": 20591,
"materializeTasks": 36,
"directReply": 14820,
"peerRelayAB": 32039,
"peerRelayBC": 27306,
"concurrentReplies": 15426,
"hygiene": 1
},
"stageFailures": {},
"taskRefChecks": {
@ -253,7 +253,7 @@
"latencyStable": true
},
"diagnostics": [
"runId=55b87bf5-abe4-4c21-9b98-db1ed8c22cfb"
"runId=44f5aa40-e169-49ed-9ea3-4c72aaf4a9f1"
]
}
]

View file

@ -1,6 +1,6 @@
# OpenCode Model Gauntlet Results
Generated: 2026-05-08T21:28:15.433Z
Generated: 2026-05-08T22:48:31.416Z
Runs per model: 1
Recommended threshold: average >= 80, successful runs >= 1, consistency >= 85, hard failures = 0
@ -13,7 +13,7 @@ Scoring weights: launchBootstrap=15, directReply=10, peerRelayAB=15, peerRelayBC
| Model | Verdict | Confidence | Readiness | Consistency | Score Spread | Behavior Avg | Overall Avg | Counted | Pass Runs | Weakest Stage | Weakest TaskRef | Dominant Failure | Blockers | Provider Infra | Runtime Transport | Model Fails | Protocol Runs | p50 | p95 |
| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- | --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
| `opencode/big-pickle` | Recommended | low | 100 | 100 | 0 | 100 | 100 | 1/1 | 1/1 | cleanTranscript 1/1 (100%) | concurrentBob 1/1 (100%) | none | - | 0 | 0 | 0 | 0 | 173403ms | 173403ms |
| `opencode/big-pickle` | Recommended | low | 100 | 100 | 0 | 100 | 100 | 1/1 | 1/1 | cleanTranscript 1/1 (100%) | concurrentBob 1/1 (100%) | none | - | 0 | 0 | 0 | 0 | 118757ms | 118757ms |
## opencode/big-pickle
@ -33,5 +33,5 @@ Protocol totals: badMessages=0, duplicateOrMissingTokens=0, affectedRuns=0.
| Run | Outcome | Category | Score | Counted | Duration | Failed Stages | Slowest Stage | TaskRefs | Protocol | Diagnostics |
| ---: | --- | --- | ---: | --- | ---: | --- | --- | --- | --- | --- |
| 1 | passed | none | 100 | yes | 173403ms | - | concurrentReplies:77988ms | directReply:ok, peerRelayAB:ok, peerRelayBC:ok, concurrentBob:ok, concurrentTom:ok | - | runId=55b87bf5-abe4-4c21-9b98-db1ed8c22cfb |
| 1 | passed | none | 100 | yes | 118757ms | - | peerRelayAB:32039ms | directReply:ok, peerRelayAB:ok, peerRelayBC:ok, concurrentBob:ok, concurrentTom:ok | - | runId=44f5aa40-e169-49ed-9ea3-4c72aaf4a9f1 |

View file

@ -376,6 +376,66 @@ describe('OpenCode semantic model gauntlet report helpers', () => {
expect(markdown).toContain('Protocol totals: badMessages=1');
});
it('does not count token prefixes as duplicate user replies', async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-gauntlet-hygiene-'));
const tempClaudeRoot = path.join(tempDir, '.claude');
const teamName = 'prefix-token-team';
try {
setClaudeBasePathOverride(tempClaudeRoot);
const inboxDir = path.join(getTeamsBasePath(), teamName, 'inboxes');
await fs.mkdir(inboxDir, { recursive: true });
await fs.writeFile(
path.join(inboxDir, 'user.json'),
`${JSON.stringify(
[
{
from: 'bob',
to: 'user',
text: 'GAUNTLET_DIRECT_BOB_OK_1',
timestamp: '2026-04-26T00:00:01.000Z',
read: false,
},
{
from: 'bob',
to: 'user',
text: 'GAUNTLET_DIRECT_BOB_OK_11',
timestamp: '2026-04-26T00:00:02.000Z',
read: false,
},
{
from: 'bob',
to: 'user',
text: 'GAUNTLET_DIRECT_BOB_OK_12',
timestamp: '2026-04-26T00:00:03.000Z',
read: false,
},
],
null,
2
)}\n`,
'utf8'
);
await expect(
inspectMessageHygiene({
teamName,
members: ['bob'],
expectedUserReplyTokens: [
'GAUNTLET_DIRECT_BOB_OK_1',
'GAUNTLET_DIRECT_BOB_OK_11',
'GAUNTLET_DIRECT_BOB_OK_12',
],
})
).resolves.toMatchObject({
noDuplicateTokens: true,
duplicateOrMissingTokens: [],
});
} finally {
setClaudeBasePathOverride(null);
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
}
});
it('ranks failure impact by lost weighted score instead of raw failure count only', () => {
const model = createTestGauntletModel({
runs: [
@ -1131,6 +1191,15 @@ function hasTaskRef(message: InboxMessage, expected: TaskRef): boolean {
);
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function textContainsExactToken(text: string | undefined, token: string): boolean {
const pattern = new RegExp(`(^|[^A-Za-z0-9_])${escapeRegExp(token)}($|[^A-Za-z0-9_])`);
return pattern.test(text ?? '');
}
async function inspectMessageHygiene(input: {
teamName: string;
members: string[];
@ -1167,7 +1236,8 @@ async function inspectMessageHygiene(input: {
diagnostics.push(`badMessages=${JSON.stringify(badMessages.slice(0, 5))}`);
}
const duplicateTokens = input.expectedUserReplyTokens.filter((token) => {
const count = userMessages.filter((message) => message.text?.includes(token)).length;
const count = userMessages.filter((message) => textContainsExactToken(message.text, token))
.length;
return count !== 1;
});
if (duplicateTokens.length > 0) {

View file

@ -439,6 +439,7 @@ async function configureOpenCodeBobDeliveryService(input: {
svc: TeamProvisioningService;
sendMessageToMember: ReturnType<typeof vi.fn>;
observeMessageDelivery?: ReturnType<typeof vi.fn>;
memberModel?: string;
}): Promise<void> {
const registry = new TeamRuntimeAdapterRegistry([
{
@ -469,7 +470,11 @@ async function configureOpenCodeBobDeliveryService(input: {
projectPath: '/repo',
members: [
{ name: 'team-lead', providerId: 'codex', model: 'gpt-5.4' },
{ name: 'bob', providerId: 'opencode', model: 'minimax-m2.5-free' },
{
name: 'bob',
providerId: 'opencode',
model: input.memberModel ?? 'minimax-m2.5-free',
},
],
})),
};
@ -484,7 +489,7 @@ async function configureOpenCodeBobDeliveryService(input: {
{
name: 'bob',
providerId: 'opencode',
model: 'opencode/minimax-m2.5-free',
model: input.memberModel ?? 'opencode/minimax-m2.5-free',
},
]),
};
@ -5156,15 +5161,17 @@ describe('TeamProvisioningService', () => {
delivered: true,
diagnostics: [],
});
expect(sendMessageToMember).toHaveBeenCalledWith({
runId: 'opencode-run-bob',
teamName: 'team-a',
laneId: 'secondary:opencode:bob',
memberName: 'bob',
cwd: '/repo',
text: 'hello bob',
messageId: 'msg-1',
});
expect(sendMessageToMember).toHaveBeenCalledWith(
expect.objectContaining({
runId: 'opencode-run-bob',
teamName: 'team-a',
laneId: 'secondary:opencode:bob',
memberName: 'bob',
cwd: '/repo',
text: 'hello bob',
messageId: 'msg-1',
})
);
});
it('persists verified OpenCode bridge runtime pids so member cards can show memory', async () => {
@ -6082,7 +6089,7 @@ describe('TeamProvisioningService', () => {
});
});
it('observes OpenCode message_send tool errors quickly before retrying duplicate prompts', async () => {
it('waits through delayed OpenCode message_send tool-error fallback inline', async () => {
const svc = new TeamProvisioningService();
const taskRef = {
taskId: 'task-tool-error-observe-first',
@ -6108,36 +6115,60 @@ describe('TeamProvisioningService', () => {
},
diagnostics: ['OpenCode tool failed without output'],
}));
const observeMessageDelivery = vi.fn(async (input: Record<string, unknown>) => ({
ok: true,
let observeAttempts = 0;
const opencodeAdapter = {
providerId: 'opencode',
memberName: String(input.memberName),
sessionId: 'oc-session-bob',
responseObservation: {
state: 'responded_plain_text',
deliveredUserMessageId: 'oc-user-tool-error-observe',
assistantMessageId: 'oc-assistant-plain-fallback',
toolCallNames: ['agent-teams_message_send'],
visibleMessageToolCallId: 'call-message-send-observe',
visibleReplyMessageId: null,
visibleReplyCorrelation: 'plain_assistant_text',
latestAssistantPreview: 'GAUNTLET_OBSERVE_FIRST_OK_1',
reason: 'assistant_replied_with_plain_text',
},
diagnostics: ['Observed OpenCode plain-text fallback after message_send tool error'],
}));
svc.setRuntimeAdapterRegistry(
new TeamRuntimeAdapterRegistry([
{
prepare: vi.fn(),
launch: vi.fn(),
reconcile: vi.fn(),
stop: vi.fn(),
sendMessageToMember,
observeMessageDelivery: vi.fn(async function (
this: unknown,
input: Record<string, unknown>
) {
expect(this).toBe(opencodeAdapter);
observeAttempts += 1;
return {
ok: true,
providerId: 'opencode',
prepare: vi.fn(),
launch: vi.fn(),
reconcile: vi.fn(),
stop: vi.fn(),
sendMessageToMember,
observeMessageDelivery,
} as any,
])
memberName: String(input.memberName),
sessionId: 'oc-session-bob',
responseObservation:
observeAttempts === 1
? {
state: 'pending',
deliveredUserMessageId: 'oc-user-tool-error-observe',
assistantMessageId: null,
toolCallNames: ['agent-teams_message_send'],
visibleMessageToolCallId: 'call-message-send-observe',
visibleReplyMessageId: null,
visibleReplyCorrelation: null,
latestAssistantPreview: null,
reason: 'assistant_reply_not_visible_yet',
}
: {
state: 'responded_plain_text',
deliveredUserMessageId: 'oc-user-tool-error-observe',
assistantMessageId: 'oc-assistant-plain-fallback',
toolCallNames: ['agent-teams_message_send'],
visibleMessageToolCallId: 'call-message-send-observe',
visibleReplyMessageId: null,
visibleReplyCorrelation: 'plain_assistant_text',
latestAssistantPreview: 'GAUNTLET_OBSERVE_FIRST_OK_1',
reason: 'assistant_replied_with_plain_text',
},
diagnostics: [
observeAttempts === 1
? 'OpenCode assistant reply not visible yet'
: 'Observed OpenCode plain-text fallback after message_send tool error',
],
};
}),
} as any;
const observeMessageDelivery = opencodeAdapter.observeMessageDelivery;
svc.setRuntimeAdapterRegistry(
new TeamRuntimeAdapterRegistry([opencodeAdapter])
);
(svc as any).getTrackedRunId = vi.fn(() => 'run-1');
@ -6190,45 +6221,14 @@ describe('TeamProvisioningService', () => {
).resolves.toMatchObject({
delivered: true,
accepted: true,
responsePending: true,
responseState: 'tool_error',
reason: 'tool_error_without_required_delivery_proof',
});
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<{ nextAttemptAt: string | null }>;
};
const nextAttemptAt = ledgerEnvelope.data[0]?.nextAttemptAt;
expect(nextAttemptAt).toBeTruthy();
const delayMs = Date.parse(nextAttemptAt!) - Date.now();
expect(delayMs).toBeGreaterThanOrEqual(0);
expect(delayMs).toBeLessThanOrEqual(5_000);
ledgerEnvelope.data[0]!.nextAttemptAt = '2000-01-01T00:00:00.000Z';
await fsPromises.writeFile(ledgerPath, JSON.stringify(ledgerEnvelope, null, 2), 'utf8');
await expect(
svc.deliverOpenCodeMemberMessage('team-a', {
memberName: 'bob',
text: 'Reply to user with GAUNTLET_OBSERVE_FIRST_OK_1.',
messageId: 'msg-tool-error-observe-first',
replyRecipient: 'user',
actionMode: 'ask',
taskRefs: [taskRef],
source: 'watcher',
inboxTimestamp: '2026-04-25T10:00:00.000Z',
})
).resolves.toMatchObject({
delivered: true,
responsePending: false,
responseState: 'responded_plain_text',
visibleReplyCorrelation: 'plain_assistant_text',
diagnostics: expect.arrayContaining([
'opencode_message_send_tool_error_inline_observe',
'opencode_direct_user_delivery_inline_observe_attempt_2',
'opencode_plain_text_reply_materialized_to_user_inbox',
]),
});
const userInbox = JSON.parse(
@ -6247,13 +6247,219 @@ describe('TeamProvisioningService', () => {
taskRefs: [taskRef],
});
expect(sendMessageToMember).toHaveBeenCalledTimes(1);
expect(observeMessageDelivery).toHaveBeenCalledTimes(1);
expect(observeMessageDelivery).toHaveBeenCalledTimes(2);
expect(observeMessageDelivery).toHaveBeenCalledWith(
expect.objectContaining({
messageId: 'msg-tool-error-observe-first',
prePromptCursor: 'cursor-before-tool-error',
})
);
}, 15_000);
it('keeps accepted OpenCode delivery retryable when inline observe throws', async () => {
const svc = new TeamProvisioningService();
const taskRef = {
taskId: 'task-tool-error-observe-throws',
displayId: 'obsthrow',
teamName: 'team-a',
};
const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({
ok: true,
providerId: 'opencode',
memberName: String(input.memberName),
sessionId: 'oc-session-bob',
prePromptCursor: 'cursor-before-observe-throws',
responseObservation: {
state: 'tool_error',
deliveredUserMessageId: 'oc-user-observe-throws',
assistantMessageId: 'oc-assistant-observe-throws',
toolCallNames: ['agent-teams_message_send'],
visibleMessageToolCallId: 'call-message-send-observe-throws',
visibleReplyMessageId: null,
visibleReplyCorrelation: null,
latestAssistantPreview: null,
reason: 'message_send_tool_error_without_visible_reply_proof',
},
diagnostics: ['OpenCode tool failed without output'],
}));
const observeMessageDelivery = vi.fn(async () => {
throw new Error('observe bridge fs write failed');
});
await configureOpenCodeBobDeliveryService({
svc,
sendMessageToMember,
observeMessageDelivery,
});
await expect(
svc.deliverOpenCodeMemberMessage('team-a', {
memberName: 'bob',
text: 'Reply to user with GAUNTLET_OBSERVE_THROW_OK_1.',
messageId: 'msg-tool-error-observe-throws',
replyRecipient: 'user',
actionMode: 'ask',
taskRefs: [taskRef],
source: 'watcher',
inboxTimestamp: '2026-04-25T10:00:00.000Z',
})
).resolves.toMatchObject({
delivered: true,
accepted: true,
responsePending: true,
responseState: 'reconcile_failed',
ledgerStatus: 'retry_scheduled',
reason: expect.stringContaining('opencode_direct_user_delivery_inline_observe_failed'),
diagnostics: expect.arrayContaining([
'opencode_direct_user_delivery_inline_observe_attempt_1',
expect.stringContaining('observe bridge fs write failed'),
]),
});
expect(sendMessageToMember).toHaveBeenCalledTimes(1);
expect(observeMessageDelivery).toHaveBeenCalledTimes(1);
}, 10_000);
it('resolves stored attachment payloads for OpenCode inbox relay before delivery', 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',
responseObservation: {
state: 'responded_plain_text',
deliveredUserMessageId: 'oc-user-attachment',
assistantMessageId: 'oc-assistant-attachment',
toolCallNames: [],
visibleMessageToolCallId: null,
visibleReplyMessageId: null,
visibleReplyCorrelation: null,
latestAssistantPreview: 'I reviewed the attached image and can proceed.',
reason: 'assistant_replied_with_plain_text',
},
diagnostics: [],
}));
await configureOpenCodeBobDeliveryService({
svc,
sendMessageToMember,
memberModel: 'openai/gpt-5.4-mini',
});
await (svc as any).attachmentStore.saveAttachments('team-a', 'msg-image-attachment', [
{
id: 'att-1',
filename: 'diagram.png',
mimeType: 'image/png',
size: 5,
data: 'aW1nMQ==',
},
]);
const inboxDir = path.join(tempTeamsBase, 'team-a', 'inboxes');
await fsPromises.mkdir(inboxDir, { recursive: true });
await fsPromises.writeFile(
path.join(inboxDir, 'bob.json'),
`${JSON.stringify(
[
{
from: 'team-lead',
to: 'bob',
text: 'Review this image.',
timestamp: '2026-04-25T10:00:00.000Z',
read: false,
messageId: 'msg-image-attachment',
attachments: [
{
id: 'att-1',
filename: 'diagram.png',
mimeType: 'image/png',
size: 5,
},
],
},
],
null,
2
)}\n`,
'utf8'
);
await expect(
svc.relayOpenCodeMemberInboxMessages('team-a', 'bob', {
onlyMessageId: 'msg-image-attachment',
})
).resolves.toMatchObject({
attempted: 1,
delivered: 1,
failed: 0,
relayed: 1,
});
expect(sendMessageToMember).toHaveBeenCalledWith(
expect.objectContaining({
messageId: 'msg-image-attachment',
fileParts: [
{
type: 'file',
mime: 'image/png',
url: 'data:image/png;base64,aW1nMQ==',
filename: 'diagram.png',
},
],
})
);
});
it('keeps OpenCode inbox relay unread when attachment payload data is unavailable', async () => {
const svc = new TeamProvisioningService();
const sendMessageToMember = vi.fn();
await configureOpenCodeBobDeliveryService({ svc, sendMessageToMember });
const inboxDir = path.join(tempTeamsBase, 'team-a', 'inboxes');
await fsPromises.mkdir(inboxDir, { recursive: true });
await fsPromises.writeFile(
path.join(inboxDir, 'bob.json'),
`${JSON.stringify(
[
{
from: 'team-lead',
to: 'bob',
text: 'Review this image.',
timestamp: '2026-04-25T10:00:00.000Z',
read: false,
messageId: 'msg-missing-attachment',
attachments: [
{
id: 'missing-att',
filename: 'missing.png',
mimeType: 'image/png',
size: 5,
},
],
},
],
null,
2
)}\n`,
'utf8'
);
await expect(
svc.relayOpenCodeMemberInboxMessages('team-a', 'bob', {
onlyMessageId: 'msg-missing-attachment',
})
).resolves.toMatchObject({
attempted: 1,
delivered: 0,
failed: 1,
relayed: 0,
lastDelivery: {
delivered: false,
reason: 'opencode_inbox_attachment_payload_unavailable: missing-att',
},
});
expect(sendMessageToMember).not.toHaveBeenCalled();
const inbox = JSON.parse(await fsPromises.readFile(path.join(inboxDir, 'bob.json'), 'utf8'));
expect(inbox[0]).toMatchObject({
messageId: 'msg-missing-attachment',
read: false,
});
});
it('treats OpenCode send bridge timeouts as acceptance-unknown observe-first records', async () => {