Skip to content

fix(mothership): end refused replay turns cleanly and bound oversized stream events - #8450

Merged
waleedlatif1 merged 18 commits into
stagingfrom
fix/chat-long-task-stalls
Sep 30, 2026
Merged

waleedlatif1 merged 18 commits into
stagingfrom
fix/chat-long-task-stalls

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Long Chat turns could stall until the 60-minute run limit, or break on large tasks. The root cause is the replay buffer fence added in Mothership v1.0.0. Several related failures surfaced around it; this PR fixes all of them. The worker side is in simstudioai/mothership#552.

Replay buffer refusals no longer stall a turn

  • The leased StreamWriter.publish persists every event to the Redis replay buffer before delivering it or dispatching its tool. A budget refusal (1 MiB per write, 32 MiB per stream, 128 MiB per user per hour) threw a generic error that start.ts treated as a controller handoff. Nothing was delivered, the tool never ran, the run stayed recoverable, and every reconnect started a new controller that was refused again until the run deadline.
  • A refusal is now a distinct StreamReplayBudgetExhaustedError. The turn ends as an error with an honest message, the run is marked terminal before the lock is released, and the worker is told to stop. Only a genuine takeover still hands off.
  • Teardown runs only for a turn this controller ended while it still holds the lease, so a successor's buffer and Stop are never touched.
  • The terminal run status is written even when publishing the terminal events fails, except when the controller was superseded.

Oversized events are bounded before the fence

  • replay-compaction.ts works on the copy that is both delivered and persisted, so live and replay stay identical. Dispatch still uses the full event.
    • Strings over 8K are cut to their head, with an inline …[truncated, N total] note.
    • Arrays over 100 items keep their head plus a count.
    • A cheap size estimate skips small events.
    • Never compacted: arguments of client-executed tools (one shared isClientExecutedToolCall, also used by client dispatch), file-preview events, and assistant text, whose length is part of the text receipt.
    • Past one replay write, the smallest sufficient bulk is replaced by an …[omitted, N total] note in one pass over exact, memoized sizes. Fields beside it (ids, resources, status) are kept.
    • Anything still too large ends the turn as above.
  • File-preview snapshots are paced by size and bounded per frame (one replay write) and per turn (8 MiB, shared across resume legs and restores, final snapshots included). A skipped frame leaves the preview on its last content; on completion the client loads the stored file instead of caching the unfinished preview text.

Related fixes

  • Approval cards: a worker frame stamped awaiting_approval is cleared on replayed and partial frames. Only a live, complete frame can be gated, so Allow/Skip cards never appear while tool permissions are off. The gate itself is unchanged.
  • Unfinished tool rows: Stop, error and completion all settle them through one isUnsettledToolState. Stored messages render any leftover rows as interrupted instead of spinning.
  • Backend errors: user-facing messages never include upstream bodies. 5xx and HTML responses get a generic message, and 4xx responses show the worker's short, plain reason. Status and body stay on the error for logs.
  • Retries: the stream retry has two budgets. A worker that is unreachable (a gateway page or connection failure) is retried for up to 120 s, long enough for a task replacement. A reachable failure, including a stream cut mid-body, keeps the original 3 attempts in 30 s and shows the generic message instead of the raw socket error. Retry logs include the underlying network error. Retries reuse the same messageId, which the worker handles as a duplicate send.

Testing

  • New replay-budget.integration.ts against real Redis and Postgres covers:
    • oversized call and result frames delivered compacted, identical to the replay, with contiguous seqs;
    • an exhausted budget ending the turn with a terminal run, a worker abort, and no recovery controller on reconnect;
    • restore parity;
    • stale-lease and successor-protection cases;
    • settling a run when the terminal publish fails.
  • New and updated unit tests cover compaction (including a 16 MB balanced tree bounded in about 70 ms), the writer, retry budgets, backend messages, approval normalization, the settlers, display, and preview pacing.
  • Each regression test fails on the previous code, and reverting each guard turns its test red.
  • bun run lint, bun run type-check, bun run check:audits and the full apps/sim suite pass.

@vercel

vercel Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
docs Skipped Skipped Sep 30, 2026 7:45am UTC

Request Review

@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Refactors tool state handling and stream event finalization.

The PR appears safe to merge; no new actionable issue or outstanding previous finding remains.

Summary

The PR makes replay-budget refusals terminal instead of recoverable, bounds oversized stream events and file previews, and improves retry, approval, error-message, and unfinished-tool handling. The changes since the previous review add cancelled-turn persistence coverage and allow oversized preview metadata to be compacted.

Reviews (8) · Last reviewed commit: "fix(mothership): bound preview metadata ..."

Comment thread apps/sim/lib/mothership/request/go/file-preview-adapter.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 36 files

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread apps/sim/lib/mothership/request/session/replay-compaction.ts
Comment thread apps/sim/lib/mothership/request/go/file-preview-adapter.ts Outdated
Comment thread apps/sim/lib/mothership/request/session/replay-compaction.ts Outdated
Comment thread apps/sim/lib/mothership/request/lifecycle/run.ts
Comment thread apps/sim/lib/mothership/request/lifecycle/stream-retry.ts Outdated
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 37 files

