fix(opencode): harden team delivery reconciliation
This commit is contained in:
parent
3ed6dd3159
commit
bcbcb621c2
5 changed files with 579 additions and 122 deletions
|
|
@ -5,6 +5,7 @@ import {
|
||||||
type CodexNativeImageArgPart,
|
type CodexNativeImageArgPart,
|
||||||
type OpenCodeFilePart,
|
type OpenCodeFilePart,
|
||||||
} from '@features/agent-attachments/main';
|
} from '@features/agent-attachments/main';
|
||||||
|
import { AgentAttachmentError } from '@features/agent-attachments/core/domain';
|
||||||
import {
|
import {
|
||||||
resolveAnthropicFastMode,
|
resolveAnthropicFastMode,
|
||||||
resolveAnthropicRuntimeSelection,
|
resolveAnthropicRuntimeSelection,
|
||||||
|
|
@ -322,6 +323,15 @@ import type {
|
||||||
} from './runtime';
|
} from './runtime';
|
||||||
import type { RuntimeTurnSettledProvider } from '@features/member-work-sync/main';
|
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).
|
* Kill a team CLI process using SIGKILL (uncatchable).
|
||||||
*
|
*
|
||||||
|
|
@ -6739,28 +6749,12 @@ export class TeamProvisioningService {
|
||||||
return this.runtimeAdapterRegistry.get('opencode');
|
return this.runtimeAdapterRegistry.get('opencode');
|
||||||
}
|
}
|
||||||
|
|
||||||
private getOpenCodeRuntimeMessageAdapter():
|
private getOpenCodeRuntimeMessageAdapter(): OpenCodeRuntimeMessageAdapter | null {
|
||||||
| (TeamLaunchRuntimeAdapter & {
|
|
||||||
sendMessageToMember(
|
|
||||||
input: OpenCodeTeamRuntimeMessageInput
|
|
||||||
): Promise<OpenCodeTeamRuntimeMessageResult>;
|
|
||||||
observeMessageDelivery?(
|
|
||||||
input: OpenCodeTeamRuntimeMessageInput & { prePromptCursor?: string | null }
|
|
||||||
): Promise<OpenCodeTeamRuntimeMessageResult>;
|
|
||||||
})
|
|
||||||
| null {
|
|
||||||
const adapter = this.getOpenCodeRuntimeAdapter();
|
const adapter = this.getOpenCodeRuntimeAdapter();
|
||||||
if (!adapter || !('sendMessageToMember' in adapter)) {
|
if (!adapter || !('sendMessageToMember' in adapter)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return adapter as TeamLaunchRuntimeAdapter & {
|
return adapter as OpenCodeRuntimeMessageAdapter;
|
||||||
sendMessageToMember(
|
|
||||||
input: OpenCodeTeamRuntimeMessageInput
|
|
||||||
): Promise<OpenCodeTeamRuntimeMessageResult>;
|
|
||||||
observeMessageDelivery?(
|
|
||||||
input: OpenCodeTeamRuntimeMessageInput & { prePromptCursor?: string | null }
|
|
||||||
): Promise<OpenCodeTeamRuntimeMessageResult>;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveRuntimeRecipientProviderIdFromSources(
|
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: {
|
private getOpenCodeDeliveryWatchdogKey(input: {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
memberName: string;
|
memberName: string;
|
||||||
|
|
@ -8788,14 +8937,27 @@ export class TeamProvisioningService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openCodeFileParts: OpenCodeFilePart[] =
|
let openCodeFileParts: OpenCodeFilePart[] = [];
|
||||||
input.attachments?.length && laneIdentity.laneOwnerProviderId === 'opencode'
|
if (input.attachments?.length && laneIdentity.laneOwnerProviderId === 'opencode') {
|
||||||
? buildOpenCodeAttachmentDeliveryParts({
|
try {
|
||||||
text: input.text,
|
openCodeFileParts = buildOpenCodeAttachmentDeliveryParts({
|
||||||
model: metaMember?.model ?? configMember?.model ?? '',
|
text: input.text,
|
||||||
attachments: input.attachments,
|
model: metaMember?.model ?? configMember?.model ?? '',
|
||||||
}).fileParts
|
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()) {
|
if (!this.isOpenCodePromptDeliveryWatchdogEnabled()) {
|
||||||
const result = await adapter.sendMessageToMember({
|
const result = await adapter.sendMessageToMember({
|
||||||
|
|
@ -9251,6 +9413,25 @@ export class TeamProvisioningService {
|
||||||
visibleReply: proof.visibleReply,
|
visibleReply: proof.visibleReply,
|
||||||
});
|
});
|
||||||
ledgerRecord = proof.ledgerRecord;
|
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(
|
this.logOpenCodePromptDeliveryEvent(
|
||||||
promptAccepted
|
promptAccepted
|
||||||
? ledgerRecord.status === 'unanswered'
|
? ledgerRecord.status === 'unanswered'
|
||||||
|
|
@ -20040,7 +20221,7 @@ export class TeamProvisioningService {
|
||||||
|
|
||||||
private asOpenCodeAttachmentPayload(meta: AttachmentMeta): AttachmentPayload | null {
|
private asOpenCodeAttachmentPayload(meta: AttachmentMeta): AttachmentPayload | null {
|
||||||
const data = (meta as Partial<AttachmentPayload>).data;
|
const data = (meta as Partial<AttachmentPayload>).data;
|
||||||
return typeof data === 'string' && data.length > 0
|
return typeof data === 'string'
|
||||||
? {
|
? {
|
||||||
...meta,
|
...meta,
|
||||||
data,
|
data,
|
||||||
|
|
@ -20284,6 +20465,7 @@ export class TeamProvisioningService {
|
||||||
const effectiveTaskRefs =
|
const effectiveTaskRefs =
|
||||||
existingRecord?.taskRefs ?? options.deliveryMetadata?.taskRefs ?? message.taskRefs ?? [];
|
existingRecord?.taskRefs ?? options.deliveryMetadata?.taskRefs ?? message.taskRefs ?? [];
|
||||||
const effectiveSource = existingRecord?.source ?? options.source ?? 'watcher';
|
const effectiveSource = existingRecord?.source ?? options.source ?? 'watcher';
|
||||||
|
result.attempted += 1;
|
||||||
const attachmentPayloads = await this.resolveOpenCodeInboxAttachmentPayloads({
|
const attachmentPayloads = await this.resolveOpenCodeInboxAttachmentPayloads({
|
||||||
teamName,
|
teamName,
|
||||||
message,
|
message,
|
||||||
|
|
@ -20298,7 +20480,6 @@ export class TeamProvisioningService {
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
result.attempted += 1;
|
|
||||||
const delivery = await this.deliverOpenCodeMemberMessage(teamName, {
|
const delivery = await this.deliverOpenCodeMemberMessage(teamName, {
|
||||||
memberName,
|
memberName,
|
||||||
text: message.text,
|
text: message.text,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"generatedAt": "2026-05-08T21:28:15.433Z",
|
"generatedAt": "2026-05-08T22:48:31.416Z",
|
||||||
"runsPerModel": 1,
|
"runsPerModel": 1,
|
||||||
"qualification": {
|
"qualification": {
|
||||||
"minimumAverageScore": 80,
|
"minimumAverageScore": 80,
|
||||||
|
|
@ -25,8 +25,8 @@
|
||||||
"runtimeTransportFailures": 0,
|
"runtimeTransportFailures": 0,
|
||||||
"modelBehaviorFailures": 0,
|
"modelBehaviorFailures": 0,
|
||||||
"harnessFailures": 0,
|
"harnessFailures": 0,
|
||||||
"p50DurationMs": 173403,
|
"p50DurationMs": 118757,
|
||||||
"p95DurationMs": 173403,
|
"p95DurationMs": 118757,
|
||||||
"stagePassRates": {
|
"stagePassRates": {
|
||||||
"launchBootstrap": {
|
"launchBootstrap": {
|
||||||
"passed": 1,
|
"passed": 1,
|
||||||
|
|
@ -217,17 +217,17 @@
|
||||||
"outcome": "passed",
|
"outcome": "passed",
|
||||||
"failureCategory": "none",
|
"failureCategory": "none",
|
||||||
"primaryFailure": null,
|
"primaryFailure": null,
|
||||||
"durationMs": 173403,
|
"durationMs": 118757,
|
||||||
"hardFailure": false,
|
"hardFailure": false,
|
||||||
"stageDurationsMs": {
|
"stageDurationsMs": {
|
||||||
"setup": 191,
|
"setup": 225,
|
||||||
"launchBootstrap": 24869,
|
"launchBootstrap": 20591,
|
||||||
"materializeTasks": 33,
|
"materializeTasks": 36,
|
||||||
"directReply": 11982,
|
"directReply": 14820,
|
||||||
"peerRelayAB": 24984,
|
"peerRelayAB": 32039,
|
||||||
"peerRelayBC": 24320,
|
"peerRelayBC": 27306,
|
||||||
"concurrentReplies": 77988,
|
"concurrentReplies": 15426,
|
||||||
"hygiene": 2
|
"hygiene": 1
|
||||||
},
|
},
|
||||||
"stageFailures": {},
|
"stageFailures": {},
|
||||||
"taskRefChecks": {
|
"taskRefChecks": {
|
||||||
|
|
@ -253,7 +253,7 @@
|
||||||
"latencyStable": true
|
"latencyStable": true
|
||||||
},
|
},
|
||||||
"diagnostics": [
|
"diagnostics": [
|
||||||
"runId=55b87bf5-abe4-4c21-9b98-db1ed8c22cfb"
|
"runId=44f5aa40-e169-49ed-9ea3-4c72aaf4a9f1"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# OpenCode Model Gauntlet Results
|
# OpenCode Model Gauntlet Results
|
||||||
|
|
||||||
Generated: 2026-05-08T21:28:15.433Z
|
Generated: 2026-05-08T22:48:31.416Z
|
||||||
|
|
||||||
Runs per model: 1
|
Runs per model: 1
|
||||||
Recommended threshold: average >= 80, successful runs >= 1, consistency >= 85, hard failures = 0
|
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 |
|
| 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
|
## 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 |
|
| 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 |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -376,6 +376,66 @@ describe('OpenCode semantic model gauntlet report helpers', () => {
|
||||||
expect(markdown).toContain('Protocol totals: badMessages=1');
|
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', () => {
|
it('ranks failure impact by lost weighted score instead of raw failure count only', () => {
|
||||||
const model = createTestGauntletModel({
|
const model = createTestGauntletModel({
|
||||||
runs: [
|
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: {
|
async function inspectMessageHygiene(input: {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
members: string[];
|
members: string[];
|
||||||
|
|
@ -1167,7 +1236,8 @@ async function inspectMessageHygiene(input: {
|
||||||
diagnostics.push(`badMessages=${JSON.stringify(badMessages.slice(0, 5))}`);
|
diagnostics.push(`badMessages=${JSON.stringify(badMessages.slice(0, 5))}`);
|
||||||
}
|
}
|
||||||
const duplicateTokens = input.expectedUserReplyTokens.filter((token) => {
|
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;
|
return count !== 1;
|
||||||
});
|
});
|
||||||
if (duplicateTokens.length > 0) {
|
if (duplicateTokens.length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -439,6 +439,7 @@ async function configureOpenCodeBobDeliveryService(input: {
|
||||||
svc: TeamProvisioningService;
|
svc: TeamProvisioningService;
|
||||||
sendMessageToMember: ReturnType<typeof vi.fn>;
|
sendMessageToMember: ReturnType<typeof vi.fn>;
|
||||||
observeMessageDelivery?: ReturnType<typeof vi.fn>;
|
observeMessageDelivery?: ReturnType<typeof vi.fn>;
|
||||||
|
memberModel?: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const registry = new TeamRuntimeAdapterRegistry([
|
const registry = new TeamRuntimeAdapterRegistry([
|
||||||
{
|
{
|
||||||
|
|
@ -469,7 +470,11 @@ async function configureOpenCodeBobDeliveryService(input: {
|
||||||
projectPath: '/repo',
|
projectPath: '/repo',
|
||||||
members: [
|
members: [
|
||||||
{ name: 'team-lead', providerId: 'codex', model: 'gpt-5.4' },
|
{ 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',
|
name: 'bob',
|
||||||
providerId: 'opencode',
|
providerId: 'opencode',
|
||||||
model: 'opencode/minimax-m2.5-free',
|
model: input.memberModel ?? 'opencode/minimax-m2.5-free',
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
|
|
@ -5156,15 +5161,17 @@ describe('TeamProvisioningService', () => {
|
||||||
delivered: true,
|
delivered: true,
|
||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
});
|
});
|
||||||
expect(sendMessageToMember).toHaveBeenCalledWith({
|
expect(sendMessageToMember).toHaveBeenCalledWith(
|
||||||
runId: 'opencode-run-bob',
|
expect.objectContaining({
|
||||||
teamName: 'team-a',
|
runId: 'opencode-run-bob',
|
||||||
laneId: 'secondary:opencode:bob',
|
teamName: 'team-a',
|
||||||
memberName: 'bob',
|
laneId: 'secondary:opencode:bob',
|
||||||
cwd: '/repo',
|
memberName: 'bob',
|
||||||
text: 'hello bob',
|
cwd: '/repo',
|
||||||
messageId: 'msg-1',
|
text: 'hello bob',
|
||||||
});
|
messageId: 'msg-1',
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('persists verified OpenCode bridge runtime pids so member cards can show memory', async () => {
|
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 svc = new TeamProvisioningService();
|
||||||
const taskRef = {
|
const taskRef = {
|
||||||
taskId: 'task-tool-error-observe-first',
|
taskId: 'task-tool-error-observe-first',
|
||||||
|
|
@ -6108,36 +6115,60 @@ describe('TeamProvisioningService', () => {
|
||||||
},
|
},
|
||||||
diagnostics: ['OpenCode tool failed without output'],
|
diagnostics: ['OpenCode tool failed without output'],
|
||||||
}));
|
}));
|
||||||
const observeMessageDelivery = vi.fn(async (input: Record<string, unknown>) => ({
|
let observeAttempts = 0;
|
||||||
ok: true,
|
const opencodeAdapter = {
|
||||||
providerId: 'opencode',
|
providerId: 'opencode',
|
||||||
memberName: String(input.memberName),
|
prepare: vi.fn(),
|
||||||
sessionId: 'oc-session-bob',
|
launch: vi.fn(),
|
||||||
responseObservation: {
|
reconcile: vi.fn(),
|
||||||
state: 'responded_plain_text',
|
stop: vi.fn(),
|
||||||
deliveredUserMessageId: 'oc-user-tool-error-observe',
|
sendMessageToMember,
|
||||||
assistantMessageId: 'oc-assistant-plain-fallback',
|
observeMessageDelivery: vi.fn(async function (
|
||||||
toolCallNames: ['agent-teams_message_send'],
|
this: unknown,
|
||||||
visibleMessageToolCallId: 'call-message-send-observe',
|
input: Record<string, unknown>
|
||||||
visibleReplyMessageId: null,
|
) {
|
||||||
visibleReplyCorrelation: 'plain_assistant_text',
|
expect(this).toBe(opencodeAdapter);
|
||||||
latestAssistantPreview: 'GAUNTLET_OBSERVE_FIRST_OK_1',
|
observeAttempts += 1;
|
||||||
reason: 'assistant_replied_with_plain_text',
|
return {
|
||||||
},
|
ok: true,
|
||||||
diagnostics: ['Observed OpenCode plain-text fallback after message_send tool error'],
|
|
||||||
}));
|
|
||||||
svc.setRuntimeAdapterRegistry(
|
|
||||||
new TeamRuntimeAdapterRegistry([
|
|
||||||
{
|
|
||||||
providerId: 'opencode',
|
providerId: 'opencode',
|
||||||
prepare: vi.fn(),
|
memberName: String(input.memberName),
|
||||||
launch: vi.fn(),
|
sessionId: 'oc-session-bob',
|
||||||
reconcile: vi.fn(),
|
responseObservation:
|
||||||
stop: vi.fn(),
|
observeAttempts === 1
|
||||||
sendMessageToMember,
|
? {
|
||||||
observeMessageDelivery,
|
state: 'pending',
|
||||||
} as any,
|
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');
|
(svc as any).getTrackedRunId = vi.fn(() => 'run-1');
|
||||||
|
|
@ -6190,45 +6221,14 @@ describe('TeamProvisioningService', () => {
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
delivered: true,
|
delivered: true,
|
||||||
accepted: 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,
|
responsePending: false,
|
||||||
responseState: 'responded_plain_text',
|
responseState: 'responded_plain_text',
|
||||||
visibleReplyCorrelation: 'plain_assistant_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(
|
const userInbox = JSON.parse(
|
||||||
|
|
@ -6247,13 +6247,219 @@ describe('TeamProvisioningService', () => {
|
||||||
taskRefs: [taskRef],
|
taskRefs: [taskRef],
|
||||||
});
|
});
|
||||||
expect(sendMessageToMember).toHaveBeenCalledTimes(1);
|
expect(sendMessageToMember).toHaveBeenCalledTimes(1);
|
||||||
expect(observeMessageDelivery).toHaveBeenCalledTimes(1);
|
expect(observeMessageDelivery).toHaveBeenCalledTimes(2);
|
||||||
expect(observeMessageDelivery).toHaveBeenCalledWith(
|
expect(observeMessageDelivery).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
messageId: 'msg-tool-error-observe-first',
|
messageId: 'msg-tool-error-observe-first',
|
||||||
prePromptCursor: 'cursor-before-tool-error',
|
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 () => {
|
it('treats OpenCode send bridge timeouts as acceptance-unknown observe-first records', async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue