authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-10-10 21:42:38-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-10 21:42:38-03:00
loge4ef8d36bad346e0b8c38da276329cc54a1cce6f
tree9d210172dfbf9957573076715435822c0ca9baac
parent477282263a9115f911380b5aec7d4973267e4edc
signature Signed by PGP key B5690EEEBB952194

fix(server): keep Claude workflows alive while they report progress (#17715)


2 files changed, 81 insertions(+), 4 deletions(-)

apps/server/src/orchestration-v2/ProviderSessionManager.test.ts+67
...@@ -2839,6 +2839,73 @@ it.effect("ProviderSessionManagerV2 releases pinned idle sessions once the pin c...@@ -2839,6 +2839,73 @@ it.effect("ProviderSessionManagerV2 releases pinned idle sessions once the pin c
2839 }),2839 }),
2840);2840);
28412841
2842it.effect("ProviderSessionManagerV2 keeps pinned work that reports progress", () =>
2843 Effect.gen(function* () {
2844 const state = yield* Ref.make(emptyState);
2845 const effect = Effect.gen(function* () {
2846 const eventSink = yield* EventSink.EventSinkV2;
2847 const idAllocator = yield* IdAllocator.IdAllocatorV2;
2848 const manager = yield* ProviderSessionManager.ProviderSessionManagerV2;
2849 const now = yield* DateTime.now;
2850 const threadId = yield* idAllocator.allocate.thread({
2851 fixtureName: "provider-session-manager-pin-progress",
2852 projectId: yield* idAllocator.allocate.project({
2853 fixtureName: "provider-session-manager-pin-progress",
2854 }),
2855 });
2856 const providerSessionId = yield* idAllocator.allocate.providerSession({
2857 providerInstanceId: modelSelection.instanceId,
2858 threadId,
2859 });
2860 yield* eventSink.write({
2861 events: [yield* makeThreadCreatedEvent({ idAllocator, threadId, now })],
2862 });
2863 const runtime = yield* manager.open({
2864 threadId,
2865 providerSessionId,
2866 modelSelection,
2867 runtimePolicy,
2868 });
2869 const adapterQueue = (yield* Ref.get(state)).eventQueues.get(String(providerSessionId));
2870 assert.isDefined(adapterQueue);
2871 const step = (duration: Parameters<typeof TestClock.adjust>[0]) =>
2872 Effect.gen(function* () {
2873 yield* TestClock.adjust(duration);
2874 for (let i = 0; i < 20; i += 1) yield* Effect.yieldNow;
2875 });
2876
2877 // A long workflow keeps reporting progress well past the pin cap.
2878 for (let tick = 0; tick < 12; tick += 1) {
2879 yield* step("1500 millis");
2880 yield* Queue.offer(adapterQueue!, {
2881 type: "provider_session.updated",
2882 driver: CODEX_DRIVER,
2883 providerSession: runtime.providerSession,
2884 });
2885 for (let i = 0; i < 20; i += 1) yield* Effect.yieldNow;
2886 }
2887 assert.equal((yield* Ref.get(state)).closeCount, 0);
2888
2889 // Work that goes silent past the cap is still stopped.
2890 for (let tick = 0; tick < 6; tick += 1) {
2891 yield* step("1 second");
2892 }
2893 assert.equal((yield* Ref.get(state)).closeCount, 1);
2894 });
2895
2896 yield* effect.pipe(
2897 Effect.provide(
2898 layerTest({
2899 state,
2900 idleTimeoutMs: 1000,
2901 maxIdlePinMs: 3000,
2902 hasPendingBackgroundWork: Effect.succeed(true),
2903 }),
2904 ),
2905 );
2906 }),
2907);
2908
2842it.effect(2909it.effect(
2843 "ProviderSessionManagerV2 does not idle-release a session that turns busy during the pending-work check",2910 "ProviderSessionManagerV2 does not idle-release a session that turns busy during the pending-work check",
2844 () =>2911 () =>
apps/server/src/orchestration-v2/ProviderSessionManager.ts+14-4
...@@ -1141,7 +1141,13 @@ export const layerWithOptions = (...@@ -1141,7 +1141,13 @@ export const layerWithOptions = (
1141 ),1141 ),
1142 );1142 );
11431143
1144 const scheduleIdleReleaseInternal = (providerSessionId: ProviderSessionId) =>1144 // Provider activity restarts the pin budget, so background work that keeps
1145 // reporting progress (a long Claude workflow) is never cut off; only work
1146 // that went silent for maxIdlePinMs is released.
1147 const scheduleIdleReleaseInternal = (
1148 providerSessionId: ProviderSessionId,
1149 options?: { readonly providerActivity?: boolean },
1150 ) =>
1145 Effect.gen(function* () {1151 Effect.gen(function* () {
1146 const key = sessionKey(providerSessionId);1152 const key = sessionKey(providerSessionId);
1147 const current = yield* Ref.get(sessions);1153 const current = yield* Ref.get(sessions);
...@@ -1168,6 +1174,7 @@ export const layerWithOptions = (...@@ -1168,6 +1174,7 @@ export const layerWithOptions = (
1168 idleGeneration: generation,1174 idleGeneration: generation,
1169 idleFiber,1175 idleFiber,
1170 lastActivityAtMs,1176 lastActivityAtMs,
1177 ...(options?.providerActivity === true ? { pinnedSinceMs: null } : {}),
1171 });1178 });
1172 return updated;1179 return updated;
1173 });1180 });
...@@ -1176,7 +1183,10 @@ export const layerWithOptions = (...@@ -1176,7 +1183,10 @@ export const layerWithOptions = (
1176 const scheduleIdleRelease = (providerSessionId: ProviderSessionId) =>1183 const scheduleIdleRelease = (providerSessionId: ProviderSessionId) =>
1177 withActivityError(providerSessionId, scheduleIdleReleaseInternal(providerSessionId));1184 withActivityError(providerSessionId, scheduleIdleReleaseInternal(providerSessionId));
11781185
1179 const touchActivity = (providerSessionId: ProviderSessionId) =>1186 const touchActivity = (
1187 providerSessionId: ProviderSessionId,
1188 options?: { readonly providerActivity?: boolean },
1189 ) =>
1180 withActivityError(1190 withActivityError(
1181 providerSessionId,1191 providerSessionId,
1182 Effect.gen(function* () {1192 Effect.gen(function* () {
...@@ -1193,7 +1203,7 @@ export const layerWithOptions = (...@@ -1193,7 +1203,7 @@ export const layerWithOptions = (
1193 });1203 });
1194 return updated;1204 return updated;
1195 });1205 });
1196 yield* scheduleIdleReleaseInternal(providerSessionId);1206 yield* scheduleIdleReleaseInternal(providerSessionId, options);
1197 }),1207 }),
1198 );1208 );
11991209
...@@ -1897,7 +1907,7 @@ export const layerWithOptions = (...@@ -1897,7 +1907,7 @@ export const layerWithOptions = (
1897 event.providerThreadId,1907 event.providerThreadId,
1898 event.runOrdinal,1908 event.runOrdinal,
1899 )1909 )
1900 : touchActivity(entry.runtime.providerSessionId),1910 : touchActivity(entry.runtime.providerSessionId, { providerActivity: true }),
1901 ).pipe(1911 ).pipe(
1902 Effect.andThen(1912 Effect.andThen(
1903 event.type === "provider_session.updated"1913 event.type === "provider_session.updated"