Skip to content

Commit 6426847

Browse files
open-design-crew[bot]xiaoche-hubcursoragent
authored
fix(web): separate run success from result delivery (#5525)
* fix(web): distinguish run success from result delivery Persist delivery outcomes separately so successful design runs cannot report completion before a usable artifact is available. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): recover missing design delivery after reload * chore(ci): rerun pull request checks * fix(web): avoid reattaching local delivery finalization * fix(web): retry delivery failures * chore(ci): rerun pull request checks * chore(ci): retrigger managed checks --------- Co-authored-by: xiaoche-hub <298951296+xiaoche-hub@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 35e8ded commit 6426847

12 files changed

Lines changed: 749 additions & 42 deletions

apps/daemon/src/db.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ function migrate(db: SqliteDb): void {
115115
content TEXT NOT NULL,
116116
agent_id TEXT,
117117
agent_name TEXT,
118+
result_delivery_state TEXT,
118119
events_json TEXT,
119120
attachments_json TEXT,
120121
produced_files_json TEXT,
@@ -278,6 +279,9 @@ function migrate(db: SqliteDb): void {
278279
if (!messageCols.some((c: DbRow) => c.name === 'run_status')) {
279280
db.exec(`ALTER TABLE messages ADD COLUMN run_status TEXT`);
280281
}
282+
if (!messageCols.some((c: DbRow) => c.name === 'result_delivery_state')) {
283+
db.exec(`ALTER TABLE messages ADD COLUMN result_delivery_state TEXT`);
284+
}
281285
if (!messageCols.some((c: DbRow) => c.name === 'last_run_event_id')) {
282286
db.exec(`ALTER TABLE messages ADD COLUMN last_run_event_id TEXT`);
283287
}
@@ -1390,6 +1394,7 @@ export function listMessages(db: SqliteDb, conversationId: string) {
13901394
.prepare(
13911395
`SELECT id, role, content, agent_id AS agentId, agent_name AS agentName,
13921396
run_id AS runId, run_status AS runStatus,
1397+
result_delivery_state AS resultDeliveryState,
13931398
last_run_event_id AS lastRunEventId,
13941399
events_json AS eventsJson,
13951400
attachments_json AS attachmentsJson,
@@ -1420,7 +1425,7 @@ export function upsertMessage(db: SqliteDb, conversationId: string, m: DbRow) {
14201425
db.prepare(
14211426
`UPDATE messages
14221427
SET role = ?, content = ?, agent_id = ?, agent_name = ?,
1423-
run_id = ?, run_status = ?, last_run_event_id = ?,
1428+
run_id = ?, run_status = ?, result_delivery_state = ?, last_run_event_id = ?,
14241429
events_json = ?, attachments_json = ?, comment_attachments_json = ?,
14251430
produced_files_json = ?, trace_object_files_json = ?, feedback_json = ?,
14261431
pre_turn_file_names_json = ?,
@@ -1438,6 +1443,7 @@ export function upsertMessage(db: SqliteDb, conversationId: string, m: DbRow) {
14381443
m.agentName ?? null,
14391444
m.runId ?? null,
14401445
m.runStatus ?? null,
1446+
normalizeResultDeliveryStateForStorage(m.resultDeliveryState),
14411447
m.lastRunEventId ?? null,
14421448
m.events ? JSON.stringify(m.events) : null,
14431449
m.attachments ? JSON.stringify(m.attachments) : null,
@@ -1465,21 +1471,21 @@ export function upsertMessage(db: SqliteDb, conversationId: string, m: DbRow) {
14651471
const createdAt = typeof m.createdAt === 'number' && Number.isFinite(m.createdAt)
14661472
? m.createdAt
14671473
: now;
1468-
// 24 values: id, conversation_id, role, content, agent_id, agent_name,
1469-
// run_id, run_status, last_run_event_id, events_json, attachments_json,
1474+
// 25 values: id, conversation_id, role, content, agent_id, agent_name,
1475+
// run_id, run_status, result_delivery_state, last_run_event_id, events_json, attachments_json,
14701476
// comment_attachments_json, produced_files_json, trace_object_files_json,
14711477
// feedback_json, pre_turn_file_names_json, session_mode, run_context_json,
14721478
// applied_plugin_snapshot_json, telemetry_finalized_at, started_at,
14731479
// ended_at, position, created_at.
14741480
db.prepare(
14751481
`INSERT INTO messages
14761482
(id, conversation_id, role, content, agent_id, agent_name,
1477-
run_id, run_status, last_run_event_id, events_json,
1483+
run_id, run_status, result_delivery_state, last_run_event_id, events_json,
14781484
attachments_json, comment_attachments_json, produced_files_json,
14791485
trace_object_files_json, feedback_json, pre_turn_file_names_json,
14801486
session_mode, run_context_json, applied_plugin_snapshot_json,
14811487
telemetry_finalized_at, started_at, ended_at, position, created_at)
1482-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1488+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
14831489
).run(
14841490
m.id,
14851491
conversationId,
@@ -1489,6 +1495,7 @@ export function upsertMessage(db: SqliteDb, conversationId: string, m: DbRow) {
14891495
m.agentName ?? null,
14901496
m.runId ?? null,
14911497
m.runStatus ?? null,
1498+
normalizeResultDeliveryStateForStorage(m.resultDeliveryState),
14921499
m.lastRunEventId ?? null,
14931500
m.events ? JSON.stringify(m.events) : null,
14941501
m.attachments ? JSON.stringify(m.attachments) : null,
@@ -1516,6 +1523,7 @@ export function upsertMessage(db: SqliteDb, conversationId: string, m: DbRow) {
15161523
.prepare(
15171524
`SELECT id, role, content, agent_id AS agentId, agent_name AS agentName,
15181525
run_id AS runId, run_status AS runStatus,
1526+
result_delivery_state AS resultDeliveryState,
15191527
last_run_event_id AS lastRunEventId,
15201528
events_json AS eventsJson,
15211529
attachments_json AS attachmentsJson,
@@ -1895,6 +1903,7 @@ function normalizeMessage(row: DbRow) {
18951903
agentName: row.agentName ?? undefined,
18961904
runId: row.runId ?? undefined,
18971905
runStatus: row.runStatus ?? undefined,
1906+
resultDeliveryState: normalizeResultDeliveryState(row.resultDeliveryState),
18981907
lastRunEventId: row.lastRunEventId ?? undefined,
18991908
events: parseJsonOrUndef(row.eventsJson),
19001909
attachments: parseJsonOrUndef(row.attachmentsJson),
@@ -1916,6 +1925,20 @@ function normalizeMessageSessionMode(value: unknown): ChatSessionMode | undefine
19161925
return value === 'chat' || value === 'design' || value === 'plan' ? value : undefined;
19171926
}
19181927

1928+
function normalizeResultDeliveryState(
1929+
value: unknown,
1930+
): 'delivered' | 'no_result' | 'delivery_failed' | undefined {
1931+
return value === 'delivered' || value === 'no_result' || value === 'delivery_failed'
1932+
? value
1933+
: undefined;
1934+
}
1935+
1936+
function normalizeResultDeliveryStateForStorage(
1937+
value: unknown,
1938+
): 'delivered' | 'no_result' | 'delivery_failed' | null {
1939+
return normalizeResultDeliveryState(value) ?? null;
1940+
}
1941+
19191942
function normalizeMessageSessionModeForStorage(value: unknown): ChatSessionMode | null {
19201943
return value === 'chat' || value === 'design' || value === 'plan' ? value : null;
19211944
}

apps/daemon/tests/db-message-events.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ describe('message event persistence', () => {
4848
content: '',
4949
runId: 'agent-run-1',
5050
runStatus: 'running',
51+
resultDeliveryState: 'delivery_failed',
5152
events: [{ kind: 'status', label: 'starting', detail: 'Codex' }],
5253
startedAt: now,
5354
});
@@ -69,6 +70,7 @@ describe('message event persistence', () => {
6970
detail: 'Agent stalled without emitting any new output for 1s.',
7071
},
7172
]);
73+
expect(listMessages(db, 'conv-1')[0]?.resultDeliveryState).toBe('delivery_failed');
7274
});
7375

7476
it('persists explicit message createdAt values on insert', () => {

apps/web/src/components/AssistantMessage.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,9 @@ function AssistantMessageImpl({
664664
const hasEmptyResponse = events.some(
665665
(e) => e.kind === "status" && e.label === "empty_response"
666666
);
667+
const hasResultDeliveryFailure =
668+
message.resultDeliveryState === "no_result" ||
669+
message.resultDeliveryState === "delivery_failed";
667670
const isBrandBrowserAssistMessage =
668671
isBrandExtractionNextStepVariant(nextStepVariant) &&
669672
(message.content.includes('<od-card type="brand-browser-assist"') ||
@@ -682,6 +685,7 @@ function AssistantMessageImpl({
682685
const unfinishedTodos = streaming ? [] : unfinishedTodosFromEvents(events);
683686
const runSucceeded =
684687
!streaming &&
688+
!hasResultDeliveryFailure &&
685689
(
686690
message.runStatus === "succeeded" ||
687691
(!message.runStatus && !!message.endedAt) ||
@@ -1320,7 +1324,13 @@ function isFeedbackEligible({
13201324
hasEmptyResponse: boolean;
13211325
hasUnfinishedTodos: boolean;
13221326
}): boolean {
1323-
if (streaming || hasEmptyResponse || hasUnfinishedTodos) return false;
1327+
if (
1328+
streaming ||
1329+
hasEmptyResponse ||
1330+
hasUnfinishedTodos ||
1331+
message.resultDeliveryState === "no_result" ||
1332+
message.resultDeliveryState === "delivery_failed"
1333+
) return false;
13241334
if (message.runStatus) return isTerminalRunStatus(message.runStatus);
13251335
return !!message.endedAt;
13261336
}

apps/web/src/components/ChatPane.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
getDesignToolboxAction,
2929
type DesignToolboxActionId,
3030
} from '../runtime/design-toolbox';
31+
import { isRetryableAssistantTerminalFailure } from '../runtime/design-delivery';
3132
import type { Dict } from '../i18n/types';
3233
import { copyToClipboard } from '../lib/copy-to-clipboard';
3334
import { projectRawUrl } from '../providers/registry';
@@ -3708,7 +3709,7 @@ export function retryableAssistantMessage(
37083709
const last = messages[messages.length - 1];
37093710
if (!last || last.role !== 'assistant') return null;
37103711
if (last.id !== lastAssistantId) return null;
3711-
return last.runStatus === 'failed' ? last : null;
3712+
return isRetryableAssistantTerminalFailure(last) ? last : null;
37123713
}
37133714

37143715
export function isAssistantMessageStreaming(

0 commit comments

Comments
 (0)