-
Notifications
You must be signed in to change notification settings - Fork 3.1k
feat: show a waiting state when background tasks outlive the turn #4255
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1340,6 +1340,13 @@ const make = Effect.gen(function* () { | |
| case "session.started": | ||
| case "thread.started": | ||
| return true; | ||
| case "session.state.changed": | ||
| // Background-task roster events are only meaningful between | ||
| // turns: they map to a "ready" status, so applying a stale or | ||
| // replayed one mid-turn would null the active turn and settle a | ||
| // genuinely running turn. The turn lifecycle already clears | ||
| // pendingBackgroundTasks when a turn starts. | ||
| return event.payload.backgroundTasks === undefined || activeTurnId === null; | ||
| case "turn.started": | ||
| return !conflictsWithActiveTurn || conflictingTurnStartIsPendingTurnStart; | ||
| case "turn.completed": | ||
|
|
@@ -1369,10 +1376,20 @@ const make = Effect.gen(function* () { | |
| event.type === "turn.started" || | ||
| event.type === "turn.completed" | ||
| ) { | ||
| // A session.state.changed carrying a backgroundTasks roster is the | ||
| // between-turns "waiting on background work" signal (e.g. a Claude | ||
| // background shell that outlived its turn). It keeps the wire status | ||
| // "ready" — old clients degrade to today's idle look — and surfaces | ||
| // the roster via session.pendingBackgroundTasks for new clients. | ||
| const backgroundTaskRoster = | ||
| event.type === "session.state.changed" ? event.payload.backgroundTasks : undefined; | ||
| const status = (() => { | ||
| switch (event.type) { | ||
| case "session.state.changed": { | ||
| const runtimeStatus = orchestrationSessionStatusFromRuntimeState(event.payload.state); | ||
| const runtimeStatus = | ||
| backgroundTaskRoster !== undefined | ||
| ? "ready" | ||
| : orchestrationSessionStatusFromRuntimeState(event.payload.state); | ||
| return hasPendingTurnStart && runtimeStatus === "ready" ? "starting" : runtimeStatus; | ||
| } | ||
| case "turn.started": | ||
|
|
@@ -1432,6 +1449,13 @@ const make = Effect.gen(function* () { | |
| ); | ||
| } | ||
|
|
||
| // Roster events replace pendingBackgroundTasks wholesale (empty | ||
| // roster clears it); every other lifecycle event clears it — a | ||
| // turn starting or the session exiting supersedes the wait. | ||
| const pendingBackgroundTasks = | ||
| backgroundTaskRoster !== undefined && backgroundTaskRoster.length > 0 | ||
| ? backgroundTaskRoster | ||
| : undefined; | ||
| yield* orchestrationEngine.dispatch({ | ||
| type: "thread.session.set", | ||
| commandId: yield* providerCommandId(event, "thread-session-set"), | ||
|
|
@@ -1446,6 +1470,7 @@ const make = Effect.gen(function* () { | |
| runtimeMode: thread.session?.runtimeMode ?? "full-access", | ||
| activeTurnId: nextActiveTurnId, | ||
| lastError, | ||
| ...(pendingBackgroundTasks !== undefined ? { pendingBackgroundTasks } : {}), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Waiting roster cleared by idle eventsHigh Severity Non-roster lifecycle events, such as Reviewed by Cursor Bugbot for commit 16c1506. Configure here. |
||
| updatedAt: now, | ||
| }, | ||
| createdAt: now, | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Medium
Layers/ProviderRuntimeIngestion.ts:1348A stale or replayed non-empty
backgroundTasksroster event arriving after a newer empty roster restorespendingBackgroundTasks, falsely showing the thread as waiting on background work. Thesession.state.changedguard accepts any roster event whenactiveTurnIdisnull, so a late or duplicate non-empty roster re-populatespendingBackgroundTasksafter it was already cleared, and the thread remains stuck in that state until another lifecycle event arrives. Consider tracking a monotonic sequence or watermark on roster events so out-of-order replays are rejected.🚀 Reply "fix it for me" or copy this AI Prompt for your agent: