Skip to content

fix(foreground-fallback): protect managed child sessions and skip replay after streamed output - #1011

Open
adevwithpurpose wants to merge 2 commits into
alvinunreal:masterfrom
adevwithpurpose:fix/foreground-fallback-lifecycle
Open

fix(foreground-fallback): protect managed child sessions and skip replay after streamed output#1011
adevwithpurpose wants to merge 2 commits into
alvinunreal:masterfrom
adevwithpurpose:fix/foreground-fallback-lifecycle

Conversation

@adevwithpurpose

@adevwithpurpose adevwithpurpose commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What changed, and why was it needed?

Relates to #595 and #560 (orphaned/resumed background tasks). Two failure modes remain when a managed child session errors:

  1. Detachment: the foreground-fallback manager aborted and re-prompted every session on a retryable error, including background-task child sessions whose completion delivery is owned by their parent's task awaiter. Re-prompting a managed child outside its task lifecycle detaches it from that awaiter.
  2. Duplicate output / re-executed tools: when a session streamed a partial answer or issued tool calls and then hit a mid-stream retryable error, the fallback replayed the user turn, duplicating the already-emitted assistant output or re-executing the tools with duplicate side effects.

This change narrows the fallback's blast radius. It does not claim to fully fix orphan-result handling — that remains owned by the background-task lifecycle (#595/#560):

  • ForegroundFallbackManager now accepts a shouldHandleSession predicate. Sessions owned by the background-task lifecycle (registered on session.created with a parentID, present on the background job board, or seeded from session.list() at init) are skipped by both the abort path and the replay path, so fallback no longer detaches managed child sessions. After a plugin reload, pre-existing child sessions do not re-emit session.created, so ownership is also seeded from the session list snapshot at init (collectManagedChildSessionIDs) — a restart can no longer leave an in-flight child unprotected.
  • A replay guard (hasAssistantActivity) blocks re-queueing the user turn when the failed attempt already produced activity after the last user message: meaningful text (v1 and v2 shapes) or tool-call parts. Reasoning/retry parts do not count as activity. Fallback still applies when the attempt failed before producing any activity (first-token rate limits, pre-stream errors).
  • The fallback model and retry budget are only consumed when the replay actually happens. A skipped replay no longer records the next model as tried or resets the retry count, so a later retryable failure can still use the fallback model (regression-covered by a sequential-event test).
  • Stream-settlement hardening: the error event can race the persistence of the streamed output, so when the failed attempt's message is still open (time.created without time.completed), the guard waits once (150 ms) and re-checks session.messages() before deciding to replay. No fixed delay is added when nothing is in flight.

Verification

  • bun test src/hooks/foreground-fallback/index.test.ts: 109 pass, 0 fail (includes managed-child detachment guards, replay-guard text/tool/settlement tests, sequential-event state-consumption regression, and predicate wiring/order tests)
  • bun test -t "restart recovery|tracks background child sessions" src/hooks/task-session-manager/index.test.ts: 3 pass, 0 fail
  • bun run typecheck: pass
  • bun run check:ci: only pre-existing failures (see below) — changed files are Biome-clean
  • git diff --check: pass

Known pre-existing failures (present at base, not introduced here; both are Windows-only path-separator issues and pass on Linux CI):

  • task-session-manager "reads before and after launch attach with unique-line counts and caps" hard-codes a forward-slash path (src/large.ts vs src\large.ts).
  • cache-safety "every hook module defining a message transform is covered here" compares Bun.Glob results against forward-slash expectations and sees backslash paths on Windows.

@greptile-apps

greptile-apps Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR narrows foreground fallback so background-task child sessions remain under their owning task lifecycle and user turns are not replayed after partial assistant text has already streamed.

  • Adds a session-ownership predicate to both fallback entry paths.
  • Tracks child-session ownership from creation through deletion.
  • Detects meaningful assistant text across v1 and v2 message shapes before replaying.
  • Adds focused tests for ownership filtering and replay behavior.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete blocking or independently actionable non-blocking defects identified.

The ownership predicate is applied before both abort and replay, task-session events update ownership before fallback handles the same event, and the replay guard is scoped to meaningful assistant output after the latest replayable user turn.

Important Files Changed

Filename Overview
src/hooks/foreground-fallback/index.ts Adds ownership gates to abort and replay paths and prevents replay when the failed turn already emitted meaningful assistant text.
src/hooks/task-session-manager/index.ts Tracks child-session IDs on creation and removes tracked ownership during session deletion.
src/index.ts Wires shared managed-session state and background-board membership into the foreground fallback predicate.
src/hooks/foreground-fallback/index.test.ts Adds coverage for managed-session suppression and v1/v2 partial-output replay guards.
src/hooks/task-session-manager/index.test.ts Verifies child ownership is recorded immediately from session creation events.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  E[Retryable session error] --> O{Managed task child?}
  O -->|Yes| T[Leave recovery to task lifecycle]
  O -->|No| M[Load session messages]
  M --> A{Meaningful assistant text after last user turn?}
  A -->|Yes| K[Keep partial output and skip replay]
  A -->|No| F[Select fallback model]
  F --> R[Replay user turn]
Loading

Reviews (1): Last reviewed commit: "fix(foreground-fallback): protect backgr..." | Re-trigger Greptile

…te leaks, and stream races

- Replay guard now treats tool-call parts as attempt activity: replaying a
  turn whose attempt issued tools would re-execute them with duplicate side
  effects (hasAssistantActivity replaces hasMeaningfulAssistantOutput).
- Fallback model (sessionTried) and retry budget are only consumed when the
  replay actually happens; a skipped replay no longer marks the next model as
  tried or resets retries, so a later failure can still use it.
- Stream-settlement hardening: when the failed attempt's message is still
  open (time.created without time.completed), wait once for the stream to
  settle and re-check before replaying, closing the race where partial output
  lands after the guard reads session.messages().
- Restart recovery: seed managed child-session ownership from
  session.list() at init so pre-existing child sessions (no session.created
  re-emission after a plugin reload) stay under their task lifecycle
  (collectManagedChildSessionIDs).
- Tests: sequential-event (skipped replay does not consume the next model),
  tool-only replay skip, settle-wait land/replay paths, wiring/order
  predicate gating, and restart ownership collection/removal.
@mhenke mhenke added the bug Something isn't working label Aug 22, 2026
@mhenke

mhenke commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

*This was generated by AI during triage.#606 has been closed as superseded by this PR. This PR implements the ownership guard to prevent foreground fallback from aborting/re-prompting managed child sessions (the foreground fallback replay guard) and includes the replay-skip-after-streamed-output mitigation.

@mhenke

mhenke commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

@adevwithpurpose can you please resolve the conflicts and we can merge this in cc @alvinunreal

@adevwithpurpose

Copy link
Copy Markdown
Contributor Author

Done — rebased both commits onto master (f47b1c2) and resolved the conflicts:

  • src/index.ts: kept the new revivedRunTracker wiring alongside the managed child-session set
  • task-session-manager: combined the child-session cleanup with the newer isFallbackInProgress / hard-timeout guards in session.deleted
  • foreground-fallback: ownership gates and replay guard merged cleanly with the current ForegroundFallbackManager

Verified after rebase: tsc clean, 113 foreground-fallback + 144 task-session-manager tests passing, biome check clean, CI green on this branch. Ready to merge from our side.

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants