fix(agent-core-v2): notify the model of previous background tasks terminated by app exit - #3292
Conversation
🦋 Changeset detectedLatest commit: 23cc6c6 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
@codex review |
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3abfc1fa1e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| } | ||
| if (tasks.length === 0) return; | ||
| const lines = tasks.map((info) => previousSessionTaskLine(info)); | ||
| this.context.append({ |
There was a problem hiding this comment.
Route the resume notice through AgentReminder
This one-off model-facing event is appended directly to context, bypassing the package's required AgentReminder.notify path and duplicating its system-reminder wrapping and injection-origin semantics. Resolve AgentReminder through IAgentLifecycleService and notify from this restore hook so the reminder runtime remains the sole owner of delivery behavior.
AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L80-L82
Useful? React with 👍 / 👎.
| for (const info of this.ghosts.values()) { | ||
| if (info.status !== 'killed') continue; |
There was a problem hiding this comment.
Include already-lost ghosts when rebuilding the reminder
If the process exits again after markLoadedTasksLost() has persisted a task as lost but before this reminder and its delivery record are persisted, the next resume loads that task as terminal, so it is absent from lostTasks; this loop then rejects it because it only scans killed ghosts. The reminder is therefore permanently skipped after this realistic repeated-crash window. Scan undelivered lost ghosts here as well, using the same delivery-key checks.
Useful? React with 👍 / 👎.
| private async appendPreviousSessionTasksReminder( | ||
| lostTasks: readonly AgentTaskInfo[], | ||
| ): Promise<void> { |
There was a problem hiding this comment.
Restore the unified notice after conversation undo
When the user invokes conversation undo immediately after resuming, undo removes the trailing reminder and rolls back its new delivery keys, but reconcileNotificationDeliveryAfterUndo() only restores ordinary task notifications and never calls this routine. Exit-killed tasks remain suppressed and disappear entirely, while lost tasks fall back to the old thin notification. Re-run the unified reminder reconciliation after undo so notification delivery continues to follow conversation undo.
AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L76-L78
Useful? React with 👍 / 👎.
3abfc1f to
3e9e109
Compare
|
Addressed the review in the rework (commit 3e9e109):
|
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3e9e109130
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| } | ||
|
|
||
| function isPreviousSessionTermination(info: AgentTaskInfo): boolean { | ||
| if (info.status === 'lost') return true; |
There was a problem hiding this comment.
Do not assume every lost process was terminated
On the non-Windows node-local backend, processes are spawned detached by default (hostProcessService.ts:19-24), so a crash before graceful shutdown can leave a bash process running; keep_alive_on_exit=true deliberately creates the same state. Reconciliation nevertheless marks its record lost, and this unconditional classification makes the reminder claim it was terminated and instructs the model to rerun it, potentially duplicating servers or other side-effectful commands. Verify process death or tell the model that tracking was lost and to check the PID before rerunning.
Useful? React with 👍 / 👎.
| const marked: AgentTaskInfo = { ...info, resumeReminded: true }; | ||
| this.ghosts.set(info.taskId, marked); | ||
| await this.persistence.writeTask(marked); |
There was a problem hiding this comment.
Preserve reminder deduplication across truncated forks
When a turn-index fork retains records containing this reminder, sessionLifecycleService.ts:560-561,713 copies the wire history but deletes every retained agent's tasks directory. Because resumeReminded exists only in those task documents and is absent from the durable TaskTerminated record, the fork restores the same ghost with no marker and appends a duplicate reminder. Store the dedupe fact in replayable state that survives truncated forks or detect the retained injection before notifying.
Useful? React with 👍 / 👎.
3e9e109 to
70f31ad
Compare
|
Round 2 addressed (commit 70f31ad):
|
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 70f31ad01a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| } | ||
|
|
||
| private async appendPreviousSessionTasksReminder(): Promise<void> { | ||
| if (this.hasResumeReminderInHistory()) return; |
There was a problem hiding this comment.
Scope reminder deduplication to the current task set
After one resume reminder remains in history, a later resume following a second app exit returns here before examining the newly terminated ghosts. Tasks started after the first resume therefore receive neither this reminder nor their ordinary terminal notification, since restoreAgentTaskNotificationsNow() excludes all previous-session terminations. Treat each exit as a distinct one-off event and deduplicate against the relevant task IDs rather than any historical injection with this variant.
AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L80-L82
Useful? React with 👍 / 👎.
70f31ad to
fc3fd32
Compare
|
Round 3 addressed (commit fc3fd32):
|
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fc3fd32f97
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (info.kind === 'agent' && info.agentId !== undefined) { | ||
| return `- ${info.taskId} "${info.description}" (subagent, ${statusText}) — resume it with Agent(resume="${info.agentId}", prompt="Pick up where you left off; redo the last tool call if its result was never observed.") to continue from its prior context.`; |
There was a problem hiding this comment.
Avoid resuming subagents that remain active in a fork source
When an idle live session is forked while a background subagent is still running, sessionLifecycleService.ts:476-488 allows the fork and :520-524 copies its running task record; the fork then marks that copy lost while the original subagent continues in the source session. This instruction tells the forked model to resume the same subagent, which can run duplicate tool calls against the shared workspace. Distinguish fork-created ghosts from tasks actually lost during application exit, or prevent/background-drain such forks before offering the resume action.
Useful? React with 👍 / 👎.
| if (info.resumeReminded === true) continue; | ||
| if (!isPreviousSessionTermination(info)) continue; | ||
| tasks.push(info); |
There was a problem hiding this comment.
Skip lost tasks whose prior notification was already delivered
On the first resume after upgrading, an older persisted lost task has no resumeReminded field even if its existing task:<id>:lost message and delivery key are already in conversation history. This loop therefore treats the historical task as a new exit event and appends a second, stale reminder claiming it happened after the last turn. Check the existing task-notification delivery state/origin before including terminal lost ghosts, while still including genuinely undelivered lost tasks.
Useful? React with 👍 / 👎.
fc3fd32 to
fcb8a66
Compare
|
Round 4:
|
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fcb8a662e7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| tasks.push(info); | ||
| } | ||
| if (tasks.length === 0) return; | ||
| if (this.hasRemindedAll(tasks)) return; |
There was a problem hiding this comment.
Persist dedupe after matching a retained reminder
In a turn-index fork, the retained injection makes this branch return, but fork cleanup removes the retained agent's tasks directory, so the ghost never acquires resumeReminded. Fresh evidence beyond the prior fork thread is that after this apparent fix, a later exit that loses a new task makes hasRemindedAll false for the combined old/new set and the next reminder lists the old task again. Mark the history-covered ghosts before returning or store the one-off delivery fact in replayable state that survives forks.
AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L80-L82
Useful? React with 👍 / 👎.
| } | ||
|
|
||
| function previousSessionStatusText(info: AgentTaskInfo): string { | ||
| if (info.status !== 'lost') return 'killed on exit'; |
There was a problem hiding this comment.
Avoid claiming exit-killed processes actually stopped
When graceful shutdown cannot kill a bash process, this text still states that it was killed and can encourage the model to start a duplicate. Fresh evidence beyond the prior lost-process thread is that the killed path remains unverified: terminateWithGrace() swallows force-stop failures before persisting killed, and on Windows hostProcessService.ts:116-128 resolves even when taskkill fails. Use stop-requested wording for processes or verify that the PID exited before making this claim.
Useful? React with 👍 / 👎.
fcb8a66 to
b4d43d0
Compare
…ngle reminder on resume
b4d43d0 to
ebd32da
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ebd32da6a6
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| for (const info of tasks) { | ||
| const marked: AgentTaskInfo = { ...info, resumeReminded: true }; | ||
| this.ghosts.set(info.taskId, marked); | ||
| await this.persistence.writeTask(marked); |
There was a problem hiding this comment.
Do not make resume depend on writing dedupe markers
When task storage is readable but this marker write fails—for example because of read-only permissions, a full disk, or transient I/O—the rejection escapes the onDidRestore hook; EventDispatcherService.restore() then marks restoration as failed, so a session with a cleanly persisted killed task can no longer resume. Treat this bookkeeping write as best-effort or record deduplication through the durable reminder-delivery path rather than allowing it to abort session restoration.
Useful? React with 👍 / 👎.
Related Issue
N/A — internal behavior fix; the motivation is described below.
Problem
When the user exits the app with background tasks (bash commands or subagents) still running and later resumes the session, the model never learns those tasks are gone. It keeps waiting for completion notifications that will never arrive, and may reason as if the tasks had succeeded.
stopAllOnExit): detached tasks are killed withstopReason = 'Session closed'and their terminal notification is suppressed — nothing reaches the model.running, reconcile marks itlost, and the per-task restore path injects a thin<notification>xxx lost.</notification>line that explains neither the cause nor what to do next.What changed
On resume, reconciliation now delivers one unified
ReminderRuntime.notifycovering every background task that was still running when the previous process exited:lost(process died without a terminal record), orkilledwithstopReason = 'Session closed'(the exit path actively stopped it).Agent(resume="agentId")recovery guidance (their context is persisted); bash tasks are listed without claiming death ("the process may still be running").resumeReminded?: booleanmarker on each ghost task so repeated reconcile passes and history replay can never re-inject the same reminder.stopReasondiffers), and excludes lost ghosts from the per-task restore path (the unified reminder replaces it, not duplicates it).Mechanically this is deliberately small: a single collector + one notify call + one persistence write, no new durable event, no new delivery-key space, no new registry. The existing rejection/suppression mechanisms are untouched.
Example (user resumes after the app exits):
Diff-size note: the final net change is roughly +50 lines of feel code (collector + reminder + persistence marker + tests), because the reminder is an additive capability; everything that could be deleted without harming existing behavior was deleted in earlier passes of this PR (suppression-abuse checks, an invented delivery-state event, fold registrations, dispatch infrastructure). What remains is the irreducible code for the new path. Tests (idle-notification-repro, rpc-events) were rewritten, not added, wherever possible.
Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update. (No doc update needed.)