| author | |
| committer | |
| log | e4ef8d36bad346e0b8c38da276329cc54a1cce6f |
| tree | 9d210172dfbf9957573076715435822c0ca9baac |
| parent | 477282263a9115f911380b5aec7d4973267e4edc |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | ); |
| 2841 | 2841 | ||
| 2842 | it.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 | |||
| 2842 | it.effect( | 2909 | it.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 | ); |
| 1143 | 1143 | ||
| 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)); |
| 1178 | 1185 | ||
| 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 | ); |
| 1199 | 1209 | ||
| ... | @@ -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" |