Repository navigation
fix(server): a restart that misses its turn starts as a new turn #32899
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
Changes from all commits
b51547c
024aab1
8f48085
14cd7cd
d3ff1ef
c987223
48782ca
80adbde
57c9f29
a9674b0
abf9f92
3e9ad88
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 | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1773,6 +1773,225 @@ it.effect("keeps a queued message queued when promoted into a run that Stop reac | |||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // regression: #32891 a restart that Stop already reached is lost | ||||||||||||||||||||||||||||||||||||||||||
| it.effect("refuses a steer but starts a restart sent into a run that Stop reached", () => | ||||||||||||||||||||||||||||||||||||||||||
| Effect.scoped( | ||||||||||||||||||||||||||||||||||||||||||
| Effect.gen(function* () { | ||||||||||||||||||||||||||||||||||||||||||
| const { started, steered, layer, startFirstTurn } = | ||||||||||||||||||||||||||||||||||||||||||
| yield* nextTurnSelectionHarness("restart-after-stop"); | ||||||||||||||||||||||||||||||||||||||||||
| yield* Effect.gen(function* () { | ||||||||||||||||||||||||||||||||||||||||||
| const orchestrator = yield* Orchestrator.OrchestratorV2; | ||||||||||||||||||||||||||||||||||||||||||
| const worker = yield* EffectWorker.OrchestrationEffectWorkerV2; | ||||||||||||||||||||||||||||||||||||||||||
| const threadId = yield* startFirstTurn; | ||||||||||||||||||||||||||||||||||||||||||
| const first = started[0]!; | ||||||||||||||||||||||||||||||||||||||||||
| // The worker has not delivered the interrupt yet, so the run still reads as running. | ||||||||||||||||||||||||||||||||||||||||||
| yield* orchestrator.dispatch({ | ||||||||||||||||||||||||||||||||||||||||||
| type: "run.interrupt", | ||||||||||||||||||||||||||||||||||||||||||
| commandId: CommandId.make("stop"), | ||||||||||||||||||||||||||||||||||||||||||
| threadId, | ||||||||||||||||||||||||||||||||||||||||||
| runId: first.runId, | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| const steerError = yield* orchestrator | ||||||||||||||||||||||||||||||||||||||||||
| .dispatch({ | ||||||||||||||||||||||||||||||||||||||||||
| type: "message.dispatch", | ||||||||||||||||||||||||||||||||||||||||||
| commandId: CommandId.make("steer"), | ||||||||||||||||||||||||||||||||||||||||||
| threadId, | ||||||||||||||||||||||||||||||||||||||||||
| messageId: MessageId.make("message:steer"), | ||||||||||||||||||||||||||||||||||||||||||
| text: "steer", | ||||||||||||||||||||||||||||||||||||||||||
| attachments: [], | ||||||||||||||||||||||||||||||||||||||||||
| modelSelection: runSelection, | ||||||||||||||||||||||||||||||||||||||||||
| dispatchMode: { type: "steer_active", targetRunId: first.runId }, | ||||||||||||||||||||||||||||||||||||||||||
| createdBy: "user", | ||||||||||||||||||||||||||||||||||||||||||
| creationSource: "web", | ||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||
| .pipe(Effect.flip); | ||||||||||||||||||||||||||||||||||||||||||
| assert.equal(steerError._tag, "OrchestratorDispatchError"); | ||||||||||||||||||||||||||||||||||||||||||
| assert.include(String(steerError.cause), "is stopping and cannot be steered"); | ||||||||||||||||||||||||||||||||||||||||||
| yield* orchestrator.dispatch({ | ||||||||||||||||||||||||||||||||||||||||||
| type: "message.dispatch", | ||||||||||||||||||||||||||||||||||||||||||
| commandId: CommandId.make("restart"), | ||||||||||||||||||||||||||||||||||||||||||
| threadId, | ||||||||||||||||||||||||||||||||||||||||||
| messageId: MessageId.make("message:restart"), | ||||||||||||||||||||||||||||||||||||||||||
| text: "restart", | ||||||||||||||||||||||||||||||||||||||||||
| attachments: [], | ||||||||||||||||||||||||||||||||||||||||||
| modelSelection: runSelection, | ||||||||||||||||||||||||||||||||||||||||||
| dispatchMode: { type: "restart_active", targetRunId: first.runId }, | ||||||||||||||||||||||||||||||||||||||||||
| createdBy: "user", | ||||||||||||||||||||||||||||||||||||||||||
| creationSource: "web", | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| yield* worker.drain(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| assert.deepEqual(steered, []); | ||||||||||||||||||||||||||||||||||||||||||
| assert.deepEqual( | ||||||||||||||||||||||||||||||||||||||||||
| started.map((turn) => turn.message.text), | ||||||||||||||||||||||||||||||||||||||||||
| ["first", "restart"], | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| }).pipe(Effect.provide(layer)); | ||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // regression: #32891 a restart that arrives after the turn ended is lost | ||||||||||||||||||||||||||||||||||||||||||
| it.effect.each([ | ||||||||||||||||||||||||||||||||||||||||||
| { status: "completed", settled: true }, | ||||||||||||||||||||||||||||||||||||||||||
| { status: "completed", settled: false }, | ||||||||||||||||||||||||||||||||||||||||||
| { status: "interrupted", settled: true }, | ||||||||||||||||||||||||||||||||||||||||||
| { status: "interrupted", settled: false }, | ||||||||||||||||||||||||||||||||||||||||||
| { status: "cancelled", settled: false }, | ||||||||||||||||||||||||||||||||||||||||||
| ] as const)( | ||||||||||||||||||||||||||||||||||||||||||
| "starts a restart that missed the turn ($status, run settled: $settled) as a new turn", | ||||||||||||||||||||||||||||||||||||||||||
| ({ status, settled: runSettled }) => | ||||||||||||||||||||||||||||||||||||||||||
| Effect.scoped( | ||||||||||||||||||||||||||||||||||||||||||
| Effect.gen(function* () { | ||||||||||||||||||||||||||||||||||||||||||
| const { events, started, steered, layer, startFirstTurn } = yield* nextTurnSelectionHarness( | ||||||||||||||||||||||||||||||||||||||||||
| `restart-after-${status}-${runSettled ? "settled" : "unsettled"}`, | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| yield* Effect.gen(function* () { | ||||||||||||||||||||||||||||||||||||||||||
| const orchestrator = yield* Orchestrator.OrchestratorV2; | ||||||||||||||||||||||||||||||||||||||||||
| const worker = yield* EffectWorker.OrchestrationEffectWorkerV2; | ||||||||||||||||||||||||||||||||||||||||||
| const threadId = yield* startFirstTurn; | ||||||||||||||||||||||||||||||||||||||||||
| const first = started[0]!; | ||||||||||||||||||||||||||||||||||||||||||
| const turn = (yield* orchestrator.getThreadProjection(threadId)).providerTurns[0]!; | ||||||||||||||||||||||||||||||||||||||||||
| const turnEnded = yield* orchestrator.streamDomainEvents.pipe( | ||||||||||||||||||||||||||||||||||||||||||
| Stream.filter( | ||||||||||||||||||||||||||||||||||||||||||
| (event) => | ||||||||||||||||||||||||||||||||||||||||||
| event.type === "provider-turn.updated" && | ||||||||||||||||||||||||||||||||||||||||||
| event.payload.id === turn.id && | ||||||||||||||||||||||||||||||||||||||||||
| event.payload.status === status, | ||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||
| Stream.take(1), | ||||||||||||||||||||||||||||||||||||||||||
| Stream.runDrain, | ||||||||||||||||||||||||||||||||||||||||||
| Effect.forkScoped, | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| yield* Queue.offer(events, { | ||||||||||||||||||||||||||||||||||||||||||
| type: "provider_turn.updated", | ||||||||||||||||||||||||||||||||||||||||||
| driver, | ||||||||||||||||||||||||||||||||||||||||||
| providerTurn: { ...turn, status, completedAt: yield* DateTime.now }, | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| yield* Fiber.join(turnEnded); | ||||||||||||||||||||||||||||||||||||||||||
| const settleRun = Effect.gen(function* () { | ||||||||||||||||||||||||||||||||||||||||||
| const settled = yield* orchestrator.streamDomainEvents.pipe( | ||||||||||||||||||||||||||||||||||||||||||
| Stream.filter( | ||||||||||||||||||||||||||||||||||||||||||
| (event) => | ||||||||||||||||||||||||||||||||||||||||||
| event.type === "run.updated" && | ||||||||||||||||||||||||||||||||||||||||||
| event.payload.id === first.runId && | ||||||||||||||||||||||||||||||||||||||||||
| event.payload.status !== "running", | ||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||
| Stream.take(1), | ||||||||||||||||||||||||||||||||||||||||||
| Stream.runDrain, | ||||||||||||||||||||||||||||||||||||||||||
| Effect.forkScoped, | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| yield* Queue.offer(events, { | ||||||||||||||||||||||||||||||||||||||||||
| type: "turn.terminal", | ||||||||||||||||||||||||||||||||||||||||||
| driver, | ||||||||||||||||||||||||||||||||||||||||||
| providerThreadId: turn.providerThreadId, | ||||||||||||||||||||||||||||||||||||||||||
| providerTurnId: turn.id, | ||||||||||||||||||||||||||||||||||||||||||
| runOrdinal: first.runOrdinal, | ||||||||||||||||||||||||||||||||||||||||||
| status, | ||||||||||||||||||||||||||||||||||||||||||
| failure: null, | ||||||||||||||||||||||||||||||||||||||||||
| threadDisposition: "reusable", | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1884
to
+1893
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. Fix the TypeScript error: The CI typecheck fails with TS2322 at this location. The matrix includes Map 🐛 Possible direction
Committable suggestion
|
||||||||||||||||||||||||||||||||||||||||||
| yield* Fiber.join(settled); | ||||||||||||||||||||||||||||||||||||||||||
| yield* worker.drain(); | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| if (runSettled) yield* settleRun; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| yield* orchestrator.dispatch({ | ||||||||||||||||||||||||||||||||||||||||||
| type: "message.dispatch", | ||||||||||||||||||||||||||||||||||||||||||
| commandId: CommandId.make("restart"), | ||||||||||||||||||||||||||||||||||||||||||
| threadId, | ||||||||||||||||||||||||||||||||||||||||||
| messageId: MessageId.make("message:restart"), | ||||||||||||||||||||||||||||||||||||||||||
| text: "restart", | ||||||||||||||||||||||||||||||||||||||||||
| attachments: [], | ||||||||||||||||||||||||||||||||||||||||||
| modelSelection: runSelection, | ||||||||||||||||||||||||||||||||||||||||||
| dispatchMode: { type: "restart_active", targetRunId: first.runId }, | ||||||||||||||||||||||||||||||||||||||||||
| createdBy: "user", | ||||||||||||||||||||||||||||||||||||||||||
| creationSource: "web", | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| yield* worker.drain(); | ||||||||||||||||||||||||||||||||||||||||||
| if (!runSettled) { | ||||||||||||||||||||||||||||||||||||||||||
| const restartLeftQueue = yield* orchestrator.streamDomainEvents.pipe( | ||||||||||||||||||||||||||||||||||||||||||
| Stream.filter( | ||||||||||||||||||||||||||||||||||||||||||
| (event) => | ||||||||||||||||||||||||||||||||||||||||||
| event.type === "run.updated" && | ||||||||||||||||||||||||||||||||||||||||||
| event.payload.userMessageId === "message:restart" && | ||||||||||||||||||||||||||||||||||||||||||
| event.payload.status !== "queued", | ||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||
| Stream.take(1), | ||||||||||||||||||||||||||||||||||||||||||
| Stream.runDrain, | ||||||||||||||||||||||||||||||||||||||||||
| Effect.forkScoped, | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| yield* settleRun; | ||||||||||||||||||||||||||||||||||||||||||
| yield* Fiber.join(restartLeftQueue); | ||||||||||||||||||||||||||||||||||||||||||
| yield* worker.drain(); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| assert.deepEqual(steered, []); | ||||||||||||||||||||||||||||||||||||||||||
| assert.deepEqual( | ||||||||||||||||||||||||||||||||||||||||||
| started.map((turn) => turn.message.text), | ||||||||||||||||||||||||||||||||||||||||||
| ["first", "restart"], | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| }).pipe(Effect.provide(layer)); | ||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // regression: #32891 a restart into a failed provider turn is lost | ||||||||||||||||||||||||||||||||||||||||||
| it.effect("queues a restart sent into a provider turn that failed before its run settled", () => | ||||||||||||||||||||||||||||||||||||||||||
| Effect.scoped( | ||||||||||||||||||||||||||||||||||||||||||
| Effect.gen(function* () { | ||||||||||||||||||||||||||||||||||||||||||
| const { events, started, steered, layer, startFirstTurn } = yield* nextTurnSelectionHarness( | ||||||||||||||||||||||||||||||||||||||||||
| "restart-after-failed-unsettled", | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| yield* Effect.gen(function* () { | ||||||||||||||||||||||||||||||||||||||||||
| const orchestrator = yield* Orchestrator.OrchestratorV2; | ||||||||||||||||||||||||||||||||||||||||||
| const worker = yield* EffectWorker.OrchestrationEffectWorkerV2; | ||||||||||||||||||||||||||||||||||||||||||
| const threadId = yield* startFirstTurn; | ||||||||||||||||||||||||||||||||||||||||||
| const first = started[0]!; | ||||||||||||||||||||||||||||||||||||||||||
| const turn = (yield* orchestrator.getThreadProjection(threadId)).providerTurns[0]!; | ||||||||||||||||||||||||||||||||||||||||||
| const turnFailed = yield* orchestrator.streamDomainEvents.pipe( | ||||||||||||||||||||||||||||||||||||||||||
| Stream.filter( | ||||||||||||||||||||||||||||||||||||||||||
| (event) => | ||||||||||||||||||||||||||||||||||||||||||
| event.type === "provider-turn.updated" && | ||||||||||||||||||||||||||||||||||||||||||
| event.payload.id === turn.id && | ||||||||||||||||||||||||||||||||||||||||||
| event.payload.status === "failed", | ||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||
| Stream.take(1), | ||||||||||||||||||||||||||||||||||||||||||
| Stream.runDrain, | ||||||||||||||||||||||||||||||||||||||||||
| Effect.forkScoped, | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| yield* Queue.offer(events, { | ||||||||||||||||||||||||||||||||||||||||||
| type: "provider_turn.updated", | ||||||||||||||||||||||||||||||||||||||||||
| driver, | ||||||||||||||||||||||||||||||||||||||||||
| providerTurn: { ...turn, status: "failed", completedAt: yield* DateTime.now }, | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| yield* Fiber.join(turnFailed); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| yield* orchestrator.dispatch({ | ||||||||||||||||||||||||||||||||||||||||||
| type: "message.dispatch", | ||||||||||||||||||||||||||||||||||||||||||
| commandId: CommandId.make("restart"), | ||||||||||||||||||||||||||||||||||||||||||
| threadId, | ||||||||||||||||||||||||||||||||||||||||||
| messageId: MessageId.make("message:restart"), | ||||||||||||||||||||||||||||||||||||||||||
| text: "restart", | ||||||||||||||||||||||||||||||||||||||||||
| attachments: [], | ||||||||||||||||||||||||||||||||||||||||||
| modelSelection: runSelection, | ||||||||||||||||||||||||||||||||||||||||||
| dispatchMode: { type: "restart_active", targetRunId: first.runId }, | ||||||||||||||||||||||||||||||||||||||||||
| createdBy: "user", | ||||||||||||||||||||||||||||||||||||||||||
| creationSource: "web", | ||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
| yield* worker.drain(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| assert.deepEqual(steered, []); | ||||||||||||||||||||||||||||||||||||||||||
| const projection = yield* orchestrator.getThreadProjection(threadId); | ||||||||||||||||||||||||||||||||||||||||||
| assert.equal( | ||||||||||||||||||||||||||||||||||||||||||
| projection.runs.find((run) => run.userMessageId === "message:restart")?.status, | ||||||||||||||||||||||||||||||||||||||||||
| "queued", | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| }).pipe(Effect.provide(layer)); | ||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| it.effect("queues a plain send behind the stopped queue and releases its hold", () => | ||||||||||||||||||||||||||||||||||||||||||
| Effect.scoped( | ||||||||||||||||||||||||||||||||||||||||||
| Effect.gen(function* () { | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
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.
Script executed:
Repository: pingdotgg/t3code
Length of output: 7760
Script executed:
Repository: pingdotgg/t3code
Length of output: 45450
Script executed:
Repository: pingdotgg/t3code
Length of output: 19947
Script executed:
Repository: pingdotgg/t3code
Length of output: 13936
Deliver an MCP restart after Stop when the queue is held.
If Stop holds an existing queued run, an MCP restart dispatched while the target is still active inherits that hold. The restart can then remain undelivered until a manual resume. If no queued run is held, it does not inherit a hold.
Make the restart deliverable without releasing the runs that Stop already held, or return
thread_not_sendableso the caller can retry. Add a test with an existing queued run,thread.stop, and an MCP restart.