From 101eac62dfec6928d59e347aef1a2b7a30b786af Mon Sep 17 00:00:00 2001 From: Matthew Riddell Date: Fri, 6 Nov 2026 09:06:59 -0500 Subject: [PATCH] fix(server): leave the console idle window to the client (#27890) Automated wakes start a console turn about every five minutes, so the thread's updatedAt was never 30 minutes old and every renewal was refused (c379877). Matt chose idle = 30 minutes since his own last message with no turn running (q7106). Only the client can tell his messages from automated input, so the server drops its updatedAt idle window. It keeps the expectedUpdatedAt compare-and-swap and every unfinished-work refusal. Co-Authored-By: Claude Opus 5.5 --- .../src/orchestration-v2/freshProviderContext.test.ts | 7 ++++--- apps/server/src/orchestration-v2/freshProviderContext.ts | 8 ++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/apps/server/src/orchestration-v2/freshProviderContext.test.ts b/apps/server/src/orchestration-v2/freshProviderContext.test.ts index 82cc8566a2ecf..cf4585e674fe5 100644 --- a/apps/server/src/orchestration-v2/freshProviderContext.test.ts +++ b/apps/server/src/orchestration-v2/freshProviderContext.test.ts @@ -155,12 +155,13 @@ it.effect.each(["none", "background", "turn", "metadata"] as const)( bootstrap: "Role; complete handoff; lessons pointer.", }, }; + // The idle policy is the client's: a just-completed, unchanged snapshot is accepted. assert.equal( typeof prepareFreshProviderThread(before, command, now), - "string", - "NO: less than 30 minutes", + "object", + "server must not re-impose a thread-updatedAt idle window", ); - yield* TestClock.adjust("31 minutes"); + yield* TestClock.adjust("1 minute"); const later = yield* DateTime.now; const refuse = (records: typeof before, cmd = command) => assert.equal(typeof prepareFreshProviderThread(records, cmd, later), "string"); diff --git a/apps/server/src/orchestration-v2/freshProviderContext.ts b/apps/server/src/orchestration-v2/freshProviderContext.ts index 779f815143f08..f6de0d34069d3 100644 --- a/apps/server/src/orchestration-v2/freshProviderContext.ts +++ b/apps/server/src/orchestration-v2/freshProviderContext.ts @@ -29,6 +29,8 @@ export function preserveFreshContext( // Refuse before emitting anything. The command's timestamp is a compare-and-swap // against the saved handoff's snapshot, including input queued since that snapshot. +// The idle window is the client's: automated turns bump updatedAt, so only the +// client can measure time since the human's last message. export function prepareFreshProviderThread( records: ProjectionRecords< "providerThreads" | "runs" | "providerTurns" | "runtimeRequests" | "subagents" | "turnItems" @@ -57,12 +59,6 @@ export function prepareFreshProviderThread( ) { return "Thread changed since the handoff snapshot."; } - if ( - DateTime.toEpochMillis(now) - DateTime.toEpochMillis(records.thread.updatedAt) < - 30 * 60_000 - ) { - return "Thread has not been idle for 30 minutes."; - } if (!seed.bootstrap.trim() || new TextEncoder().encode(seed.bootstrap).length >= 5_000) { return "Bootstrap must contain fewer than 5000 UTF-8 bytes."; }