fix(team): harden opencode launch diagnostics

This commit is contained in:
777genius 2026-05-05 00:47:05 +03:00
parent 3d8d2395a4
commit c8edfc6026
10 changed files with 1426 additions and 47 deletions

View file

@ -1,27 +1,27 @@
{
"version": "0.0.16",
"sourceRef": "v0.0.16",
"version": "0.0.17",
"sourceRef": "v0.0.17",
"sourceRepository": "777genius/agent_teams_orchestrator",
"releaseRepository": "777genius/claude_agent_teams_ui",
"releaseTag": "v1.2.0",
"assets": {
"darwin-arm64": {
"file": "agent-teams-runtime-darwin-arm64-v0.0.16.tar.gz",
"file": "agent-teams-runtime-darwin-arm64-v0.0.17.tar.gz",
"archiveKind": "tar.gz",
"binaryName": "claude-multimodel"
},
"darwin-x64": {
"file": "agent-teams-runtime-darwin-x64-v0.0.16.tar.gz",
"file": "agent-teams-runtime-darwin-x64-v0.0.17.tar.gz",
"archiveKind": "tar.gz",
"binaryName": "claude-multimodel"
},
"linux-x64": {
"file": "agent-teams-runtime-linux-x64-v0.0.16.tar.gz",
"file": "agent-teams-runtime-linux-x64-v0.0.17.tar.gz",
"archiveKind": "tar.gz",
"binaryName": "claude-multimodel"
},
"win32-x64": {
"file": "agent-teams-runtime-win32-x64-v0.0.16.zip",
"file": "agent-teams-runtime-win32-x64-v0.0.17.zip",
"archiveKind": "zip",
"binaryName": "claude-multimodel.exe"
}

View file

@ -48,6 +48,7 @@ export interface MixedSecondaryLaneMemberStateInput {
runtimeDiagnostic?: string;
runtimeDiagnosticSeverity?: TeamAgentRuntimeDiagnosticSeverity;
bootstrapStalled?: boolean;
firstSpawnAcceptedAt?: string;
diagnostics?: string[];
} | null;
pendingReason?: string;
@ -108,6 +109,59 @@ function hasMaterializedOpenCodeRuntimeMarker(value: {
);
}
const OPENCODE_MEMBER_SESSION_RECORDED_AT_PATTERN =
/\bmember_session_recorded\s+at\s+([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9:.+-]+Z?)\b/i;
function normalizeIsoTimestamp(value: unknown): string | null {
if (typeof value !== 'string') {
return null;
}
const trimmed = value.trim();
if (!trimmed) {
return null;
}
const parsed = Date.parse(trimmed);
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
}
function selectEarliestIsoTimestamp(values: readonly unknown[]): string | undefined {
let selected: { value: string; timeMs: number } | null = null;
for (const value of values) {
const normalized = normalizeIsoTimestamp(value);
if (!normalized) {
continue;
}
const timeMs = Date.parse(normalized);
if (!selected || timeMs < selected.timeMs) {
selected = { value: normalized, timeMs };
}
}
return selected?.value;
}
function extractOpenCodeMemberSessionRecordedAt(
diagnostics: readonly string[] | undefined
): string[] {
return (diagnostics ?? []).flatMap((diagnostic) => {
const match = diagnostic.match(OPENCODE_MEMBER_SESSION_RECORDED_AT_PATTERN);
return match?.[1] ? [match[1]] : [];
});
}
function resolveOpenCodeSecondaryFirstSpawnAcceptedAt(
evidence: NonNullable<MixedSecondaryLaneMemberStateInput['evidence']>,
fallbackUpdatedAt: string
): string | undefined {
if (evidence.agentToolAccepted !== true) {
return undefined;
}
return selectEarliestIsoTimestamp([
evidence.firstSpawnAcceptedAt,
...extractOpenCodeMemberSessionRecordedAt(evidence.diagnostics),
fallbackUpdatedAt,
]);
}
function buildDiagnostics(
member: Pick<
PersistedTeamLaunchMemberState,
@ -252,6 +306,9 @@ function createSecondaryLaneMemberState(
});
const hardFailure = evidence?.hardFailure === true || launchState === 'failed_to_start';
const hardFailureReason = hardFailure ? evidence?.hardFailureReason : undefined;
const firstSpawnAcceptedAt = evidence
? resolveOpenCodeSecondaryFirstSpawnAcceptedAt(evidence, params.updatedAt)
: undefined;
const base: PersistedTeamLaunchMemberState = {
name: params.member.name.trim(),
providerId,
@ -304,7 +361,7 @@ function createSecondaryLaneMemberState(
hardFailure !== true
? true
: undefined,
firstSpawnAcceptedAt: evidence?.agentToolAccepted ? params.updatedAt : undefined,
firstSpawnAcceptedAt,
lastHeartbeatAt: evidence?.bootstrapConfirmed ? params.updatedAt : undefined,
runtimeLastSeenAt: strongRuntimeAlive ? params.updatedAt : undefined,
lastRuntimeAliveAt: strongRuntimeAlive ? params.updatedAt : undefined,

File diff suppressed because it is too large Load diff

View file

@ -141,7 +141,7 @@ export interface ApplyOpenCodePromptDestinationProofInput {
id: string;
visibleReplyInbox: string;
visibleReplyMessageId: string;
visibleReplyCorrelation: 'relayOfMessageId';
visibleReplyCorrelation: OpenCodeDeliveryVisibleReplyCorrelation;
semanticallySufficient: boolean;
diagnostics?: string[];
observedAt: string;
@ -357,10 +357,14 @@ export class OpenCodePromptDeliveryLedgerStore {
async applyDestinationProof(
input: ApplyOpenCodePromptDestinationProofInput
): Promise<OpenCodePromptDeliveryLedgerRecord> {
const responseState =
input.visibleReplyCorrelation === 'plain_assistant_text'
? 'responded_plain_text'
: 'responded_visible_message';
return await this.updateExisting(input.id, (record) => ({
...record,
status: input.semanticallySufficient ? 'responded' : record.status,
responseState: 'responded_visible_message',
responseState,
lastObservedAt: input.observedAt,
respondedAt: input.semanticallySufficient
? (record.respondedAt ?? input.observedAt)
@ -786,6 +790,14 @@ function isTaskRefArray(value: unknown): value is TaskRef[] {
}
function isTerminalForAutomaticSelection(record: OpenCodePromptDeliveryLedgerRecord): boolean {
if (
record.status === 'responded' &&
record.responseState === 'responded_plain_text' &&
!record.visibleReplyMessageId &&
!record.inboxReadCommittedAt
) {
return false;
}
return record.status === 'failed_terminal' || record.status === 'responded';
}

View file

@ -59,6 +59,10 @@ export interface OpenCodeTeamRuntimeMessageInput {
replyRecipient?: string;
actionMode?: AgentActionMode;
taskRefs?: TaskRef[];
bootstrapCheckinRetry?: {
runtimeSessionId: string;
reason?: string;
};
}
export interface OpenCodeTeamRuntimeMessageResult {
@ -743,6 +747,30 @@ function buildMemberBootstrapPrompt(
}
function buildOpenCodeRuntimeMessageText(input: OpenCodeTeamRuntimeMessageInput): string {
if (input.bootstrapCheckinRetry) {
const runtimeSessionId = input.bootstrapCheckinRetry.runtimeSessionId.trim();
return [
'<opencode_runtime_bootstrap_checkin_retry>',
'The desktop app detected that this OpenCode session exists, but runtime_bootstrap_checkin has not committed durable runtime evidence yet.',
input.bootstrapCheckinRetry.reason
? `Reason: ${input.bootstrapCheckinRetry.reason.trim()}`
: null,
'Before any other tool or message, call MCP tool agent-teams_runtime_bootstrap_checkin or mcp__agent-teams__runtime_bootstrap_checkin with exactly:',
JSON.stringify({
runId: input.runId,
teamName: input.teamName,
memberName: input.memberName,
runtimeSessionId,
}),
'Do not call member_briefing, task tools, message_send, or cross_team_send before runtime_bootstrap_checkin completes.',
'After runtime_bootstrap_checkin succeeds, stop this turn immediately and wait silently.',
'If runtime_bootstrap_checkin is unavailable or fails, reply with one short sentence containing the exact error text, then stop.',
'</opencode_runtime_bootstrap_checkin_retry>',
]
.filter((line): line is string => line !== null)
.join('\n');
}
const replyRecipient = input.replyRecipient?.trim() || 'user';
const deliveryContext =
input.messageId && input.taskRefs?.length

View file

@ -22,6 +22,17 @@ const PENDING_WARNING =
const FAILED_WARNING =
'OpenCode runtime delivery failed. Message was saved to inbox, but live delivery did not complete.';
function formatOpenCodeRuntimeDeliveryFailureReason(reason: string | null | undefined): string {
const normalized = reason?.trim();
if (!normalized) {
return '';
}
if (normalized === 'empty_assistant_turn') {
return 'OpenCode returned an empty assistant turn.';
}
return '';
}
export function buildOpenCodeRuntimeDeliveryDiagnostics(
result: SendMessageResult
): OpenCodeRuntimeDeliveryDiagnostics {
@ -36,8 +47,19 @@ export function buildOpenCodeRuntimeDeliveryDiagnostics(
return { warning: null, debugDetails: null };
}
const failureReason = isFailed
? formatOpenCodeRuntimeDeliveryFailureReason(
runtimeDelivery.reason ?? runtimeDelivery.diagnostics?.[0]
)
: '';
return {
warning: isFailed ? FAILED_WARNING : PENDING_WARNING,
warning:
isFailed && failureReason
? `${FAILED_WARNING} Reason: ${failureReason}`
: isFailed
? FAILED_WARNING
: PENDING_WARNING,
debugDetails: {
messageId: result.messageId,
providerId: runtimeDelivery.providerId,

View file

@ -286,6 +286,71 @@ describe('OpenCodePromptDeliveryLedger', () => {
expect(observed.observedAssistantPreview).toBe('Понял');
});
it('keeps plain-text responses active until their visible inbox reply is materialized', async () => {
const store = createStore();
const record = await store.ensurePending({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
inboxMessageId: 'msg-plain-visible',
inboxTimestamp: '2026-04-25T09:59:00.000Z',
source: 'watcher',
replyRecipient: 'user',
actionMode: 'ask',
taskRefs: [],
payloadHash: 'sha256:plain-visible',
now: '2026-04-25T10:00:00.000Z',
});
const responded = await store.applyDeliveryResult({
id: record.id,
accepted: true,
attempted: true,
responseObservation: {
state: 'responded_plain_text',
deliveredUserMessageId: 'oc-user-plain',
assistantMessageId: 'oc-assistant-plain',
toolCallNames: [],
visibleMessageToolCallId: null,
visibleReplyMessageId: null,
visibleReplyCorrelation: null,
latestAssistantPreview: 'Concrete visible answer.',
reason: null,
},
now: '2026-04-25T10:00:05.000Z',
});
expect(responded.status).toBe('responded');
await expect(store.getActiveForMember({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
})).resolves.toMatchObject({
id: record.id,
responseState: 'responded_plain_text',
});
const materialized = await store.applyDestinationProof({
id: record.id,
visibleReplyInbox: 'user',
visibleReplyMessageId: 'opencode-plain-reply-1',
visibleReplyCorrelation: 'plain_assistant_text',
semanticallySufficient: true,
observedAt: '2026-04-25T10:00:06.000Z',
});
expect(materialized).toMatchObject({
status: 'responded',
responseState: 'responded_plain_text',
visibleReplyCorrelation: 'plain_assistant_text',
});
await expect(store.getActiveForMember({
teamName: 'team-a',
memberName: 'jack',
laneId: 'secondary:opencode:jack',
})).resolves.toBeNull();
});
it('does not keep responded live deliveries active when no inbox commit is needed', async () => {
const store = createStore();
const direct = await store.ensurePending({

View file

@ -895,6 +895,133 @@ describe('TeamProvisioningService', () => {
expect(payload.body).not.toContain('0/4');
expect(payload.body).not.toContain('did not join');
});
it('does not report persisted bootstrap-confirmed primary members as failed from a stale failed list', async () => {
const { NotificationManager } =
await import('@main/services/infrastructure/NotificationManager');
const addTeamNotification = vi.fn(async (_payload: unknown) => undefined);
NotificationManager.setInstance({ addTeamNotification } as never);
try {
const svc = new TeamProvisioningService();
const run = {
runId: 'run-forge-labs-15',
teamName: 'forge-labs-15',
isLaunch: true,
request: {
cwd: tempClaudeRoot,
displayName: 'forge-labs-15',
},
expectedMembers: ['bob', 'jack', 'alice', 'tom'],
allEffectiveMembers: [
{ name: 'bob' },
{ name: 'jack' },
{ name: 'alice' },
{ name: 'tom' },
],
memberSpawnStatuses: new Map([
[
'bob',
createMemberSpawnStatusEntry({
status: 'error',
launchState: 'failed_to_start',
runtimeAlive: false,
bootstrapConfirmed: false,
hardFailure: true,
hardFailureReason: 'Teammate was never spawned during launch.',
}),
],
[
'jack',
createMemberSpawnStatusEntry({
status: 'error',
launchState: 'failed_to_start',
runtimeAlive: false,
bootstrapConfirmed: false,
hardFailure: true,
hardFailureReason: 'Teammate was never spawned during launch.',
}),
],
[
'alice',
createMemberSpawnStatusEntry({
status: 'waiting',
launchState: 'runtime_pending_bootstrap',
runtimeAlive: false,
bootstrapConfirmed: false,
}),
],
[
'tom',
createMemberSpawnStatusEntry({
status: 'online',
launchState: 'confirmed_alive',
runtimeAlive: true,
bootstrapConfirmed: true,
}),
],
]),
};
const reconciledSnapshot = {
expectedMembers: ['bob', 'jack', 'alice', 'tom'],
members: {
bob: {
name: 'bob',
launchState: 'confirmed_alive',
agentToolAccepted: true,
runtimeAlive: true,
bootstrapConfirmed: true,
hardFailure: false,
lastEvaluatedAt: '2026-05-04T19:32:37.000Z',
},
jack: {
name: 'jack',
launchState: 'confirmed_alive',
agentToolAccepted: true,
runtimeAlive: true,
bootstrapConfirmed: true,
hardFailure: false,
lastEvaluatedAt: '2026-05-04T19:32:30.000Z',
},
alice: {
name: 'alice',
launchState: 'runtime_pending_bootstrap',
agentToolAccepted: true,
runtimeAlive: false,
bootstrapConfirmed: false,
hardFailure: false,
lastEvaluatedAt: '2026-05-04T19:35:49.000Z',
},
tom: {
name: 'tom',
launchState: 'confirmed_alive',
agentToolAccepted: true,
runtimeAlive: true,
bootstrapConfirmed: true,
hardFailure: false,
lastEvaluatedAt: '2026-05-04T19:35:49.000Z',
},
},
summary: {
confirmedCount: 3,
pendingCount: 1,
failedCount: 0,
runtimeAlivePendingCount: 0,
},
};
await (svc as any).fireTeamLaunchIncompleteNotification(
run,
[{ name: 'bob' }, { name: 'jack' }],
reconciledSnapshot.summary,
reconciledSnapshot
);
} finally {
NotificationManager.resetInstance();
}
expect(addTeamNotification).not.toHaveBeenCalled();
});
});
describe('getClaudeLogs', () => {
@ -4882,7 +5009,7 @@ describe('TeamProvisioningService', () => {
visibleMessageToolCallId: null,
visibleReplyMessageId: null,
visibleReplyCorrelation: null,
latestAssistantPreview: null,
latestAssistantPreview: 'Answer after observe.',
reason: null,
},
diagnostics: [],
@ -4973,6 +5100,19 @@ describe('TeamProvisioningService', () => {
delivered: true,
responsePending: false,
responseState: 'responded_plain_text',
visibleReplyCorrelation: 'plain_assistant_text',
});
const userInbox = JSON.parse(
await fsPromises.readFile(path.join(tempTeamsBase, 'team-a', 'inboxes', 'user.json'), 'utf8')
) as Array<Record<string, unknown>>;
expect(userInbox).toHaveLength(1);
expect(userInbox[0]).toMatchObject({
from: 'bob',
to: 'user',
text: 'Answer after observe.',
relayOfMessageId: 'msg-ledger-1',
source: 'runtime_delivery',
});
expect(sendMessageToMember).toHaveBeenCalledTimes(1);
@ -14319,6 +14459,205 @@ describe('TeamProvisioningService', () => {
});
});
it('self-heals stale persisted OpenCode secondary bootstrap without live metadata', async () => {
const teamName = 'zz-opencode-persisted-bootstrap-stall-no-live';
const leadSessionId = 'lead-session';
const acceptedAt = new Date(Date.now() - 6 * 60_000).toISOString();
writeTeamMeta(teamName, {
providerId: 'codex',
providerBackendId: 'codex-native',
model: 'gpt-5.4',
});
writeMembersMeta(teamName, [
{
name: 'tom',
providerId: 'opencode',
model: 'openrouter/minimax/minimax-m2.5',
},
]);
writeLaunchConfig(teamName, '/Users/test/proj', leadSessionId, ['tom']);
writeLaunchState(teamName, leadSessionId, {
tom: {
providerId: 'opencode',
model: 'openrouter/minimax/minimax-m2.5',
laneId: 'secondary:opencode:tom',
laneKind: 'secondary',
laneOwnerProviderId: 'opencode',
launchState: 'runtime_pending_bootstrap',
agentToolAccepted: true,
runtimeAlive: false,
bootstrapConfirmed: false,
hardFailure: false,
runtimeSessionId: 'ses_tom_partial',
runtimePid: 55947,
livenessKind: 'registered_only',
firstSpawnAcceptedAt: acceptedAt,
diagnostics: [
'OpenCode bootstrap MCP tool failed before required attach completed: runtime_bootstrap_checkin',
'member_briefing at 2026-05-04T18:25:43.091Z',
],
lastEvaluatedAt: acceptedAt,
},
});
const svc = new TeamProvisioningService();
vi.spyOn(svc as any, 'getLiveTeamAgentRuntimeMetadata').mockResolvedValue(new Map());
const result = await svc.getMemberSpawnStatuses(teamName);
expect(result.statuses.tom).toMatchObject({
launchState: 'runtime_pending_bootstrap',
bootstrapConfirmed: false,
hardFailure: false,
bootstrapStalled: true,
runtimeDiagnostic:
'OpenCode bootstrap MCP tool failed before required attach completed: runtime_bootstrap_checkin',
runtimeDiagnosticSeverity: 'warning',
});
const persisted = JSON.parse(
await fsPromises.readFile(getTeamLaunchStatePath(teamName), 'utf8')
);
expect(persisted.members.tom).toMatchObject({
launchState: 'runtime_pending_bootstrap',
bootstrapConfirmed: false,
hardFailure: false,
bootstrapStalled: true,
});
});
it('sends one targeted OpenCode bootstrap check-in retry when a partial bootstrap stalls', async () => {
const teamName = 'zz-opencode-bootstrap-checkin-retry';
const acceptedAt = new Date(Date.now() - 6 * 60_000).toISOString();
const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({
ok: true,
providerId: 'opencode',
memberName: String(input.memberName),
diagnostics: [],
}));
const svc = new TeamProvisioningService();
svc.setRuntimeAdapterRegistry(
new TeamRuntimeAdapterRegistry([
{
providerId: 'opencode',
prepare: vi.fn(),
launch: vi.fn(),
reconcile: vi.fn(),
stop: vi.fn(),
sendMessageToMember,
} as any,
])
);
vi.spyOn(svc as any, 'refreshMemberSpawnStatusesFromLeadInbox').mockResolvedValue(undefined);
vi.spyOn(svc as any, 'maybeAuditMemberSpawnStatuses').mockResolvedValue(undefined);
vi.spyOn(svc as any, 'getLiveTeamAgentRuntimeMetadata').mockResolvedValue(
new Map([
[
'tom',
{
alive: true,
providerId: 'opencode',
livenessKind: 'runtime_process',
runtimeSessionId: 'ses_tom_partial',
runtimeDiagnostic: 'OpenCode runtime process detected',
runtimeDiagnosticSeverity: 'info',
},
],
])
);
const run = createMemberSpawnRun({
teamName,
runId: 'run-bootstrap-checkin-retry',
expectedMembers: ['tom'],
memberSpawnStatuses: new Map([
[
'tom',
createMemberSpawnStatusEntry({
status: 'waiting',
launchState: 'runtime_pending_bootstrap',
agentToolAccepted: true,
runtimeAlive: true,
bootstrapConfirmed: false,
hardFailure: false,
firstSpawnAcceptedAt: acceptedAt,
livenessKind: 'runtime_process',
}),
],
]),
});
run.onProgress = vi.fn();
run.isLaunch = false;
run.request = {
teamName,
cwd: '/Users/test/proj',
providerId: 'codex',
providerBackendId: 'codex-native',
model: 'gpt-5.4',
members: [],
};
run.mixedSecondaryLanes = [
{
laneId: 'secondary:opencode:tom',
providerId: 'opencode',
member: {
name: 'tom',
providerId: 'opencode',
model: 'openrouter/minimax/minimax-m2.5',
cwd: '/Users/test/proj',
},
runId: 'opencode-run-tom',
state: 'finished',
result: {
runId: 'opencode-run-tom',
teamName,
launchPhase: 'active',
teamLaunchState: 'partial_pending',
members: {
tom: {
memberName: 'tom',
providerId: 'opencode',
launchState: 'runtime_pending_bootstrap',
agentToolAccepted: true,
runtimeAlive: false,
bootstrapConfirmed: false,
hardFailure: false,
sessionId: 'ses_tom_partial',
diagnostics: [
'runtime_bootstrap_checkin failed: Not connected',
'member_briefing at 2026-05-04T18:25:43.091Z',
],
},
},
warnings: [],
diagnostics: [],
},
warnings: [],
diagnostics: [],
},
];
(svc as any).runs.set(run.runId, run);
(svc as any).aliveRunByTeam.set(teamName, run.runId);
await (svc as any).reevaluateMemberLaunchStatus(run, 'tom');
await (svc as any).reevaluateMemberLaunchStatus(run, 'tom');
expect(sendMessageToMember).toHaveBeenCalledTimes(1);
expect(sendMessageToMember).toHaveBeenCalledWith(
expect.objectContaining({
runId: 'opencode-run-tom',
teamName,
laneId: 'secondary:opencode:tom',
memberName: 'tom',
cwd: '/Users/test/proj',
bootstrapCheckinRetry: {
runtimeSessionId: 'ses_tom_partial',
reason: 'runtime_bootstrap_checkin failed: Not connected',
},
})
);
});
it('keeps process table diagnostics visible when live metadata has no primary diagnostic', async () => {
const svc = new TeamProvisioningService();
(svc as any).getLiveTeamAgentRuntimeMetadata = vi.fn(
@ -16288,6 +16627,8 @@ describe('TeamProvisioningService', () => {
const teamName = 'atlas-hq-source-aware-persisted';
const exactOpenCodeReason =
'Latest assistant message msg_alice failed with APIError - Insufficient credits.';
const transientBobMcpFailure =
'resources/read failed: resources/read failed for `agent-teams` (member_briefing?teamName=atlas-hq-source-aware-persisted&memberName=bob): Mcp error: -32601: Method not found';
writeTeamMeta(teamName, {
providerId: 'codex',
providerBackendId: 'codex-native',
@ -16337,7 +16678,7 @@ describe('TeamProvisioningService', () => {
runtimeAlive: false,
bootstrapConfirmed: false,
hardFailure: true,
hardFailureReason: 'Teammate was never spawned during launch.',
hardFailureReason: transientBobMcpFailure,
lastEvaluatedAt: '2026-04-23T10:02:00.000Z',
},
jack: {

View file

@ -173,6 +173,29 @@ describe('OpenCodeDeliveryWarning', () => {
});
});
it('shows terminal empty assistant turn reason in the compact failed warning', async () => {
const failedWarning =
'OpenCode runtime delivery failed. Message was saved to inbox, but live delivery did not complete. Reason: OpenCode returned an empty assistant turn.';
const { host, root } = renderWarning({
warning: failedWarning,
debugDetails: {
...debugDetails,
delivered: false,
responsePending: false,
responseState: 'empty_assistant_turn',
ledgerStatus: 'failed_terminal',
reason: 'empty_assistant_turn',
diagnostics: ['empty_assistant_turn'],
},
});
expect(host.textContent).toContain(failedWarning);
await act(async () => {
root.unmount();
});
});
it('hides details again when a different runtime delivery payload arrives', async () => {
const { host, root } = renderWarning();

View file

@ -0,0 +1,30 @@
import { describe, expect, it } from 'vitest';
import { buildOpenCodeRuntimeDeliveryDiagnostics } from '../../../src/renderer/utils/openCodeRuntimeDeliveryDiagnostics';
describe('openCodeRuntimeDeliveryDiagnostics', () => {
it('surfaces terminal empty assistant turn in the compact failed warning', () => {
const diagnostics = buildOpenCodeRuntimeDeliveryDiagnostics({
deliveredToInbox: true,
messageId: 'msg-empty',
runtimeDelivery: {
providerId: 'opencode',
attempted: true,
delivered: false,
responsePending: false,
responseState: 'empty_assistant_turn',
ledgerStatus: 'failed_terminal',
reason: 'empty_assistant_turn',
diagnostics: ['empty_assistant_turn'],
},
});
expect(diagnostics.warning).toBe(
'OpenCode runtime delivery failed. Message was saved to inbox, but live delivery did not complete. Reason: OpenCode returned an empty assistant turn.'
);
expect(diagnostics.debugDetails).toMatchObject({
responseState: 'empty_assistant_turn',
reason: 'empty_assistant_turn',
});
});
});