🛡️ fix: Codex round 5 — refuse unresolvable resolves; expose pending action

Two of three findings on c8abd826e1 (the third deferred to Slice B):

- J3 resolve() refuses a requires_action job that has lost its pendingAction
  (e.g. a malformed record dropped on deserialize): it expires/finalizes the
  job instead of driving a resumed run with no reviewed interrupt payload —
  consistent with how active-listing + cleanup already treat a stale prompt.
- J2 /chat/status returns the live pendingAction for a paused stream, so a
  client rebuilding from status (reload / cross-replica) has the action id +
  payload to render and submit the prompt, not just "paused".

Deferred (Slice B): J1 — emitting a terminal SSE event on approval expiry so
already-subscribed clients close. The store-level lifecycle can't emit
transport events, and there are no live SSE subscribers to a paused stream
until the Slice B runtime wiring exists; tracked for that work.

tsc + lint clean; policy + type-contract specs pass.
This commit is contained in:
Danny Avila 2026-06-16 15:05:58 -04:00
parent c8abd826e1
commit e7d9cf21b6
2 changed files with 12 additions and 0 deletions

View file

@ -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,
});
});

View file

@ -77,6 +77,14 @@ export class ApprovalLifecycle {
*/
async resolve(streamId: string, expectedActionId?: string): Promise<boolean> {
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 &&