Skip to content

Commit b499e5f

Browse files
committed
fix(mothership): keep the ended-run message surface-neutral and below a replay refusal
The fallback is shared by Chat, workflow execute and inbox, so it no longer tells the reader to send a message. A replay refusal now wins by guard rather than by spread order, with a test that covers the combination.
1 parent e35bb91 commit b499e5f

2 files changed

Lines changed: 32 additions & 7 deletions

File tree

‎apps/sim/lib/mothership/request/lifecycle/run.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2037,6 +2037,28 @@ describe('runCopilotLifecycle', () => {
20372037
}
20382038
)
20392039

2040+
it('reports a replay refusal over a reasonless error terminal', async () => {
2041+
const abortController = new AbortController()
2042+
const refusal = ownerRefusal()
2043+
mockRunStreamLoop.mockImplementationOnce(
2044+
async (_url: string, _init: RequestInit, context: StreamingContext): Promise<void> => {
2045+
context.completionStatus = MothershipStreamV1CompletionStatus.error
2046+
abortController.abort(refusal)
2047+
}
2048+
)
2049+
2050+
const result = await runWithStreamAbort(abortController)
2051+
2052+
expect(result).toEqual(
2053+
expect.objectContaining({
2054+
success: false,
2055+
cancelled: false,
2056+
error: refusal.userMessage,
2057+
errorCode: REPLAY_BUDGET_EXHAUSTED_CODE,
2058+
})
2059+
)
2060+
})
2061+
20402062
it('keeps a Stop a cancellation when a replay refusal follows it', async () => {
20412063
const abortController = new AbortController()
20422064
mockRunStreamLoop.mockImplementationOnce(

‎apps/sim/lib/mothership/request/lifecycle/run.ts‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,11 @@ const logger = createLogger('CopilotLifecycle')
9090

9191
const COPILOT_MODEL_CONTENT_PROJECTION_ERROR = 'Copilot model input could not be safely projected'
9292

93-
/** Shown when the worker ends a turn with an error terminal but gives no reason. */
94-
const ENDED_RUN_MESSAGE =
95-
'This run had already ended before it could continue. Send a message to pick up where it left off.'
93+
/**
94+
* Shown when the worker ends a turn with an error terminal but gives no reason. Every surface
95+
* (Chat, workflow execute, inbox) reports it, so it carries no surface-specific next step.
96+
*/
97+
const ENDED_RUN_MESSAGE = 'This run had already ended before it could continue.'
9698

9799
class CopilotModelContentProjectionError extends Error {
98100
constructor() {
@@ -577,11 +579,12 @@ export async function runCopilotLifecycle(
577579
!refusal &&
578580
!turnWasAborted &&
579581
(backendFinishedTurn || (!context.completionStatus && context.errors.length === 0))
580-
// The worker sends an error terminal with no `error` event only when it rebuilds an
581-
// ended run from its log, which drops the stored reason: a resume or reattach that
582-
// reaches a run that already ended, for example at its deadline. Say so rather than
583-
// leave the turn to a generic failure; a reported reason always wins.
582+
// The worker sends an error terminal with no `error` event only when it replays a run
583+
// that already ended (for example at its deadline) to a resume or reattach, because
584+
// that replay does not carry the run's stored reason. Say so rather than leave the turn
585+
// to a generic failure; a reported reason or a replay refusal always wins.
584586
const endedWithoutReason =
587+
!refusal &&
585588
!turnWasAborted &&
586589
context.completionStatus === MothershipStreamV1CompletionStatus.error &&
587590
context.errors.length === 0

0 commit comments

Comments
 (0)