Skip to content

📬 fix: Show Background Results Still Waiting for the Agent - #16363

Open
danny-avila wants to merge 1 commit into
devfrom
danny-avila/bg-task-delivery-state
Open

danny-avila wants to merge 1 commit into
devfrom
danny-avila/bg-task-delivery-state

Conversation

@danny-avila

Copy link
Copy Markdown
Collaborator

Summary

The chat header's background task list marks a task Completed as soon as its tool finishes. The agent may not have its result yet. A result that finishes while its conversation is busy waits for the running turn to end, then arrives as its own new agent turn. So the header can read "everything completed" while results keep arriving for minutes.

On the demo, one long turn started 12 background checks it never polled. They finished within seconds to about a minute, and the header showed them as completed. Their results then arrived 18 to 35 minutes later, as 12 separate wake-up turns in 7 minutes, mixed in with the user's own messages. Each result was delivered exactly once, so nothing was duplicated or lost. The header just had no way to say "finished, but not yet with the agent".

The header also read only the process-local task registry. With more than one replica, it never showed tasks owned by another process.

This PR makes the task index read the durable delivery store that the agent's check_background_task has used since #16343:

  • Each finished task carries delivery: pending (the result will still arrive as a new turn), delivered, or failed (automatic delivery dead-lettered).
  • Finished results that only the store knows about are listed too: settled on another replica, before a restart, or past the registry's retention. Running work on another replica stays out, because its stop control lives there.
  • The durable read is shared with check_background_task (readDurableCompletions and resolveTaskDelivery), so the header and the agent always agree on delivery state.
  • If the store is unreachable, the list still returns this process's tasks, as before.

In the header:

  • A finished task shows Result pending until its result reaches the agent, or Result not delivered if delivery failed.
  • Pending rows stay above delivered ones and do not age out while they wait.
  • The header dot stays on (not pulsing) while results are pending. The button's label reads "N results pending".
  • Pending-only lists poll every 10 s. Running work still polls every 2 s, and idle lists every 60 s. This adds one indexed MongoDB read per poll per open conversation.

Follow-ups for canary, not in this PR: #16352 coalesces a conversation's ready results into one wake-up turn, and a busy turn could receive finished results at its next tool step instead of after it ends.

Type of change

  • Bug fix

Testing

The motivating case is from the demo (dev at 7b2362d): background task deliveries and Langfuse tool calls for one conversation, matched by task id. That confirmed the late results were never polled in-turn and were each delivered once.

Automated tests:

  • tasks.spec.ts, new cases for the index route:
    • finished result pending until received
    • delivered once a wake-up on any replica took it
    • local view kept when the durable listing is truncated
    • finished results from the durable store listed, remote running work not
    • a dead-lettered local result shows as failed
    • store outage still returns local tasks
  • background.spec.ts and backgroundCompletionWakeup.spec.ts: existing check_background_task delivery cases, unchanged behavior on the shared helper. Fixtures now carry toolCallId.
  • rows.test.ts: ordering and retention of pending rows. queries.polling.test.tsx: poll intervals.
  • convos.spec.js, convos-duplicate-ratelimit.spec.js: the route module with the new dependency mocked.
  • npx tsc --noEmit in packages/api and client.

Screenshots / recordings

The finished task row gains a "Result pending" label next to "Completed".

Risk / compatibility

  • BackgroundTaskSummary.delivery is optional and additive.
  • The index handler is now async. Its only caller is the Express route.
  • PendingBackgroundCompletion now carries toolCallId, which the data layer already returned.
  • No schema, index, or config changes.

Checklist

  • I reviewed my own changes
  • Relevant tests have been added or updated
  • Existing relevant tests pass
  • The change does not introduce new warnings or errors
  • User-facing or complex behavior is documented where necessary
  • Required dependency changes have been merged/published
  • Required documentation PR: N/A

The chat header listed a background task as completed as soon as its tool
finished, while its result could still be queued behind a busy turn and
arrive minutes later as a new agent turn. The header also read only the
process-local registry, so tasks on another replica were invisible.

The task index now reads the durable delivery store the agent's
check_background_task already uses: each finished task carries
delivery pending, delivered or failed, and finished results known only to
the store are listed too. The header marks results still on their way,
keeps them ahead of delivered ones, and polls them every 10 s.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T15:55:44.646782Z cb7243f Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb7243f874

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

(row) =>
isActive(row) || row.settledAt == null || now - row.settledAt <= RECENT_SUBAGENT_WINDOW_MS,
isActive(row) ||
isAwaitingDelivery(row) ||

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve failed deliveries past the settlement window

When a result remains pending for more than the one-hour retention window and its wake-up then dead-letters, this predicate immediately removes the row: isAwaitingDelivery only recognizes pending, while settledAt is the original tool settlement time rather than the later delivery-failure time. Long-running parent turns can defer delivery indefinitely without consuming attempts, so users in this scenario never see the newly added “Result not delivered” state. Retain failed deliveries long enough to expose the failure, or base their expiry on the delivery transition time.

AGENTS.md reference: AGENTS.md:L42-L45

Useful? React with 👍 / 👎.

return;
}
const tasks = deps.registry.list(userId, conversationId);
const durable = await readDurable(deps, { userId, conversationId }, tasks);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Index the durable lookup before polling it

This adds a MongoDB read to every background-task request, including the quiet 60-second header poll, but listPendingAgentBackgroundToolCompletions filters by user, nested conversation/source fields, capability, and status and sorts by createdAt; triggerDeliverySchema has no compound index matching that lookup (the closest is { user, status, availableAt }). For users who accumulate persistent dead letters across many conversations, every open conversation repeatedly scans and sorts that user's unrelated delivery rows. Add an index matching the conversation-scoped query or avoid polling this durable lookup when it is unnecessary.

AGENTS.md reference: AGENTS.md:L51-L54

Useful? React with 👍 / 👎.

tasks: deps.registry.list(userId, conversationId).map(toBackgroundTaskSummary),
tasks: [
...tasks.map((task) => toBackgroundTaskSummary(task, resolveTaskDelivery(task, durable))),
...(durable?.pending ?? [])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Page the durable completion list instead of dropping its tail

When a conversation has more than 200 durable rows, this route consumes only the first page and silently omits remote completions beyond it. The underlying lookup sorts oldest-first and applies a shared 200-row limit before separating pending and dead entries, while dead letters intentionally never expire; after a restart or across replicas, 200 old dead rows can therefore occupy the page permanently and prevent every newer remote result from appearing in the header. Handle the incomplete result by paging, or query the visible categories separately so persistent dead rows cannot starve newer pending work.

AGENTS.md reference: AGENTS.md:L42-L45

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant