diff --git a/api/server/routes/agents/index.js b/api/server/routes/agents/index.js index f634da0d16..9a6c6bde6e 100644 --- a/api/server/routes/agents/index.js +++ b/api/server/routes/agents/index.js @@ -218,6 +218,10 @@ router.get('/chat/status/:conversationId', async (req, res) => { aggregatedContent: resumeState?.aggregatedContent ?? [], createdAt: job.createdAt, resumeState, + // Surface the live pending approval so a client rebuilding from /chat/status + // (reload / cross-replica) has the action id + payload to render and submit + // the prompt, not just the knowledge that the stream is paused. + pendingAction: job.status === 'requires_action' && pendingLive ? pendingAction : undefined, }); }); diff --git a/packages/api/src/stream/ApprovalLifecycle.ts b/packages/api/src/stream/ApprovalLifecycle.ts index 1daff23c85..56fe00d859 100644 --- a/packages/api/src/stream/ApprovalLifecycle.ts +++ b/packages/api/src/stream/ApprovalLifecycle.ts @@ -77,6 +77,14 @@ export class ApprovalLifecycle { */ async resolve(streamId: string, expectedActionId?: string): Promise { const job = await this.store.getJob(streamId); + if (job?.status === 'requires_action' && !job.pendingAction) { + // The prompt was lost (e.g. a malformed record dropped on deserialize). + // It can't be reviewed, so finalize the job instead of driving a resumed + // run with no reviewed interrupt payload — consistent with how the active + // listing and cleanup treat a stale pending action. + await this.expire(streamId); + return false; + } if ( job?.status === 'requires_action' && job.pendingAction &&