Confidence score: 4/5

  • In apps/sim/lib/mothership/request/session/replay-budget.integration.ts, the tests could miss a regression that removes replay-batch splitting, allowing oversized combined batches to go unnoticed; add a case where individually writable frames exceed the batch limit when serialized together.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/sim/lib/mothership/request/session/replay-budget.integration.ts">

<violation number="1" location="apps/sim/lib/mothership/request/session/replay-budget.integration.ts:222">
P2: A regression that removes replay-batch splitting would still pass these tests because they never exercise an oversized batch. Add a case with multiple individually writable frames whose combined serialized size exceeds one write, and verify all frames persist.

(Based on your team's feedback about preview frame and replay batch bounds.) .</violation>
</file>

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Fix all with cubic | Re-trigger cubic

Comment thread apps/sim/lib/mothership/request/session/replay-compaction.ts
Comment thread apps/sim/lib/mothership/request/go/file-preview-adapter.ts
Comment thread apps/sim/lib/mothership/request/session/replay-compaction.ts Outdated
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found across 40 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread apps/sim/lib/mothership/request/session/replay-compaction.ts Outdated
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found across 40 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

… handing them off

A leased Chat stream persists every event to its Redis replay buffer before
delivering or dispatching it. When the buffer refused a write (a frame over
the 1 MiB single-write ceiling, or a stream past its 32 MiB budget), the
writer threw a generic error, the controller classified it as a takeover and
released its lock without finalizing, and every reconnect started a recovery
controller that re-received and re-refused the same event until the run
deadline. The refused tool call was never dispatched, so the worker waited.

Refusals:
- The writer throws StreamReplayBudgetExhaustedError and rejects later
  publishes the same way, but still delivers the turn's terminal error and
  complete events unpersisted; flush no longer rethrows the refusal.
- The controller aborts with that error rather than a supersession, the
  lifecycle classifies it as an error (not a user cancel), the run is
  finalized as an error with code replay_budget_exhausted, and the worker is
  told to stop. Reconnects then see a terminal run and start no controller.
- Only a genuine StreamControllerSupersededError still hands the run off.

Compaction, applied only to the copy the writer delivers and persists:
- Payloads over 256 KiB have long string leaves cut to an 8 KiB head in
  place, then medium strings to 500 characters, and only then the largest
  unprotected subtrees replaced by { omitted, bytes }, targeting 128 KiB.
  A payload-level streamTruncation marker lists every changed field.
- Identity fields, UI-read keys, arguments of client-executable calls, file
  preview events, and generate_api_key results are never compacted; such a
  frame that stays too large is refused and ends the turn cleanly.
- Oversized assistant text splits into contiguous exact pieces, and each tool
  call forwards only the head of its argument deltas.
A call frame whose whole argument object was omitted by stream compaction no longer replaces a tool node's arguments, and the permission card and generic tool output render omitted values as "Too large to show (N KB)" instead of raw marker JSON.
…ation pass

Compaction is now a single linear walk: when a payload's strings could
serialize past 256 KiB, string leaves longer than 8K units are cut to their
head with an inline "…[truncated, N total]" note, copying only what changes.
Identity and UI-read keys, file previews, generate_api_key results, and the
arguments of calls the browser executes are never cut; anything still over
the write ceiling is refused and ends the turn. The subtree omission, second
cut pass, truncation marker, text splitting, argument-delta cap, and the UI
stub handling are removed.

One predicate, isClientExecutedToolCall, now names the tool calls the browser
starts from the call frame; both the stream compaction and the client
dispatch use it, and the workflow tool names move beside it.

Also fixes three defects in the refusal path:
- A superseded controller that is refused an oversized frame no longer
  expires its successor's stream or clears its abort marker; teardown keys on
  whether this controller handed off, not on the abort reason.
- A refusal after a user Stop stays a cancellation: the abort reason decides.
- The per-user hourly ceiling gets its own message instead of telling the
  user to continue immediately.
…rrored-turn spinners

- A replayed call frame is history and never gates anything, so Sim now clears
  the worker's awaiting_approval stamp on it as it already does for live
  frames. Replays no longer render Allow/Skip cards when approvals are off.
- Stop settles every unfinished tool row (pending, executing, or awaiting
  approval) through one shared predicate, isUnsettledToolState, on the
  persisted, live, and pending-message paths.
- An errored turn settles its unfinished tool rows as errored when it is
  persisted, so they no longer reload as spinners.
- A failed backend response no longer puts the upstream body in the error
  shown to users: 5xx and non-JSON bodies say the service is temporarily
  unavailable, 4xx uses the worker's displayMessage or a generic message.
  The status and body stay on the error for logs.
…ot be gated

The worker stamps awaiting_approval on resolved integration calls and relies
on Sim to keep or clear it. Sim cleared it on live complete frames that were
not gated, but a partial frame returned before that decision and kept the
stamp. Only a live, complete call frame can be held behind a prompt, so every
replayed or partial frame now drops the stamp at the one point each frame
passes through before it is persisted and delivered. With approvals off, no
frame, replay, snapshot, or persisted row can carry the stamp to the client.
…n up only ended turns

- An unreachable worker and a reachable one now have independent retry
  budgets. Any answer from the worker restarts only the two-minute
  unreachable window; the three-retry, 30 s budget for failures of a worker
  that answered keeps its original semantics, so a failure that repeats
  after every reattach still stops, and a long outage no longer spends the
  replacement's retries.
- Intermediate file preview content is capped at 8M characters per edit;
  past it the preview holds until the final snapshot, which is always sent.
- A controller cleans up its stream only when it finalized the turn and
  still holds the lock, so a run it leaves recoverable keeps its buffer and
  any pending Stop.
- Remove the unreachable generate_api_key compaction exemption, avoid a
  second copy of trimmed arrays, and correct the size-estimate comment.
@cubic-dev-ai

cubic-dev-ai Bot commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

… previews per turn in bytes

- A controller that ended its turn but could not publish the terminal events
  still cleans up its stream: the run is settled either way, so the buffer no
  longer lingers for its full TTL. Only a superseded controller skips cleanup,
  and the lease check still protects a successor.
- The intermediate file preview cap is now 8 MiB of UTF-8 per stream loop,
  across all of its edits, instead of characters per edit. Each edit's final
  snapshot is still always sent.
…row unreachable retries

- A file preview frame whose content would not fit one replay write, or that
  would exceed the turn's preview budget, is skipped instead of sent: the
  preview holds its last content and the client loads the stored file on
  completion. A completion too large to send whole drops its tool output.
  The per-frame limit derives from the replay buffer's single-write ceiling,
  and the 8 MiB budget now covers every preview frame in the turn, finals
  included, across all legs. A large file preview no longer ends the turn.
- Only a request that fails before any response headers counts as an
  unreachable worker. A TypeError raised after the response began, including
  one from handling delivered events, keeps the three-retry budget, so a
  deterministic failure can no longer retry until the run deadline.
- A worker that cannot be reached is reported with the generic "temporarily
  unavailable" message; the network error stays on the error's cause.
- Retry logs and spans count retries from both budgets.
… and persist closed lanes

- An event whose many short values survive every string and array cut, such
  as an object with thousands of keys, now has its largest bulk field
  replaced by a size note when it would still exceed one replay write,
  instead of ending the turn. Identity fields and client-executed arguments
  are never replaced; such an event is still refused cleanly.
- finalizeResidualToolCalls reports closing an open subagent lane as a
  change, so the error finalizer persists it instead of leaving a stale lane.
…che a skipped preview as file content

- The last-resort omission for an event no cut can bound now walks down from
  the largest field while one child holds most of its parent's bytes and
  replaces only that node, so the ids, resources, and status fields beside
  the bulk keep driving the UI's resource updates.
- On completion the client seeds the file content cache from preview text
  only when it received the completed version. When the server skipped the
  final content frame, the text is an earlier draft, so the stored file loads
  instead.
A result with more than one large object of short fields left the second
one over the write limit after the first was omitted, so the event was
refused and the turn ended. Omission now repeats until the event fits or
nothing large enough is left; each pass removes at least 64 KiB, so it
always terminates.
…id-body stream cuts

- Replace the replay-compaction omit loop with one bounded pass over memoized
  exact serialized sizes: recurse into the smallest child that covers the
  overage, otherwise drop the largest children whole, keeping small siblings
- Map a non-abort body-read failure from the worker stream to a retryable
  WorkerStreamInterruptedError on the reachable budget, with the generic
  message and the original error on cause
- Log the cause message on retry warnings and orchestration failures
- Count replayed file preview content toward a restored turn's preview budget
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 41 files

You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Fix all with cubic | Re-trigger cubic

Comment thread apps/sim/lib/mothership/request/context/restore.ts Outdated
…eview

- Share the replay write ceiling between compaction and file previews
- Scan for the smallest sufficient child only once one can cover the need,
  and reuse one size memo across every compaction step
- Stop retrying a raw TypeError; fetch and body-read failures are already
  wrapped as WorkerUnreachableError and WorkerStreamInterruptedError
- Use absolute imports, a private retry counter, a typed unsettled-state set,
  and hasUnsettledTool naming
- Cover a timed-out body read and the preview completion edge cases
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 41 files

You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Fix all with cubic | Re-trigger cubic

Comment thread apps/sim/lib/mothership/request/session/replay-compaction.ts Outdated
Comment thread apps/sim/lib/mothership/chat/persisted-message.ts Outdated
…s on every save

- Compact every preview phase except content and completion, which the preview
  adapter already bounds; a model-written patch search string could otherwise
  exceed one replay write in an edit_meta frame
- Settle unfinished tool rows as stopped in buildPersistedAssistantMessage for a
  cancelled turn, so background and API callers that persist the result
  directly never save pending, executing, or awaiting-approval rows
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found across 41 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

@waleedlatif1
waleedlatif1 merged commit 85402d0 into staging Sep 30, 2026
23 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/chat-long-task-stalls branch September 30, 2026 07:51

This branch was previously deployed

1 inactive deployment
Preview — 77dedc2c Deployed Sep 30, 2026 by vercel[bot]
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