| @@ -28,6 +28,10 @@ const TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY = 10_000; · 27 unmodified lines |
| 28 | 28 | const TURN_MESSAGE_IDS_BY_TURN_TTL = Duration.minutes(120); |
| 29 | 29 | const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_CACHE_CAPACITY = 20_000; |
| 30 | 30 | const BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL = Duration.minutes(120); |
| 31 | +const BUFFERED_ASSISTANT_MESSAGE_SPILLED_CACHE_CAPACITY = 20_000; |
| 32 | +const BUFFERED_ASSISTANT_MESSAGE_SPILLED_TTL = Duration.minutes(120); |
| 33 | +const MAX_BUFFERED_ASSISTANT_CHARS = 24_000; |
| 34 | +const BUFFERED_ASSISTANT_KEEP_CHARS = 8_000; |
| 31 | 35 | |
| 32 | 36 | type TurnStartRequestedDomainEvent = Extract< |
| 33 | 37 | OrchestrationEvent, |
| @@ -169,6 +173,11 @@ const make = Effect.gen(function* () { · 135 unmodified lines |
| 169 | 173 | timeToLive: BUFFERED_MESSAGE_TEXT_BY_MESSAGE_ID_TTL, |
| 170 | 174 | lookup: () => Effect.succeed(""), |
| 171 | 175 | }); |
| 176 | + const bufferedAssistantMessageSpilledByMessageId = yield* Cache.make<MessageId, true>({ |
| 177 | + capacity: BUFFERED_ASSISTANT_MESSAGE_SPILLED_CACHE_CAPACITY, |
| 178 | + timeToLive: BUFFERED_ASSISTANT_MESSAGE_SPILLED_TTL, |
| 179 | + lookup: () => Effect.succeed(true), |
| 180 | + }); |
| 172 | 181 | |
| 173 | 182 | const rememberAssistantMessageId = ( |
| 174 | 183 | sessionId: ProviderSessionId, |
| @@ -226,14 +235,23 @@ const make = Effect.gen(function* () { · 51 unmodified lines |
| 226 | 235 | const appendBufferedAssistantText = (messageId: MessageId, delta: string) => |
| 227 | 236 | Cache.getOption(bufferedAssistantTextByMessageId, messageId).pipe( |
| 228 | 237 | Effect.flatMap((existingText) => |
| 229 | | − Cache.set( |
| 230 | | − bufferedAssistantTextByMessageId, |
| 231 | | − messageId, |
| 232 | | − Option.match(existingText, { |
| 238 | + Effect.gen(function* () { |
| 239 | + const nextText = Option.match(existingText, { |
| 233 | 240 | onNone: () => delta, |
| 234 | 241 | onSome: (text) => `${text}${delta}`, |
| 235 | | − }), |
| 236 | | − ), |
| 242 | + }); |
| 243 | + if (nextText.length <= MAX_BUFFERED_ASSISTANT_CHARS) { |
| 244 | + yield* Cache.set(bufferedAssistantTextByMessageId, messageId, nextText); |
| 245 | + return ""; |
| 246 | + } |
| 247 | + |
| 248 | + // Safety valve: spill oldest text as a persisted assistant delta to cap memory. |
| 249 | + const spillChars = Math.max(1, nextText.length - BUFFERED_ASSISTANT_KEEP_CHARS); |
| 250 | + const spillChunk = nextText.slice(0, spillChars); |
| 251 | + const retainedTail = nextText.slice(spillChars); |
| 252 | + yield* Cache.set(bufferedAssistantTextByMessageId, messageId, retainedTail); |
| 253 | + return spillChunk; |
| 254 | + }), |
| 237 | 255 | ), |
| 238 | 256 | ); |
| 239 | 257 | |
| @@ -249,18 +267,51 @@ const make = Effect.gen(function* () { · 9 unmodified lines |
| 249 | 267 | const clearBufferedAssistantText = (messageId: MessageId) => |
| 250 | 268 | Cache.invalidate(bufferedAssistantTextByMessageId, messageId); |
| 251 | 269 | |
| 252 | | − const clearAssistantMessageState = (messageId: MessageId) => clearBufferedAssistantText(messageId); |
| 270 | + const markBufferedAssistantMessageSpilled = (messageId: MessageId) => |
| 271 | + Cache.set(bufferedAssistantMessageSpilledByMessageId, messageId, true); |
| 272 | + |
| 273 | + const takeBufferedAssistantMessageSpilled = (messageId: MessageId) => |
| 274 | + Cache.getOption(bufferedAssistantMessageSpilledByMessageId, messageId).pipe( |
| 275 | + Effect.flatMap((spilled) => |
| 276 | + Cache.invalidate(bufferedAssistantMessageSpilledByMessageId, messageId).pipe( |
| 277 | + Effect.as(Option.isSome(spilled)), |
| 278 | + ), |
| 279 | + ), |
| 280 | + ); |
| 281 | + |
| 282 | + const clearBufferedAssistantMessageSpilled = (messageId: MessageId) => |
| 283 | + Cache.invalidate(bufferedAssistantMessageSpilledByMessageId, messageId); |
| 284 | + |
| 285 | + const clearAssistantMessageState = (messageId: MessageId) => |
| 286 | + Effect.all( |
| 287 | + [clearBufferedAssistantText(messageId), clearBufferedAssistantMessageSpilled(messageId)], |
| 288 | + { concurrency: 1 }, |
| 289 | + ).pipe(Effect.asVoid); |
| 253 | 290 | |
| 254 | | − const dispatchAssistantCompletion = (input: { |
| 291 | + const finalizeAssistantMessage = (input: { |
| 255 | 292 | event: ProviderRuntimeEvent; |
| 256 | 293 | threadId: ThreadId; |
| 257 | 294 | messageId: MessageId; |
| 258 | 295 | turnId?: TurnId; |
| 259 | 296 | createdAt: string; |
| 260 | 297 | commandTag: string; |
| 298 | + spillRemainderCommandTag: string; |
| 261 | 299 | }) => |
| 262 | 300 | Effect.gen(function* () { |
| 263 | 301 | const text = yield* takeBufferedAssistantText(input.messageId); |
| 302 | + const spilled = yield* takeBufferedAssistantMessageSpilled(input.messageId); |
| 303 | + |
| 304 | + if (spilled && text.length > 0) { |
| 305 | + yield* orchestrationEngine.dispatch({ |
| 306 | + type: "thread.message.assistant.delta", |
| 307 | + commandId: providerCommandId(input.event, input.spillRemainderCommandTag), |
| 308 | + threadId: input.threadId, |
| 309 | + messageId: input.messageId, |
| 310 | + delta: text, |
| 311 | + ...(input.turnId ? { turnId: input.turnId } : {}), |
| 312 | + createdAt: input.createdAt, |
| 313 | + }); |
| 314 | + } |
| 264 | 315 | |
| 265 | 316 | yield* orchestrationEngine.dispatch({ |
| 266 | 317 | type: "thread.message.assistant.complete", |
| @@ -268,7 +319,7 @@ const make = Effect.gen(function* () { · 1 unmodified line |
| 268 | 319 | threadId: input.threadId, |
| 269 | 320 | messageId: input.messageId, |
| 270 | 321 | ...(input.turnId ? { turnId: input.turnId } : {}), |
| 271 | | − text, |
| 322 | + text: spilled ? "" : text, |
| 272 | 323 | createdAt: input.createdAt, |
| 273 | 324 | }); |
| 274 | 325 | yield* clearAssistantMessageState(input.messageId); |
| @@ -369,7 +420,19 @@ const make = Effect.gen(function* () { · 94 unmodified lines |
| 369 | 420 | |
| 370 | 421 | const assistantDeliveryMode = yield* Ref.get(assistantDeliveryModeRef); |
| 371 | 422 | if (assistantDeliveryMode === "buffered") { |
| 372 | | − yield* appendBufferedAssistantText(assistantMessageId, event.delta); |
| 423 | + const spillChunk = yield* appendBufferedAssistantText(assistantMessageId, event.delta); |
| 424 | + if (spillChunk.length > 0) { |
| 425 | + yield* markBufferedAssistantMessageSpilled(assistantMessageId); |
| 426 | + yield* orchestrationEngine.dispatch({ |
| 427 | + type: "thread.message.assistant.delta", |
| 428 | + commandId: providerCommandId(event, "assistant-delta-buffer-spill"), |
| 429 | + threadId: thread.id, |
| 430 | + messageId: assistantMessageId, |
| 431 | + delta: spillChunk, |
| 432 | + ...(turnId ? { turnId } : {}), |
| 433 | + createdAt: now, |
| 434 | + }); |
| 435 | + } |
| 373 | 436 | } else { |
| 374 | 437 | yield* orchestrationEngine.dispatch({ |
| 375 | 438 | type: "thread.message.assistant.delta", |
| @@ -390,13 +453,14 @@ const make = Effect.gen(function* () { · 14 unmodified lines |
| 390 | 453 | yield* rememberAssistantMessageId(event.sessionId, turnId, assistantMessageId); |
| 391 | 454 | } |
| 392 | 455 | |
| 393 | | − yield* dispatchAssistantCompletion({ |
| 456 | + yield* finalizeAssistantMessage({ |
| 394 | 457 | event, |
| 395 | 458 | threadId: thread.id, |
| 396 | 459 | messageId: assistantMessageId, |
| 397 | 460 | ...(turnId ? { turnId } : {}), |
| 398 | 461 | createdAt: now, |
| 399 | 462 | commandTag: "assistant-complete", |
| 463 | + spillRemainderCommandTag: "assistant-delta-buffer-remainder", |
| 400 | 464 | }); |
| 401 | 465 | |
| 402 | 466 | if (turnId) { |
| @@ -412,13 +476,14 @@ const make = Effect.gen(function* () { · 9 unmodified lines |
| 412 | 476 | assistantMessageIds, |
| 413 | 477 | (assistantMessageId) => |
| 414 | 478 | Effect.gen(function* () { |
| 415 | | − yield* dispatchAssistantCompletion({ |
| 479 | + yield* finalizeAssistantMessage({ |
| 416 | 480 | event, |
| 417 | 481 | threadId: thread.id, |
| 418 | 482 | messageId: assistantMessageId, |
| 419 | 483 | turnId, |
| 420 | 484 | createdAt: now, |
| 421 | 485 | commandTag: "assistant-complete-finalize", |
| 486 | + spillRemainderCommandTag: "assistant-delta-buffer-remainder-finalize", |
| 422 | 487 | }); |
| 423 | 488 | }), |
| 424 | 489 | { concurrency: 1 }, |