37 KiB
OpenCode Runtime Delivery User-Visible Status - Phase 1.3 Plan
Summary
Extend the Phase 1.2 advisory policy into the direct send/composer runtime delivery status path.
Phase 1.2 fixes member cards, snapshots, notifications, and lead notices. Phase 1.3 makes the direct user send UX use the same user-impact classification, so a temporary generic proof gap does not show as OpenCode runtime delivery failed while the app is still inside the late-proof window.
Recommended scope: 🎯 8 🛡️ 8 🧠 8 - roughly 650-900 changed lines including tests.
Why This Is Separate
The direct send path has an existing public-ish shared type:
SendMessageResult.runtimeDelivery
OpenCodeRuntimeDeliveryStatus
Renderer code currently maps:
runtimeDelivery.delivered === false -> failed warning
runtimeDelivery.responsePending === true -> pending warning
Changing delivered semantics directly would be risky because it is already used by store actions and tests. Phase 1.3 should add a user-visible impact field while preserving the old ledger fact fields for compatibility.
Current Direct Send Paths
There are two runtime status entry points:
- Immediate send result path:
src/main/ipc/teams.ts
handleSendMessage()
-> provisioning.relayOpenCodeMemberInboxMessages()
-> result.runtimeDelivery = relay.lastDelivery
- Later polling path:
src/main/services/team/TeamProvisioningService.ts
getOpenCodeRuntimeDeliveryStatus()
-> toOpenCodeRuntimeDeliveryStatus(record)
Renderer consumers:
src/renderer/store/slices/teamSlice.ts
src/renderer/utils/openCodeRuntimeDeliveryDiagnostics.ts
src/renderer/components/team/messages/OpenCodeDeliveryWarning.tsx
src/renderer/components/team/messages/MessageComposer.tsx
src/renderer/components/team/dialogs/SendMessageDialog.tsx
Important: both backend paths must use the same user-impact contract. If only polling is fixed, the initial composer warning can still flash the old failure text.
Shared Type Extension
Extend SendMessageResult.runtimeDelivery in src/shared/types/team.ts.
Recommended additive fields:
export type OpenCodeRuntimeDeliveryUserVisibleState =
| 'none'
| 'checking'
| 'warning'
| 'error';
export interface OpenCodeRuntimeDeliveryUserVisibleImpact {
state: OpenCodeRuntimeDeliveryUserVisibleState;
reasonCode?: MemberRuntimeAdvisory['reasonCode'];
message?: string;
observedAt?: string;
nextReviewAt?: string;
}
Inside SendMessageResult.runtimeDelivery:
userVisibleImpact?: OpenCodeRuntimeDeliveryUserVisibleImpact;
Why a nested object:
- avoids overloading
delivered; - keeps old fields readable for debugging;
- allows renderer to prefer the new impact but fall back to old behavior;
- makes Phase 1.3 backwards compatible with older IPC payloads during development.
State Semantics
none
No user warning should be shown.
Examples:
- successful visible reply proof;
- newer success suppressed older failure;
- late visible reply or task progress proof exists.
checking
The ledger may already say terminal, but the user-facing policy is still in the grace window for generic proof.
Examples:
- recent
failed_terminal / empty_assistant_turn; - recent
failed_terminal / prompt_delivered_no_assistant_message; - recent
failed_terminal / visible_reply_still_required; - UI timeout pending;
- queued behind older OpenCode delivery.
Renderer copy should be non-scary:
OpenCode delivery is still being checked. Message was saved and will be observed before retry if needed.
warning
Grace expired and proof is still missing, but this is not a hard provider/runtime error.
Examples:
- old
failed_terminal / empty_assistant_turnwith no late proof; - old
failed_terminal / non_visible_tool_without_task_progresswith no same-task progress.
Renderer copy:
OpenCode reply could not be verified. Message was saved to inbox, but the app did not find a correlated reply or progress proof.
If there is a specific message:
OpenCode reply could not be verified. Message was saved to inbox, but the app did not find a correlated reply or progress proof. Detail: OpenCode returned an empty assistant turn.
error
Hard delivery error. This is the old scary warning path and remains valid.
Examples:
- insufficient credits;
- invalid API key;
- runtime bridge unavailable;
- payload mismatch;
- attachment payload unavailable;
- recipient unavailable/removed for the direct send recovery path.
Recipient unavailable/removed should not create a member-card runtime advisory or notification by itself. For direct send UX it can still be an error impact so the draft is preserved and the user can choose another recipient.
Renderer copy:
OpenCode runtime delivery failed. Message was saved to inbox, but live delivery did not complete.
Backend Mapping
Add a converter from Phase 1.2 decision to shared userVisibleImpact.
There are two backend mapping modes:
- Full status mapping for explicit status reads and polling. This may read proof sources with a small budget.
- Immediate result mapping for
sendMessageIPC. This must stay cheap and should not scan tasks/inboxes before returning the send result.
Example:
function toOpenCodeRuntimeDeliveryUserVisibleImpact(
decision: OpenCodeRuntimeDeliveryAdvisoryDecision
): OpenCodeRuntimeDeliveryUserVisibleImpact {
if (decision.action === 'suppress') {
return { state: 'none' };
}
if (decision.action === 'defer') {
return {
state: 'checking',
observedAt: decision.observedAt,
nextReviewAt: decision.nextReviewAt,
};
}
return {
state: decision.severity === 'error' ? 'error' : 'warning',
reasonCode: decision.reasonCode,
message: decision.message,
observedAt: decision.observedAt,
};
}
Special immediate statuses that may not have a ledger record:
function getImmediateOpenCodeRuntimeDeliveryImpact(input: {
delivered?: boolean;
responsePending?: boolean;
reason?: string;
diagnostics?: string[];
queuedBehindMessageId?: string;
}): OpenCodeRuntimeDeliveryUserVisibleImpact {
const observedAt = new Date().toISOString();
if (input.responsePending === true) {
return { state: 'checking', observedAt };
}
if (input.queuedBehindMessageId) {
return { state: 'checking', observedAt };
}
if (input.reason === 'opencode_runtime_delivery_ui_timeout_pending') {
return { state: 'checking', observedAt };
}
if (input.reason === 'opencode_delivery_response_pending') {
return { state: 'checking', observedAt };
}
if (
input.reason === 'opencode_runtime_not_active' &&
(input.diagnostics ?? []).some((line) =>
line.toLowerCase().includes('will be retried after runtime check-in')
)
) {
return { state: 'checking', observedAt };
}
if (input.delivered === false) {
return { state: 'error', message: input.reason, observedAt };
}
return { state: 'none' };
}
Do not use the immediate fallback when a full ledger status read is available. Ledger plus policy is authoritative there.
For immediate sendMessage responses, use cheap ledger-fact classification first and leave full proof reconciliation to the later status poll or member-advisory refresh.
Service Changes
TeamProvisioningService.getOpenCodeRuntimeDeliveryStatus
Current:
if (record) {
return this.toOpenCodeRuntimeDeliveryStatus(record);
}
Recommended:
if (record) {
return await this.toOpenCodeRuntimeDeliveryStatus(record);
}
Make toOpenCodeRuntimeDeliveryStatus async, or pass in a precomputed impact.
Example:
private async toOpenCodeRuntimeDeliveryStatus(
record: OpenCodePromptDeliveryLedgerRecord
): Promise<OpenCodeRuntimeDeliveryStatus> {
const base = this.toOpenCodeRuntimeDeliveryStatusFacts(record);
const decision = await this.decideOpenCodeRuntimeDeliveryAdvisoryForRecord(record, {
proofReadBudgetMs: 750,
});
return {
...base,
userVisibleImpact: toOpenCodeRuntimeDeliveryUserVisibleImpact(decision),
};
}
If proof reading exceeds the budget, fall back conservatively:
- hard/action-required reason ->
error; - recent generic terminal proof gap ->
checking; - old generic terminal proof gap ->
warning; - responded or newer success visible in the ledger ->
none.
Keep fact semantics unchanged:
const failed = record.status === 'failed_terminal';
return {
delivered: !failed,
responsePending: !failed && !responded,
ledgerStatus: record.status,
responseState: record.responseState,
reason: record.lastReason ?? undefined,
diagnostics: record.diagnostics,
};
The renderer should no longer interpret these fact fields directly when userVisibleImpact exists.
Immediate sendMessage IPC result
In src/main/ipc/teams.ts, after relay:
result.runtimeDelivery = {
providerId: 'opencode',
attempted: true,
delivered: delivery.delivered,
responsePending: delivery.responsePending,
acceptanceUnknown: delivery.acceptanceUnknown,
responseState: delivery.responseState,
ledgerStatus: delivery.ledgerStatus,
visibleReplyMessageId: delivery.visibleReplyMessageId,
visibleReplyCorrelation: delivery.visibleReplyCorrelation,
reason: delivery.reason,
diagnostics: delivery.diagnostics,
};
This path should ask provisioning to decorate the delivery with impact, but it should not run the full proof reader. The user is waiting for the send call to return, and the relay path may already have done substantial I/O.
Important timestamp caveat: OpenCodeMemberInboxDelivery does not currently carry failedAt or updatedAt. If immediate classification needs exact grace-age, either:
- read the single ledger record by
ledgerRecordIdandlaneIdwithout scanning inboxes/tasks; or - conservatively classify generic terminal proof failures as
checkingand letgetOpenCodeRuntimeDeliveryStatus()correct the state on the next poll.
Prefer the conservative fallback unless a single-record ledger read is already cheap in the call site.
Recommended helper:
async getOpenCodeRuntimeDeliveryImpactForResult(input: {
teamName: string;
delivery: OpenCodeMemberInboxDelivery;
}): Promise<OpenCodeRuntimeDeliveryUserVisibleImpact>
Implementation:
async getOpenCodeRuntimeDeliveryImpactForResult(input: {
teamName: string;
delivery: OpenCodeMemberInboxDelivery;
}): Promise<OpenCodeRuntimeDeliveryUserVisibleImpact> {
if (input.delivery.responsePending === true) {
return { state: 'checking' };
}
const factImpact = getImmediateOpenCodeRuntimeDeliveryImpact({
delivered: input.delivery.delivered,
responsePending: input.delivery.responsePending,
reason: input.delivery.reason,
});
if (factImpact.state !== 'error') {
return factImpact;
}
// If the immediate delivery result carries generic terminal proof failure facts,
// report checking during the grace window rather than hard failure.
const deliveryRecordFacts = {
ledgerStatus: input.delivery.ledgerStatus,
responseState: input.delivery.responseState,
reason: input.delivery.reason,
diagnostics: input.delivery.diagnostics ?? [],
};
// Without record timestamps, generic terminal proof gaps should become checking,
// not warning. The next explicit status poll can use the full record time.
return classifyOpenCodeRuntimeDeliveryFactsForImmediateUx(deliveryRecordFacts);
}
If a caller needs exact suppression because a late proof already exists, it should call getOpenCodeRuntimeDeliveryStatus() after the send result. The immediate result can temporarily say checking; it should not temporarily say hard failed for generic proof gaps.
Then IPC:
const userVisibleImpact = await provisioning.getOpenCodeRuntimeDeliveryImpactForResult({
teamName: tn,
delivery,
});
result.runtimeDelivery = {
...oldFacts,
userVisibleImpact,
};
Renderer Diagnostics
Update:
src/renderer/utils/openCodeRuntimeDeliveryDiagnostics.ts
Preferred logic:
export function buildOpenCodeRuntimeDeliveryDiagnostics(
result: SendMessageResult
): OpenCodeRuntimeDeliveryDiagnostics {
const runtimeDelivery = result.runtimeDelivery;
if (runtimeDelivery?.attempted !== true) {
return { warning: null, debugDetails: null };
}
const impact = runtimeDelivery.userVisibleImpact;
if (impact) {
return buildDiagnosticsFromUserVisibleImpact(result, impact);
}
return buildLegacyDiagnostics(result);
}
Impact mapping:
function buildDiagnosticsFromUserVisibleImpact(
result: SendMessageResult,
impact: OpenCodeRuntimeDeliveryUserVisibleImpact
): OpenCodeRuntimeDeliveryDiagnostics {
if (impact.state === 'none') {
return { warning: null, debugDetails: null };
}
if (impact.state === 'checking') {
return {
warning: CHECKING_WARNING,
debugDetails: buildDebugDetails(result),
};
}
if (impact.state === 'warning') {
const detail = formatOpenCodeRuntimeDeliveryFailureReason(impact.message);
return {
warning: detail ? `${PROOF_WARNING} Detail: ${detail}` : PROOF_WARNING,
debugDetails: buildDebugDetails(result),
};
}
const detail = formatOpenCodeRuntimeDeliveryFailureReason(
impact.message ?? result.runtimeDelivery?.reason
);
return {
warning: detail ? `${FAILED_WARNING} Reason: ${detail}` : FAILED_WARNING,
debugDetails: buildDebugDetails(result),
};
}
Recommended copy constants:
const CHECKING_WARNING =
'OpenCode delivery is still being checked. Message was saved and will be observed before retry if needed.';
const PROOF_WARNING =
'OpenCode reply could not be verified. Message was saved to inbox, but the app did not find a correlated reply or progress proof.';
Keep the old fallback for safety:
function buildLegacyDiagnostics(result: SendMessageResult): OpenCodeRuntimeDeliveryDiagnostics {
// Current logic.
}
Renderer Success/Failure Semantics
Update renderer code that currently treats delivered === false as a terminal UX failure.
Current examples:
// teamSlice.ts
const runtimeDeliveryFailed =
result.runtimeDelivery?.attempted === true && result.runtimeDelivery.delivered === false;
// SendMessageDialog.tsx
if (
result?.runtimeDelivery?.attempted === true &&
result.runtimeDelivery.delivered === false
) {
return;
}
After Phase 1.3, delivered === false is a ledger fact, not a UX decision. Use userVisibleImpact first:
function isOpenCodeRuntimeDeliveryHardUxFailure(
runtimeDelivery: SendMessageResult['runtimeDelivery'] | undefined
): boolean {
if (runtimeDelivery?.attempted !== true) {
return false;
}
if (runtimeDelivery.userVisibleImpact) {
return runtimeDelivery.userVisibleImpact.state === 'error';
}
return runtimeDelivery.delivered === false;
}
Use this helper for:
lastSendMessageResultdecision inteamSlice.ts;- draft-clearing decision in
SendMessageDialog.tsx; - pending-send restore/finalize decision in
MessageComposer.tsx; - any test that currently uses
delivered === falseas "do not clear draft".
Expected UX:
checkingclears the draft like a saved send, because the message is persisted and still being observed;warningalso should not invite blind duplicate resend;errorpreserves the draft for user recovery, preserving the existing hard-failure behavior;- legacy payloads without
userVisibleImpactkeep the olddelivered === falsebehavior.
MessageComposer.tsx currently computes pending-send failure from:
const failed = sendError !== null || sendDebugDetails?.delivered === false;
That must become user-visible-impact aware. Otherwise a terminal generic proof gap with userVisibleState: "checking" and delivered: false will restore the draft even though the message was saved and is still being observed.
Recommended helper:
function isOpenCodeRuntimeDeliveryHardUxFailureFromDebugDetails(
debugDetails: OpenCodeRuntimeDeliveryDebugDetails | null | undefined
): boolean {
if (!debugDetails) return false;
if (debugDetails.userVisibleState) {
return debugDetails.userVisibleState === 'error';
}
return debugDetails.delivered === false;
}
Use it only for the optimistic draft restore path. If a delayed status poll later changes from checking to error, do not resurrect an old draft automatically; by then the saved user message is already in the conversation and restoring a stale composer draft would look like a duplicate-send prompt.
Pending-reply clearing needs a different helper.
Current paths:
src/renderer/components/team/messages/MessagesPanel.tsxsrc/renderer/components/team/TeamDetailView.tsx
They currently clear pendingRepliesByMember when runtimeDelivery.delivered === false. After Phase 1.3:
function shouldClearPendingReplyForOpenCodeRuntimeDelivery(
runtimeDelivery: SendMessageResult['runtimeDelivery'] | undefined
): boolean {
if (runtimeDelivery?.attempted !== true) {
return false;
}
if (runtimeDelivery.userVisibleImpact) {
return (
runtimeDelivery.userVisibleImpact.state === 'warning' ||
runtimeDelivery.userVisibleImpact.state === 'error'
);
}
return runtimeDelivery.delivered === false;
}
Expected pending behavior:
checkingkeeps pending reply, because a real reply can still arrive;noneclears only through the existing visible-reply reconciliation;warningclears pending reply because the live reply could not be verified after grace;errorclears pending reply because live delivery failed;- legacy payloads keep current
delivered === falseclearing behavior.
Debug Details
Extend renderer debug details to include impact:
export interface OpenCodeRuntimeDeliveryDebugDetails {
messageId: string;
statusMessageId: string | null;
ledgerRecordId: string | null;
laneId: string | null;
queuedBehindMessageId: string | null;
providerId: string;
delivered: boolean | null;
responsePending: boolean | null;
responseState: string | null;
ledgerStatus: string | null;
acceptanceUnknown: boolean | null;
reason: string | null;
diagnostics: string[];
userVisibleState: string | null;
userVisibleReasonCode: string | null;
userVisibleMessage: string | null;
userVisibleObservedAt: string | null;
userVisibleNextReviewAt: string | null;
}
This keeps support/debugging transparent for visible warning/checking/error states. For userVisibleImpact.state === 'none', return debugDetails: null so the store clears stale hidden runtime diagnostics.
Update formatOpenCodeRuntimeDeliveryDebugDetails() as well as the debug details builder. The expandable JSON/details view should include the user-visible impact fields when a warning/checking/error is visible, otherwise support logs will show ledgerStatus: failed_terminal without the reason the UI chose not to render it as a hard failure.
Also update the expanded details grid in OpenCodeDeliveryWarning.tsx to render:
statusMessageId;ledgerRecordId;laneId;queuedBehindMessageId;userVisibleState;userVisibleReasonCode;userVisibleMessage;userVisibleObservedAt;userVisibleNextReviewAt.
Do not attach debug details for state: "none":
if (runtimeDelivery.userVisibleImpact?.state === 'none') {
return { warning: null, debugDetails: null };
}
This matters for stale UI cleanup. A hidden none state with non-null debug details can keep OpenCodeDeliveryWarning mounted, keep polling dependencies alive, or leave a collapsed "delivery details" affordance with no user-facing warning.
messageId should remain the original user-sent inbox row id because clearSendMessageRuntimeDiagnostics(messageId) and visible-reply reconciliation use it. statusMessageId is the id to poll:
const statusMessageId =
runtimeDelivery.queuedBehindMessageId?.trim() ||
result.messageId;
Do not replace debugDetails.messageId with queuedBehindMessageId; that breaks clearing the warning for the original send row. Use statusMessageId only for getOpenCodeRuntimeDeliveryStatus().
Warning Delay Logic
Update:
src/renderer/components/team/messages/OpenCodeDeliveryWarning.tsx
Current delay logic only delays when:
debugDetails?.responsePending === true && debugDetails.delivered !== false
That will not work for Phase 1.3 because a generic terminal ledger fact can be:
delivered: false
ledgerStatus: 'failed_terminal'
userVisibleState: 'checking'
Change the delay condition to prefer user-visible state:
const delayPendingWarning =
debugDetails?.userVisibleState === 'checking' ||
(debugDetails?.userVisibleState == null &&
debugDetails?.responsePending === true &&
debugDetails.delivered !== false);
This keeps the existing legacy behavior and prevents a terminal-generic proof gap from flashing even as a non-scary checking warning.
Status Polling Logic
Update:
src/renderer/components/team/messages/MessagesPanel.tsx
Current polling starts only when:
debugDetails?.responsePending === true
That is insufficient after Phase 1.3. A terminal generic proof gap should have:
responsePending: false
ledgerStatus: 'failed_terminal'
userVisibleState: 'checking'
Change the polling gate:
const messageId = debugDetails?.messageId;
const statusMessageId = debugDetails?.statusMessageId || messageId;
const shouldPollRuntimeDeliveryStatus =
debugDetails?.responsePending === true ||
debugDetails?.userVisibleState === 'checking';
if (!messageId || !statusMessageId || sendMessageRuntimeReplyVisible || !shouldPollRuntimeDeliveryStatus) {
return;
}
Update the effect dependencies to include:
sendMessageDebugDetails?.statusMessageId;sendMessageDebugDetails?.userVisibleState;sendMessageDebugDetails?.userVisibleNextReviewAt.
Without this, the composer can show a checking warning forever after the immediate send result, because the follow-up getOpenCodeRuntimeDeliveryStatus() call never runs.
When calling refreshSendMessageRuntimeDeliveryStatus, pass both ids or add a small wrapper:
void refreshSendMessageRuntimeDeliveryStatus(teamName, {
messageId,
statusMessageId,
});
The store should still update/clear only if state.sendMessageDebugDetails?.messageId === messageId. The IPC status lookup uses statusMessageId.
If statusMessageId !== messageId, treat the returned status as blocker status, not as the final status of the original send:
checkingkeeps the original send in checking;nonemeans the blocker cleared, so schedule or attempt a follow-up status read for the originalmessageId;warningorerroron the blocker should not be copied as the new message's warning/error;- if the original
messageIdstill has no ledger record, keep checking until the stale-check window expires.
This prevents a hard failure from an older active delivery row being shown as the failure for a newly queued message.
Stop polling when impact becomes terminal from a UX perspective:
noneclears warning/debug details;warningstops polling because the proof grace window has expired;errorstops polling because delivery is a hard failure;- legacy
responsePending: falsekeeps current behavior.
Do not keep polling a warning forever waiting for a late reply. Late runtime replies already reach the renderer through message-feed/member-advisory refresh paths; status polling should only cover the short "checking" window.
Polling And Refresh Behavior
Current store behavior:
- send action stores warning/debug details from immediate
runtimeDelivery; refreshSendMessageRuntimeDeliveryStatus()pollsgetOpenCodeRuntimeDeliveryStatus;OpenCodeDeliveryWarningdelays pending warning display.
Phase 1.3 expected behavior:
-
Direct send returns terminal generic proof inside grace:
- ledger facts:
delivered: false,ledgerStatus: "failed_terminal" - impact:
{ state: "checking", nextReviewAt } - renderer warning: checking copy, not failed copy.
- ledger facts:
-
Poll before grace expires:
- still checking.
-
Late reply arrives:
- impact becomes
none; - warning clears.
- impact becomes
-
Grace expires without proof:
- impact becomes
warning; - copy changes to proof warning.
- impact becomes
The polling caller must not rely only on responsePending.
If nextReviewAt is present, add one extra status refresh just after that time in addition to the existing short poll cadence. The current fixed delays are [15s, 45s, 90s]; if the backend grace is around 120s, those fixed timers can all fire before the proof window closes and leave checking visible forever.
Clamp the extra delay to a sane range. Do not schedule an arbitrary long timer from IPC data:
const nextReviewDelayMs = Number.isFinite(Date.parse(nextReviewAt ?? ''))
? Math.max(1_000, Math.min(Date.parse(nextReviewAt!) - Date.now() + 500, 180_000))
: null;
Schedule it only when userVisibleState === 'checking'. De-dupe it against existing fixed delays if it is within roughly 500ms of one of them.
This is an optimization only. Correctness still comes from explicit status calls and from team/message refresh events, but this timer prevents the common "checking never transitions to proof warning" case.
Handle status failures inside refreshSendMessageRuntimeDeliveryStatus():
try {
const status = await unwrapIpc(...);
if (!status) {
maybeClearStaleCheckingDiagnostics(normalizedMessageId);
return;
}
// existing diagnostic update
} catch (error) {
logger.debug('OpenCode runtime delivery status refresh failed', error);
maybeClearStaleCheckingDiagnostics(normalizedMessageId);
}
maybeClearStaleCheckingDiagnostics() should only clear non-terminal checking after the backend review window is already stale, for example now > userVisibleNextReviewAt + 60s. If userVisibleNextReviewAt is absent, fall back to a conservative max checking age from userVisibleObservedAt, for example three minutes. It must not convert a transient status miss into a hard delivery error.
Edge Cases
Backward-compatible IPC
If userVisibleImpact is absent:
- use current legacy behavior;
- do not crash browser mode or tests with older fixtures.
Immediate hard error without ledger
Example:
{
"attempted": true,
"delivered": false,
"reason": "opencode_runtime_message_bridge_unavailable"
}
Expected:
- impact fallback
error; - old failed warning still appears.
Runtime not active but bootstrap still checking in
Example:
{
"attempted": true,
"delivered": false,
"reason": "opencode_runtime_not_active",
"diagnostics": [
"OpenCode runtime bootstrap is not confirmed for jack. Message was saved and will be retried after runtime check-in."
]
}
Expected:
- impact
checking; - draft clears like a saved send;
- pending reply remains;
- no hard failed copy.
If opencode_runtime_not_active has no retry/check-in diagnostic and the lane is stopped/deleted, keep error for direct-send recovery.
UI timeout pending
Example:
{
"attempted": true,
"delivered": true,
"responsePending": true,
"reason": "opencode_runtime_delivery_ui_timeout_pending"
}
Expected:
- impact
checking; - pending/checking warning;
- no failed copy.
Terminal generic proof inside grace
Example:
{
"attempted": true,
"delivered": false,
"responsePending": false,
"responseState": "empty_assistant_turn",
"ledgerStatus": "failed_terminal",
"reason": "empty_assistant_turn",
"userVisibleImpact": {
"state": "checking",
"nextReviewAt": "2026-05-09T07:54:30.998Z"
}
}
Expected:
- renderer shows checking, not failed;
- debug details still show
ledgerStatus: "failed_terminal".
Terminal generic proof after grace
Expected:
- impact
warning; - renderer shows proof warning;
- not hard failed warning.
Hard diagnostic mixed with generic state
Expected:
- impact
error; - renderer shows failed warning with provider/auth/quota reason;
- no checking state.
Late proof between immediate send and polling
Expected:
- immediate result may show checking;
- next poll returns
none; - warning clears.
Terminal facts with none impact
Example:
{
"attempted": true,
"delivered": false,
"ledgerStatus": "failed_terminal",
"userVisibleImpact": {
"state": "none"
}
}
Expected:
- renderer clears warning;
- renderer clears debug details;
- draft remains cleared because the send was saved;
- pending reply clears only through existing visible-reply reconciliation, not through a hard-failure path.
Message queued behind older active delivery
Expected:
- impact
checking; - copy should not say failed;
- debug reason can include older message id.
debugDetails.messageIdremains the newly sent user message id;debugDetails.statusMessageIdusesqueuedBehindMessageIdwhile the older active delivery is blocking;- status polling must not clear the original send warning only because the queued-behind active record is not the same message id.
Acceptance unknown
Expected:
- impact
checkingwhile observe-first watchdog can still recover; - if later hard failure, impact
error; - if later generic terminal and inside grace, still
checking.
Status request fails during checking
Expected:
- keep the current checking state for one retry window;
- do not convert a transient IPC/status error into
OpenCode delivery error; - surface hard failure only when backend status returns
erroror legacy hard facts withoutuserVisibleImpact. - if the backend status remains unavailable past
userVisibleNextReviewAt + 60s, clear the checking diagnostic instead of leaving a permanent warning; - if
userVisibleNextReviewAtis missing, use a conservative max age fromuserVisibleObservedAt.
Warning after grace with late reply later
Expected:
- proof warning can appear after grace;
- a later visible correlated reply still clears advisory through the normal message/member refresh path;
- no desktop notification or lead notice is retro-fired for the previous proof warning.
Tests
Shared type tests
No runtime test required for type-only additions, but compile must pass:
pnpm typecheck --pretty false
Backend status tests
Update:
test/main/services/team/TeamProvisioningService.test.ts
test/main/services/team/TeamProvisioningServiceRelay.test.ts
Add:
it('decorates getOpenCodeRuntimeDeliveryStatus with checking impact for recent generic terminal proof failure', async () => {});
it('decorates getOpenCodeRuntimeDeliveryStatus with warning impact after proof grace expires', async () => {});
it('decorates getOpenCodeRuntimeDeliveryStatus with error impact for quota diagnostics', async () => {});
it('decorates immediate sendMessage runtimeDelivery with checking impact for generic terminal proof failure', async () => {});
it('decorates immediate runtime_not_active bootstrap check-in retry as checking', async () => {});
it('decorates stopped runtime_not_active without retry diagnostic as error', async () => {});
it('returns none impact when late visible runtime reply supersedes terminal proof failure', async () => {});
it('returns none impact when late visible reply is recovered by observed message id or taskRefs', async () => {});
it('returns none impact when ledger already has plain_assistant_text visible reply proof', async () => {});
Renderer diagnostics tests
Update:
test/renderer/utils/openCodeRuntimeDeliveryDiagnostics.test.ts
Existing tests that expect failed copy for terminal generic states should split:
it('shows checking copy for terminal empty assistant turn while impact is checking', () => {});
it('shows proof warning for terminal empty assistant turn when impact is warning', () => {});
it('keeps legacy failed copy when impact is absent', () => {});
it('shows hard failed copy when impact is error', () => {});
it('returns null warning and null debug details when impact is none', () => {});
it('formats user-visible impact fields in debug details for checking and warning states', () => {});
it('preserves original messageId and stores statusMessageId for queued-behind delivery', () => {});
Store tests
Update:
test/renderer/store/teamSlice.test.ts
Cases:
it('updates pending OpenCode diagnostics to checking when terminal generic proof is still in grace', async () => {});
it('updates checking OpenCode diagnostics to proof warning after grace impact is warning', async () => {});
it('clears OpenCode diagnostics when status impact becomes none', async () => {});
it('does not retain hidden debug details when impact becomes none', async () => {});
it('keeps failed warning for hard OpenCode runtime error impact', async () => {});
it('keeps polling while userVisibleState is checking even when responsePending is false', async () => {});
it('polls statusMessageId while updating diagnostics for the original messageId', async () => {});
it('does not copy a queued-behind blocker hard error onto the newly sent message', async () => {});
it('rechecks the original messageId after queued-behind blocker status becomes none', async () => {});
it('schedules an extra status refresh at userVisibleNextReviewAt for checking impact', async () => {});
it('clears stale checking diagnostics after the review window is stale and status stays unavailable', async () => {});
it('does not treat checking impact as lastSendMessageResult failure when delivered is false', async () => {});
it('keeps pending reply for checking impact even when delivered is false', async () => {});
it('clears pending reply for warning and error impacts', async () => {});
it('stops status polling when checking becomes warning', async () => {});
it('does not convert a transient status request failure into a hard delivery error', async () => {});
Component tests
Update:
test/renderer/components/team/messages/OpenCodeDeliveryWarning.test.tsx
test/renderer/components/team/messages/MessagesPanel.test.tsx
test/renderer/components/team/messages/MessageComposer.pendingSend.test.tsx
test/renderer/components/team/dialogs/SendMessageDialog.test.tsx
Expected:
- checking warning respects the delay even when ledger facts say
delivered: false; - proof warning appears immediately once impact is
warning; - hard error appears immediately;
- debug details include both ledger fact and impact.
Add a focused component case:
it('delays checking impact even when ledger facts are terminal failed', async () => {});
Dialog draft cases:
it('does not restore the composer draft for checking impact even when delivered is false', async () => {});
it('clears the send dialog draft for checking impact even when delivered is false', async () => {});
it('preserves the send dialog draft for hard error impact', async () => {});
it('keeps legacy delivered-false draft preservation when userVisibleImpact is absent', async () => {});
Verification
Focused:
pnpm vitest run test/renderer/utils/openCodeRuntimeDeliveryDiagnostics.test.ts
pnpm vitest run test/renderer/store/teamSlice.test.ts --testNamePattern "OpenCode"
pnpm vitest run test/renderer/components/team/messages/OpenCodeDeliveryWarning.test.tsx
pnpm vitest run test/renderer/components/team/messages/MessageComposer.pendingSend.test.tsx
pnpm vitest run test/main/services/team/TeamProvisioningService.test.ts --testNamePattern "OpenCode runtime"
pnpm vitest run test/main/services/team/TeamProvisioningServiceRelay.test.ts --testNamePattern "OpenCode"
Broader:
pnpm vitest run test/renderer/components/team/messages/MessagesPanel.test.tsx
pnpm vitest run test/renderer/components/team/dialogs/SendMessageDialog.test.tsx
pnpm vitest run test/main/services/team/TeamProvisioningService.test.ts
pnpm vitest run test/main/services/team/TeamProvisioningServiceRelay.test.ts
pnpm typecheck --pretty false
git diff --check
Manual smoke:
- Send a direct message to an OpenCode teammate.
- Force or simulate a recent
failed_terminal / empty_assistant_turn. - Confirm composer/dialog shows checking copy.
- Add a correlated runtime reply.
- Confirm warning clears after refresh.
- Simulate old proof missing record.
- Confirm proof warning copy, not hard failed copy.
- Simulate quota/auth diagnostic.
- Confirm hard failed copy.
Rollout Notes
- Do not remove old
delivered,responsePending,ledgerStatus, orresponseStatefields. - Renderer must prefer
userVisibleImpactwhen present and fall back to legacy fields when absent. - Do not hide debug details.
- Do not make
failed_terminalmean success. - Do not mark failed inbox rows read.
- Do not delay hard provider/runtime failures.
Acceptance Criteria
- Direct send no longer flashes
OpenCode runtime delivery failedfor recent generic proof gaps. - The same status still exposes
failed_terminalin debug details. - Hard errors still show failed copy.
- Confirmed proof gaps after grace show a warning, not hard error.
- Late proof clears the warning.
- Existing legacy payloads without
userVisibleImpactstill render with old behavior.