@vikrant82/opencode-cache-keepalive 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/MIGRATION.md +57 -0
  2. package/README.md +75 -48
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +1097 -614
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/config.d.ts +8 -25
  7. package/dist/lib/config.d.ts.map +1 -1
  8. package/dist/lib/control.d.ts +10 -4
  9. package/dist/lib/control.d.ts.map +1 -1
  10. package/dist/lib/paths.d.ts +5 -12
  11. package/dist/lib/paths.d.ts.map +1 -1
  12. package/dist/lib/replay-warmer.d.ts +95 -0
  13. package/dist/lib/replay-warmer.d.ts.map +1 -0
  14. package/dist/lib/state.d.ts +15 -114
  15. package/dist/lib/state.d.ts.map +1 -1
  16. package/dist/lib/tui/commands.d.ts +2 -0
  17. package/dist/lib/tui/commands.d.ts.map +1 -1
  18. package/dist/lib/tui/footer.d.ts +5 -5
  19. package/dist/lib/tui/footer.d.ts.map +1 -1
  20. package/dist/lib/tui/format.d.ts +8 -0
  21. package/dist/lib/tui/format.d.ts.map +1 -1
  22. package/dist/lib/types.d.ts +123 -0
  23. package/dist/lib/types.d.ts.map +1 -0
  24. package/dist/tui.d.ts +3 -0
  25. package/dist/tui.d.ts.map +1 -1
  26. package/lib/config.ts +106 -74
  27. package/lib/control.ts +69 -20
  28. package/lib/paths.ts +12 -14
  29. package/lib/replay-warmer.ts +1200 -0
  30. package/lib/state.ts +172 -175
  31. package/lib/tui/commands.tsx +76 -0
  32. package/lib/tui/footer.tsx +55 -130
  33. package/lib/tui/format.ts +40 -0
  34. package/lib/types.ts +130 -0
  35. package/package.json +3 -2
  36. package/tui.tsx +40 -0
  37. package/dist/lib/keepalive.d.ts +0 -85
  38. package/dist/lib/keepalive.d.ts.map +0 -1
  39. package/dist/lib/model.d.ts +0 -8
  40. package/dist/lib/model.d.ts.map +0 -1
  41. package/dist/lib/system.d.ts +0 -11
  42. package/dist/lib/system.d.ts.map +0 -1
  43. package/lib/keepalive.ts +0 -636
  44. package/lib/model.ts +0 -19
  45. package/lib/system.ts +0 -19
  46. package/lib/tui/commands.ts +0 -59
@@ -0,0 +1,1200 @@
1
+ import type { KeepaliveConfig } from "./config"
2
+ import type { Logger } from "./logger"
3
+ import type {
4
+ ReplayResult,
5
+ ResumeResult,
6
+ SessionWarmEntry,
7
+ WarmApi,
8
+ WarmState,
9
+ WarmStateSink,
10
+ } from "./types"
11
+ import { readSessionControls } from "./control"
12
+
13
+ type Recorded = {
14
+ url: string
15
+ method: string
16
+ headers: Headers
17
+ body: string
18
+ model: string
19
+ api: WarmApi
20
+ sentAt: number
21
+ intervalMs: number
22
+ }
23
+ type Session = {
24
+ request: Recorded
25
+ entry: SessionWarmEntry
26
+ timer?: ReturnType<typeof setTimeout>
27
+ controller?: AbortController
28
+ runningTools: Set<string>
29
+ active: boolean
30
+ busy: boolean
31
+ realRequests: number
32
+ previousPrompt?: number
33
+ replayReadTokens: number
34
+ pendingResume?: { replays: number; promptPrev: number; replayReadTokens: number }
35
+ retries: number
36
+ child: boolean
37
+ stopLoggedGap?: number
38
+ touchedAt: number
39
+ }
40
+ type Runtime = {
41
+ originalFetch: typeof fetch
42
+ sessions: Map<string, Session>
43
+ owners: Map<string, ReplayWarmer>
44
+ instancesSet: Set<ReplayWarmer>
45
+ childSessions: Set<string>
46
+ now: () => number
47
+ loggedApis: Set<WarmApi>
48
+ maxStoredBytes: number
49
+ storedBytes: number
50
+ hosts: Set<string>
51
+ pruneTimer?: ReturnType<typeof setInterval>
52
+ instances: Map<
53
+ ReplayWarmer,
54
+ {
55
+ enabled: boolean
56
+ controls: Map<string, boolean>
57
+ effective: Map<string, boolean>
58
+ directory: string
59
+ hosts: Set<string>
60
+ config: KeepaliveConfig
61
+ sink: WarmStateSink
62
+ }
63
+ >
64
+ }
65
+ const STATE_KEY = Symbol.for("opencode-cache-keepalive.replay-warmer")
66
+ const RETRY_MS = 10_000
67
+
68
+ /** Replays allowlisted model requests at cache-refresh deadlines without generating model output. */
69
+ export class ReplayWarmer {
70
+ private runtime?: Runtime
71
+ private controlTimer?: ReturnType<typeof setInterval>
72
+ private enabled = false
73
+
74
+ constructor(
75
+ private readonly client: any,
76
+ private readonly config: KeepaliveConfig,
77
+ private readonly sink: WarmStateSink,
78
+ private readonly logger: Logger,
79
+ private readonly directory: string,
80
+ private readonly now: () => number = Date.now,
81
+ ) {}
82
+
83
+ start(): void {
84
+ if (this.runtime) return
85
+ const runtime = getRuntime(this.now)
86
+ this.enabled = this.config.enabled
87
+ runtime.instances.set(this, {
88
+ enabled: this.enabled,
89
+ effective: new Map(),
90
+ directory: this.directory,
91
+ controls: new Map(
92
+ Object.entries(readSessionControls(this.directory, this.now()).sessions).map(
93
+ ([id, value]) => [id, value.enabled],
94
+ ),
95
+ ),
96
+ hosts: new Set(this.config.hosts.map((host) => host.toLowerCase())),
97
+ config: this.config,
98
+ sink: this.sink,
99
+ })
100
+ runtime.maxStoredBytes = Math.min(runtime.maxStoredBytes, this.config.maxStoredBytes)
101
+ for (const host of this.config.hosts) runtime.hosts.add(host.toLowerCase())
102
+ runtime.instancesSet.add(this)
103
+ this.runtime = runtime
104
+ if (!runtime.pruneTimer) {
105
+ runtime.pruneTimer = setInterval(() => pruneOrphanSessions(runtime), 60_000)
106
+ }
107
+ if (runtime.instancesSet.size === 1) {
108
+ globalThis.fetch = ((input: string | URL | Request, init?: RequestInit) => {
109
+ let bodySource: Promise<string> | undefined
110
+ let rejection: string | undefined
111
+ let shouldRecord = false
112
+ try {
113
+ rejection = cheapEligibilityRejection(runtime, input, init)
114
+ if (rejection) {
115
+ if (isCopilotRequest(input))
116
+ runtime.instancesSet
117
+ .values()
118
+ .next()
119
+ .value?.debugRecordRejected(rejection)
120
+ } else if (!rejection) {
121
+ shouldRecord = true
122
+ if (
123
+ isReplayRequest(input, init) &&
124
+ input instanceof Request &&
125
+ init?.body === undefined
126
+ ) {
127
+ // Clone synchronously: the transport may consume or lock the original Request.
128
+ bodySource = input.clone().text()
129
+ // Keep early-disposal paths safe while preserving rejection for recorder handling.
130
+ void bodySource.catch(() => {})
131
+ }
132
+ }
133
+ } catch {
134
+ // Observation must never change whether or how the real fetch is forwarded.
135
+ }
136
+ const result = runtime.originalFetch(input, init)
137
+ if (shouldRecord) {
138
+ queueMicrotask(() => {
139
+ void recordRequest(runtime, input, init, bodySource).catch(() => {})
140
+ })
141
+ }
142
+ return result
143
+ }) as typeof fetch
144
+ }
145
+ this.controlTimer = setInterval(() => this.syncControl(), 1000)
146
+ }
147
+
148
+ dispose(): void {
149
+ if (this.controlTimer) clearInterval(this.controlTimer)
150
+ this.controlTimer = undefined
151
+ const runtime = this.runtime
152
+ this.runtime = undefined
153
+ if (!runtime) return
154
+ runtime.instancesSet.delete(this)
155
+ runtime.instances.delete(this)
156
+ for (const [id, owner] of runtime.owners) {
157
+ if (owner !== this) continue
158
+ const session = runtime.sessions.get(id)
159
+ if (session) {
160
+ this.clearTimerFrom(session)
161
+ session.controller?.abort()
162
+ runtime.storedBytes = Math.max(
163
+ 0,
164
+ runtime.storedBytes - bodyBytes(session.request.body),
165
+ )
166
+ session.request.body = ""
167
+ }
168
+ runtime.owners.delete(id)
169
+ runtime.sessions.delete(id)
170
+ }
171
+ if (runtime.instancesSet.size === 0) {
172
+ if (runtime.pruneTimer) clearInterval(runtime.pruneTimer)
173
+ globalThis.fetch = runtime.originalFetch
174
+ for (const [id, session] of runtime.sessions) {
175
+ this.clearTimerFrom(session)
176
+ session.controller?.abort()
177
+ runtime.sessions.delete(id)
178
+ }
179
+ delete (globalThis as typeof globalThis & { [STATE_KEY]?: Runtime })[STATE_KEY]
180
+ }
181
+ }
182
+
183
+ onEvent(event: any): void {
184
+ const runtime = this.runtime
185
+ if (!runtime) return
186
+ const props = event?.properties ?? {}
187
+ const id = props.sessionID ?? props.info?.id ?? props.part?.sessionID
188
+ const info = props.info ?? props.session
189
+ if (event?.type === "session.created" && typeof info?.id === "string" && info.parentID)
190
+ runtime.childSessions.add(info.id)
191
+ if (event?.type === "session.deleted" && typeof id === "string")
192
+ runtime.childSessions.delete(id)
193
+ if (typeof id !== "string") return
194
+ if (event?.type !== "session.deleted" && !this.sessionEnabled(id)) {
195
+ const disabled = runtime.sessions.get(id)
196
+ if (disabled && runtime.owners.get(id) === this) {
197
+ this.disableSession(runtime, id, disabled)
198
+ } else if (
199
+ !disabled &&
200
+ this.config.enabled &&
201
+ this.runtime?.instances.get(this)?.controls.get(id) === false
202
+ ) {
203
+ runtime.owners.set(id, this)
204
+ this.sink.upsert({
205
+ sessionID: id,
206
+ updatedAt: this.now(),
207
+ enabled: false,
208
+ model: "",
209
+ api: "messages",
210
+ state: "off",
211
+ gapId: 0,
212
+ gapReplays: 0,
213
+ cap: this.cap,
214
+ nextReplayAt: null,
215
+ sessionReplays: 0,
216
+ avoidedTokens: 0,
217
+ resumeHits: 0,
218
+ resumeCount: 0,
219
+ })
220
+ }
221
+ return
222
+ }
223
+ if (!runtime.owners.has(id)) runtime.owners.set(id, this)
224
+ if (runtime.owners.get(id) !== this) return
225
+ const session = runtime.sessions.get(id)
226
+ if (!session && event.type === "session.deleted") {
227
+ this.sink.remove(id)
228
+ return
229
+ }
230
+ if (!session) return
231
+ session.touchedAt = this.now()
232
+ const type = event.type
233
+ if (type === "session.deleted") {
234
+ this.clearTimer(session)
235
+ session.controller?.abort()
236
+ runtime.sessions.delete(id)
237
+ runtime.storedBytes = Math.max(0, runtime.storedBytes - bodyBytes(session.request.body))
238
+ session.request.body = ""
239
+ this.sink.remove(id)
240
+ return
241
+ }
242
+ if (type === "session.status" || type === "session.idle") {
243
+ const status = props.status?.type ?? "idle"
244
+ if (status === "idle") {
245
+ session.active = false
246
+ session.busy = false
247
+ session.runningTools.clear()
248
+ this.updateState(session, "idle")
249
+ this.schedule(id, session, session.request.sentAt + session.request.intervalMs)
250
+ } else if (status === "busy" || status === "running") {
251
+ session.active = session.runningTools.size === 0
252
+ session.busy = session.runningTools.size > 0
253
+ if (session.active) this.clearTimer(session)
254
+ this.updateState(session, session.active ? "active" : "busy")
255
+ }
256
+ return
257
+ }
258
+ if (type === "message.updated") {
259
+ const info = props.info ?? {}
260
+ const interactionType = String(info.interactionType ?? props.interactionType ?? "")
261
+ if (/agent-session-(name|summary)-generation/i.test(interactionType)) return
262
+ if (
263
+ info.role === "assistant" &&
264
+ ((typeof info.providerID === "string" &&
265
+ !info.providerID
266
+ .toLowerCase()
267
+ .replace(/[^a-z]/g, "")
268
+ .includes("copilot")) ||
269
+ (typeof info.modelID === "string" &&
270
+ !info.modelID.toLowerCase().includes(session.request.model.toLowerCase()) &&
271
+ !session.request.model.toLowerCase().includes(info.modelID.toLowerCase())))
272
+ ) {
273
+ this.invalidateTarget(runtime, session, this.now())
274
+ return
275
+ }
276
+ if (info.role === "assistant" && info.time?.completed === undefined) {
277
+ session.active = true
278
+ this.clearTimer(session)
279
+ this.updateState(session, "active")
280
+ }
281
+ return
282
+ }
283
+ if (type !== "message.part.updated") return
284
+ const part = props.part
285
+ if (part?.type === "step-start") {
286
+ session.active = true
287
+ this.clearTimer(session)
288
+ this.updateState(session, "active")
289
+ } else if (part?.type === "step-finish") {
290
+ session.active = false
291
+ const cache = part.tokens?.cache ?? {}
292
+ const read = Number(cache.read ?? 0)
293
+ const prompt = Number(part.tokens?.input ?? 0) + read + Number(cache.write ?? 0)
294
+ const completedGap = session.pendingResume
295
+ if (completedGap) {
296
+ const hit = read >= 0.9 * completedGap.promptPrev
297
+ const avoidedTokens = hit ? completedGap.promptPrev : 0
298
+ const resume: ResumeResult = {
299
+ at: this.now(),
300
+ read,
301
+ promptPrev: completedGap.promptPrev,
302
+ hit,
303
+ replays: completedGap.replays,
304
+ avoidedTokens,
305
+ replayReadTokens: completedGap.replayReadTokens,
306
+ }
307
+ session.entry.resume = resume
308
+ session.entry.avoidedTokens += avoidedTokens
309
+ session.entry.resumeCount++
310
+ if (hit) session.entry.resumeHits++
311
+ const totals = this.totals
312
+ totals.avoidedTokens += avoidedTokens
313
+ totals.replayReadTokens += completedGap.replayReadTokens
314
+ if (hit) totals.resumeHits++
315
+ else totals.resumeMisses++
316
+ this.sink.setTotals(totals)
317
+ this.logger.info(
318
+ `resume session=${id} gap=${session.entry.gapId} hit=${hit} read=${read} prev=${completedGap.promptPrev} replays=${completedGap.replays} avoided=${avoidedTokens} replayRead=${completedGap.replayReadTokens}`,
319
+ )
320
+ session.pendingResume = undefined
321
+ }
322
+ session.previousPrompt = prompt
323
+ if (session.busy) this.updateState(session, "busy")
324
+ else {
325
+ this.updateState(session, "idle")
326
+ this.schedule(id, session, session.request.sentAt + session.request.intervalMs)
327
+ }
328
+ } else if (part?.type === "tool" && typeof part.callID === "string") {
329
+ if (part.state?.status === "running") session.runningTools.add(part.callID)
330
+ else if (["completed", "error"].includes(part.state?.status))
331
+ session.runningTools.delete(part.callID)
332
+ session.busy = session.runningTools.size > 0
333
+ if (session.busy) {
334
+ session.active = false
335
+ this.updateState(session, "busy")
336
+ this.schedule(id, session, session.request.sentAt + session.request.intervalMs)
337
+ } else if (!session.active) {
338
+ this.updateState(session, "idle")
339
+ this.schedule(id, session, session.request.sentAt + session.request.intervalMs)
340
+ }
341
+ }
342
+ }
343
+
344
+ private totals = {
345
+ replays: 0,
346
+ avoidedTokens: 0,
347
+ replayReadTokens: 0,
348
+ resumeHits: 0,
349
+ resumeMisses: 0,
350
+ }
351
+
352
+ /** @internal Called asynchronously by the process-wide recorder. */
353
+ debugRecordRejected(reason: string): void {
354
+ this.logger.dbg(`record rejected reason=${reason}`)
355
+ }
356
+
357
+ /** @internal Called asynchronously by the process-wide recorder. */
358
+ async recordRequest(
359
+ runtime: Runtime,
360
+ input: string | URL | Request,
361
+ init?: RequestInit,
362
+ bodySource?: Promise<string>,
363
+ selectedBody?: string,
364
+ selectedParsed?: any,
365
+ ): Promise<void> {
366
+ if (!this.runtime) return
367
+ const request = input instanceof Request ? input : undefined
368
+ const body = init?.body
369
+ const method = (init?.method ?? request?.method ?? "GET").toUpperCase()
370
+ const url =
371
+ input instanceof URL ? input.href : typeof input === "string" ? input : input.url
372
+ let u: URL
373
+ try {
374
+ u = new URL(url)
375
+ } catch {
376
+ this.debugRecordRejected("invalid-url")
377
+ return
378
+ }
379
+ if (
380
+ !this.config.hosts.some((host) => {
381
+ const allowed = host.toLowerCase()
382
+ const hostname = u.hostname.toLowerCase()
383
+ return hostname === allowed || hostname.endsWith(`.${allowed}`)
384
+ })
385
+ ) {
386
+ if (isCopilotHost(u.hostname)) this.debugRecordRejected("host-not-allowed")
387
+ return
388
+ }
389
+ const headers = new Headers(request?.headers)
390
+ if (init?.headers) new Headers(init.headers).forEach((v, k) => headers.set(k, v))
391
+ if (
392
+ /agent-session-(name|summary)-generation/i.test(headers.get("X-Interaction-Type") ?? "")
393
+ ) {
394
+ if (isCopilotHost(u.hostname)) this.debugRecordRejected("utility-interaction")
395
+ return
396
+ }
397
+ const sessionID =
398
+ headers.get("X-Session-Id") ??
399
+ headers.get("x-session-affinity") ??
400
+ headers.get("X-Interaction-Id")
401
+ if (!sessionID) {
402
+ if (isCopilotHost(u.hostname)) this.debugRecordRejected("missing-session-id")
403
+ return
404
+ }
405
+ const owner = runtime.owners.get(sessionID)
406
+ if (owner && owner !== this) return
407
+ if (
408
+ !runtime.instances.has(this) ||
409
+ !this.enabled ||
410
+ this.sessionEnabled(sessionID) === false
411
+ )
412
+ return
413
+ let stringBody: string | undefined
414
+ try {
415
+ stringBody =
416
+ selectedBody !== undefined
417
+ ? selectedBody
418
+ : typeof body === "string"
419
+ ? body
420
+ : body === undefined && request && isReplayRequest(input, init)
421
+ ? await (bodySource ?? request.clone().text())
422
+ : undefined
423
+ } catch {
424
+ stringBody = undefined
425
+ }
426
+ // Body reading may yield after this instance was disabled, disposed, or lost ownership.
427
+ const instance = runtime.instances.get(this)
428
+ if (instance) {
429
+ instance.controls = new Map(
430
+ Object.entries(readSessionControls(this.directory, this.now()).sessions).map(
431
+ ([id, value]) => [id, value.enabled],
432
+ ),
433
+ )
434
+ }
435
+ if (
436
+ !this.runtime ||
437
+ !instance ||
438
+ (runtime.owners.has(sessionID) && runtime.owners.get(sessionID) !== this) ||
439
+ !this.enabled ||
440
+ this.sessionEnabled(sessionID) === false
441
+ )
442
+ return
443
+ const session = runtime.sessions.get(sessionID)
444
+ const sentAt = this.now()
445
+ if (session) session.touchedAt = sentAt
446
+ if (session) {
447
+ if (session.entry.gapReplays > 0 && session.previousPrompt !== undefined) {
448
+ session.pendingResume = {
449
+ promptPrev: session.previousPrompt,
450
+ replays: session.entry.gapReplays,
451
+ replayReadTokens: session.replayReadTokens,
452
+ }
453
+ }
454
+ this.startGap(session, sentAt)
455
+ session.entry.gapId++
456
+ }
457
+ if (method !== "POST" || !apiFor(u.pathname)) {
458
+ if (session) this.invalidateTarget(runtime, session, sentAt)
459
+ return
460
+ }
461
+ if (stringBody === undefined) {
462
+ if (isCopilotHost(u.hostname)) this.debugRecordRejected("unsupported-body-type")
463
+ if (session) {
464
+ this.invalidateTarget(runtime, session, sentAt)
465
+ }
466
+ return
467
+ }
468
+ let parsed: any
469
+ try {
470
+ if (stringBody === undefined) return
471
+ parsed = selectedBody !== undefined ? selectedParsed : JSON.parse(stringBody)
472
+ } catch {
473
+ if (isCopilotHost(u.hostname)) this.debugRecordRejected("invalid-json-body")
474
+ if (session) {
475
+ this.invalidateTarget(runtime, session, sentAt)
476
+ }
477
+ return
478
+ }
479
+ this.recordEligible(runtime, sessionID, url, method, headers, stringBody, parsed, u)
480
+ }
481
+
482
+ private recordEligible(
483
+ runtime: Runtime,
484
+ sessionID: string,
485
+ url: string,
486
+ method: string,
487
+ headers: Headers,
488
+ stringBody: string,
489
+ parsed: any,
490
+ u: URL,
491
+ ): void {
492
+ const child = runtime.childSessions.has(sessionID)
493
+ let session = runtime.sessions.get(sessionID)
494
+ const sentAt = this.now()
495
+ if (session) session.touchedAt = sentAt
496
+ const eligible =
497
+ this.sessionEnabled(sessionID) &&
498
+ typeof parsed?.model === "string" &&
499
+ (!child || this.config.includeChildSessions)
500
+ const model = typeof parsed?.model === "string" ? parsed.model.toLowerCase() : ""
501
+ const match = Object.entries(this.config.intervals).find(([key]) =>
502
+ model.includes(key.toLowerCase()),
503
+ )
504
+ const api = apiFor(u.pathname)
505
+ if (!eligible || !match || !api) {
506
+ if (isCopilotHost(u.hostname))
507
+ this.debugRecordRejected(
508
+ !this.sessionEnabled(sessionID)
509
+ ? "session-disabled"
510
+ : child && !this.config.includeChildSessions
511
+ ? "child-session"
512
+ : typeof parsed?.model !== "string"
513
+ ? "missing-model"
514
+ : !match
515
+ ? "model-not-configured"
516
+ : "unsupported-path",
517
+ )
518
+ if (session) {
519
+ this.invalidateTarget(runtime, session, sentAt)
520
+ }
521
+ return
522
+ }
523
+ runtime.instances.get(this)!.effective.set(sessionID, true)
524
+ if (!runtime.loggedApis.has(api)) {
525
+ runtime.loggedApis.add(api)
526
+ this.logger.info(`record api=${api} host=${u.hostname}${u.pathname}`)
527
+ }
528
+ if (session) {
529
+ session.entry.gapReplays = 0
530
+ session.entry.resume = undefined
531
+ session.entry.lastReplay = undefined
532
+ session.entry.stopReason = undefined
533
+ session.entry.stopStatus = undefined
534
+ session.entry.state = "active"
535
+ session.entry.enabled = true
536
+ session.entry.model = parsed.model
537
+ session.entry.api = api
538
+ session.entry.nextReplayAt = null
539
+ session.entry.updatedAt = sentAt
540
+ session.active = true
541
+ session.busy = false
542
+ session.retries = 0
543
+ }
544
+ runtime.owners.set(sessionID, this)
545
+ const oldSession = runtime.sessions.get(sessionID)
546
+ if (oldSession) {
547
+ runtime.storedBytes = Math.max(
548
+ 0,
549
+ runtime.storedBytes - bodyBytes(oldSession.request.body),
550
+ )
551
+ oldSession.request = {
552
+ url,
553
+ method,
554
+ headers,
555
+ body: stringBody,
556
+ model: parsed.model,
557
+ api,
558
+ sentAt,
559
+ intervalMs: match[1],
560
+ }
561
+ runtime.storedBytes += bodyBytes(stringBody)
562
+ oldSession.child = child
563
+ this.publish(oldSession)
564
+ this.enforceStoredBudget(runtime)
565
+ return
566
+ }
567
+ session = {
568
+ request: {
569
+ url,
570
+ method,
571
+ headers,
572
+ body: stringBody,
573
+ model: parsed.model,
574
+ api,
575
+ sentAt,
576
+ intervalMs: match[1],
577
+ },
578
+ entry: {
579
+ sessionID,
580
+ updatedAt: sentAt,
581
+ enabled: true,
582
+ model: parsed.model,
583
+ api,
584
+ state: "active",
585
+ gapId: 1,
586
+ gapReplays: 0,
587
+ cap: this.cap,
588
+ nextReplayAt: null,
589
+ sessionReplays: 0,
590
+ avoidedTokens: 0,
591
+ resumeHits: 0,
592
+ resumeCount: 0,
593
+ },
594
+ runningTools: new Set(),
595
+ active: true,
596
+ busy: false,
597
+ realRequests: 1,
598
+ retries: 0,
599
+ child,
600
+ replayReadTokens: 0,
601
+ touchedAt: sentAt,
602
+ }
603
+ runtime.sessions.set(sessionID, session)
604
+ runtime.owners.set(sessionID, this)
605
+ runtime.storedBytes += bodyBytes(stringBody)
606
+ this.publish(session)
607
+ this.enforceStoredBudget(runtime)
608
+ }
609
+
610
+ private startGap(session: Session, sentAt: number, model?: string): void {
611
+ const runtime = this.runtime!
612
+ session.controller?.abort()
613
+ session.controller = undefined
614
+ this.clearTimer(session)
615
+ runtime.storedBytes = Math.max(0, runtime.storedBytes - bodyBytes(session.request.body))
616
+ session.request.body = ""
617
+ session.entry.gapReplays = 0
618
+ session.entry.resume = undefined
619
+ session.entry.lastReplay = undefined
620
+ session.entry.stopReason = undefined
621
+ session.entry.stopStatus = undefined
622
+ session.entry.nextReplayAt = null
623
+ session.replayReadTokens = 0
624
+ session.entry.updatedAt = sentAt
625
+ session.touchedAt = sentAt
626
+ session.entry.model = model ?? session.entry.model
627
+ session.active = false
628
+ session.busy = false
629
+ session.retries = 0
630
+ }
631
+
632
+ private invalidateTarget(runtime: Runtime, session: Session, at: number): void {
633
+ this.clearTimer(session)
634
+ session.controller?.abort()
635
+ session.controller = undefined
636
+ runtime.storedBytes = Math.max(0, runtime.storedBytes - bodyBytes(session.request.body))
637
+ session.request.body = ""
638
+ session.entry.state = this.sessionEnabled(session.entry.sessionID) ? "idle" : "off"
639
+ session.entry.nextReplayAt = null
640
+ session.entry.updatedAt = at
641
+ this.publish(session)
642
+ }
643
+
644
+ private enforceStoredBudget(runtime: Runtime): void {
645
+ while (runtime.storedBytes > runtime.maxStoredBytes) {
646
+ const oldestID = [...runtime.sessions.keys()]
647
+ .filter((id) => runtime.sessions.get(id)!.request.body.length > 0)
648
+ .sort(
649
+ (a, b) =>
650
+ runtime.sessions.get(a)!.touchedAt - runtime.sessions.get(b)!.touchedAt,
651
+ )[0]
652
+ if (!oldestID) break
653
+ const session = runtime.sessions.get(oldestID)!
654
+ this.clearTimer(session)
655
+ session.controller?.abort()
656
+ runtime.storedBytes -= bodyBytes(session.request.body)
657
+ session.request.body = ""
658
+ runtime.owners.get(oldestID)?.logger.info(`evict session=${oldestID}`)
659
+ session.entry.nextReplayAt = null
660
+ session.entry.state = "idle"
661
+ session.entry.updatedAt = this.now()
662
+ runtime.owners.get(oldestID)?.publish(session)
663
+ }
664
+ }
665
+
666
+ private get cap(): number {
667
+ return this.config.maxReplaysPerGap === "auto"
668
+ ? Math.floor(
669
+ (this.config.missFactor - this.config.cacheReadFactor) /
670
+ this.config.cacheReadFactor,
671
+ )
672
+ : this.config.maxReplaysPerGap
673
+ }
674
+
675
+ private schedule(id: string, session: Session, at: number): void {
676
+ this.clearTimer(session)
677
+ if (
678
+ this.runtime?.owners.get(id) !== this ||
679
+ !session.entry.enabled ||
680
+ session.active ||
681
+ session.entry.state === "stopped" ||
682
+ !session.request.body
683
+ )
684
+ return
685
+ session.entry.nextReplayAt = at
686
+ this.publish(session)
687
+ this.logger.dbg(
688
+ `schedule session=${id} state=${session.busy ? "busy" : "idle"} next=${new Date(at).toISOString()}`,
689
+ )
690
+ session.timer = setTimeout(
691
+ () => void this.replay(id, session),
692
+ Math.max(0, at - this.now()),
693
+ )
694
+ }
695
+
696
+ private async replay(id: string, session: Session): Promise<void> {
697
+ const runtime = this.runtime
698
+ if (
699
+ !runtime ||
700
+ runtime.owners.get(id) !== this ||
701
+ !session.entry.enabled ||
702
+ runtime.sessions.get(id) !== session ||
703
+ session.active
704
+ )
705
+ return
706
+ const sentAt = this.now()
707
+ session.request.sentAt = sentAt
708
+ session.touchedAt = sentAt
709
+ const controller = new AbortController()
710
+ session.controller = controller
711
+ session.entry.nextReplayAt = null
712
+ this.updateState(session, session.busy ? "busy" : "idle")
713
+ let timeout: ReturnType<typeof setTimeout> | undefined
714
+ let latencyMs = 0
715
+ let status = 0
716
+ try {
717
+ const timeoutPromise = new Promise<never>((_, reject) => {
718
+ timeout = setTimeout(() => {
719
+ controller.abort()
720
+ reject(new Error("timeout"))
721
+ }, this.config.replayTimeoutMs)
722
+ })
723
+ const response = await Promise.race([
724
+ runtime.originalFetch(session.request.url, {
725
+ method: session.request.method,
726
+ headers: new Headers(session.request.headers),
727
+ body: session.request.body,
728
+ signal: controller.signal,
729
+ }),
730
+ timeoutPromise,
731
+ ])
732
+ status = response.status
733
+ if ([400, 401, 403].includes(status)) {
734
+ session.entry.lastReplay = { at: sentAt, status, latencyMs: this.now() - sentAt }
735
+ this.logger.info(replayLogLine(session, session.entry.lastReplay, "http-status"))
736
+ this.stopSession(id, session, "rejected-4xx", status)
737
+ return
738
+ }
739
+ if (!response.ok) {
740
+ session.entry.lastReplay = { at: sentAt, status, latencyMs: this.now() - sentAt }
741
+ this.logger.info(replayLogLine(session, session.entry.lastReplay, "http-status"))
742
+ this.retryOrLapse(id, session, sentAt, undefined, status)
743
+ return
744
+ }
745
+ const result = await Promise.race([
746
+ readAbortPoint(response, session.request.api, controller),
747
+ timeoutPromise,
748
+ ])
749
+ latencyMs = this.now() - sentAt
750
+ if (session.controller !== controller || runtime.sessions.get(id) !== session) return
751
+ const { abortEvent, ...usage } = result
752
+ const last: ReplayResult = { at: sentAt, status, latencyMs, ...usage }
753
+ session.entry.lastReplay = last
754
+ session.entry.gapReplays++
755
+ session.entry.sessionReplays++
756
+ session.replayReadTokens += result.cacheRead ?? session.previousPrompt ?? 0
757
+ this.totals.replays++
758
+ this.sink.setTotals(this.totals)
759
+ session.retries = 0
760
+ this.logger.info(replayLogLine(session, last, abortEvent))
761
+ if (
762
+ session.request.api === "messages" &&
763
+ result.cacheRead === 0 &&
764
+ (result.cacheWrite ?? 0) > 0
765
+ ) {
766
+ session.entry.gapReplays--
767
+ this.stopSession(id, session, "lapsed")
768
+ return
769
+ }
770
+ if (session.entry.gapReplays >= session.entry.cap) {
771
+ this.stopSession(id, session, "cap")
772
+ return
773
+ }
774
+ this.schedule(id, session, sentAt + session.request.intervalMs)
775
+ } catch (error) {
776
+ if (runtime.sessions.get(id) !== session || session.controller !== controller) return
777
+ const timeoutHit = this.now() - sentAt >= this.config.replayTimeoutMs
778
+ session.entry.lastReplay = { at: sentAt, status: 0, latencyMs: this.now() - sentAt }
779
+ this.logger.info(
780
+ replayLogLine(
781
+ session,
782
+ session.entry.lastReplay,
783
+ timeoutHit ? "timeout" : "network",
784
+ ),
785
+ )
786
+ this.retryOrLapse(id, session, sentAt, timeoutHit ? "timeout" : "network")
787
+ } finally {
788
+ if (timeout) clearTimeout(timeout)
789
+ controller.abort()
790
+ if (session.controller === controller) session.controller = undefined
791
+ }
792
+ }
793
+
794
+ private retryOrLapse(
795
+ id: string,
796
+ session: Session,
797
+ sentAt: number,
798
+ reason?: string,
799
+ status = 0,
800
+ ): void {
801
+ session.entry.lastReplay = { at: sentAt, status, latencyMs: this.now() - sentAt }
802
+ if (session.retries++ === 0) this.schedule(id, session, sentAt + RETRY_MS)
803
+ else this.stopSession(id, session, "lapsed")
804
+ if (reason) this.logger.warn(`replay ${reason} session=${id}`)
805
+ }
806
+
807
+ private stopSession(
808
+ id: string,
809
+ session: Session,
810
+ reason: "cap" | "lapsed" | "rejected-4xx",
811
+ status?: number,
812
+ ): void {
813
+ if (session.stopLoggedGap !== session.entry.gapId) {
814
+ session.stopLoggedGap = session.entry.gapId
815
+ this.logger.info(
816
+ `stop session=${id} reason=${reason}${status === undefined ? "" : ` status=${status}`}`,
817
+ )
818
+ }
819
+ this.clearTimer(session)
820
+ session.controller?.abort()
821
+ session.entry.state = "stopped"
822
+ session.entry.stopReason = reason
823
+ session.entry.stopStatus = status
824
+ session.entry.nextReplayAt = null
825
+ session.entry.updatedAt = this.now()
826
+ if (this.runtime)
827
+ this.runtime.storedBytes = Math.max(
828
+ 0,
829
+ this.runtime.storedBytes - bodyBytes(session.request.body),
830
+ )
831
+ session.request.body = ""
832
+ session.request.headers = new Headers()
833
+ this.publish(session)
834
+ }
835
+
836
+ private updateState(session: Session, state: WarmState): void {
837
+ const enabled = this.sessionEnabled(session.entry.sessionID)
838
+ session.entry.state = enabled ? state : "off"
839
+ session.entry.enabled = enabled
840
+ session.entry.updatedAt = this.now()
841
+ session.entry.cap = this.cap
842
+ this.publish(session)
843
+ }
844
+
845
+ private publish(session: Session): void {
846
+ this.sink.upsert({ ...session.entry })
847
+ }
848
+ private clearTimer(session: Session): void {
849
+ if (!session) return
850
+ if (session.timer) clearTimeout(session.timer)
851
+ session.timer = undefined
852
+ session.entry.nextReplayAt = null
853
+ }
854
+
855
+ private clearTimerFrom(session: Session): void {
856
+ if (session.timer) clearTimeout(session.timer)
857
+ session.timer = undefined
858
+ session.entry.nextReplayAt = null
859
+ }
860
+ private sessionEnabled(sessionID: string): boolean {
861
+ const instance = this.runtime?.instances.get(this)
862
+ return this.config.enabled && instance?.controls.get(sessionID) !== false
863
+ }
864
+ private disableSession(runtime: Runtime, id: string, session: Session): void {
865
+ const instance = runtime.instances.get(this)!
866
+ instance.effective.set(id, false)
867
+ this.clearTimer(session)
868
+ session.controller?.abort()
869
+ runtime.storedBytes = Math.max(0, runtime.storedBytes - bodyBytes(session.request.body))
870
+ session.request.body = ""
871
+ session.entry.stopReason = "disabled"
872
+ session.entry.state = "off"
873
+ session.entry.enabled = false
874
+ session.entry.nextReplayAt = null
875
+ session.entry.updatedAt = this.now()
876
+ if (session.stopLoggedGap !== session.entry.gapId) {
877
+ session.stopLoggedGap = session.entry.gapId
878
+ this.logger.info(`stop session=${id} reason=disabled`)
879
+ }
880
+ this.publish(session)
881
+ }
882
+ private syncControl(): void {
883
+ const runtime = this.runtime
884
+ if (!runtime) return
885
+ const instance = runtime.instances.get(this)!
886
+ const controls = readSessionControls(this.directory, this.now()).sessions
887
+ instance.controls = new Map(
888
+ Object.entries(controls).map(([id, value]) => [id, value.enabled]),
889
+ )
890
+ for (const [id, session] of runtime.sessions) {
891
+ if (runtime.owners.get(id) !== this) continue
892
+ const enabled = this.sessionEnabled(id)
893
+ const previous = instance.effective.get(id) ?? session.entry.enabled
894
+ instance.effective.set(id, enabled)
895
+ if (previous === enabled) continue
896
+ if (!enabled) {
897
+ this.disableSession(runtime, id, session)
898
+ } else {
899
+ session.entry.stopReason = undefined
900
+ session.entry.state = session.active ? "active" : session.busy ? "busy" : "idle"
901
+ session.entry.enabled = true
902
+ session.entry.updatedAt = this.now()
903
+ this.publish(session)
904
+ }
905
+ }
906
+ }
907
+ }
908
+
909
+ function getRuntime(now: () => number): Runtime {
910
+ const target = globalThis as typeof globalThis & { [STATE_KEY]?: Runtime }
911
+ if (target[STATE_KEY]) return target[STATE_KEY]
912
+ const runtime: Runtime = {
913
+ originalFetch: globalThis.fetch,
914
+ sessions: new Map(),
915
+ owners: new Map(),
916
+ instancesSet: new Set(),
917
+ childSessions: new Set(),
918
+ loggedApis: new Set(),
919
+ now,
920
+ maxStoredBytes: Number.MAX_SAFE_INTEGER,
921
+ storedBytes: 0,
922
+ hosts: new Set(),
923
+ instances: new Map(),
924
+ }
925
+ target[STATE_KEY] = runtime
926
+ return runtime
927
+ }
928
+
929
+ async function recordRequest(
930
+ runtime: Runtime,
931
+ input: string | URL | Request,
932
+ init?: RequestInit,
933
+ bodySource?: Promise<string>,
934
+ ): Promise<void> {
935
+ const headers = new Headers(input instanceof Request ? input.headers : init?.headers)
936
+ if (init?.headers && input instanceof Request)
937
+ new Headers(init.headers).forEach((value, key) => headers.set(key, value))
938
+ const id =
939
+ headers.get("X-Session-Id") ??
940
+ headers.get("x-session-affinity") ??
941
+ headers.get("X-Interaction-Id")
942
+ if (!id) return
943
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase()
944
+ const rawUrl = input instanceof URL ? input.href : typeof input === "string" ? input : input.url
945
+ let url: URL
946
+ try {
947
+ url = new URL(rawUrl)
948
+ } catch {
949
+ return
950
+ }
951
+ const mapped = runtime.owners.get(id)
952
+ let owner: ReplayWarmer | undefined
953
+ if (mapped) {
954
+ const state = runtime.instances.get(mapped)
955
+ if (!state?.enabled || state.controls.get(id) === false) return
956
+ owner = mapped
957
+ } else {
958
+ const hostname = url.hostname.toLowerCase()
959
+ const eligiblePath = method === "POST" && !!apiFor(url.pathname)
960
+ const candidates = [...runtime.instances].filter(
961
+ ([, state]) =>
962
+ state.enabled &&
963
+ state.controls.get(id) !== false &&
964
+ (eligiblePath || runtime.sessions.has(id)) &&
965
+ [...state.hosts].some(
966
+ (allowed) => hostname === allowed || hostname.endsWith(`.${allowed}`),
967
+ ),
968
+ )
969
+ if (!candidates.length) return
970
+
971
+ let stringBody: string | undefined
972
+ try {
973
+ stringBody =
974
+ typeof init?.body === "string"
975
+ ? init.body
976
+ : init?.body === undefined && input instanceof Request
977
+ ? await (bodySource ?? input.clone().text())
978
+ : undefined
979
+ } catch {
980
+ stringBody = undefined
981
+ }
982
+ let parsed: any
983
+ try {
984
+ if (stringBody !== undefined) parsed = JSON.parse(stringBody)
985
+ } catch {
986
+ parsed = undefined
987
+ }
988
+ const concurrentOwner = runtime.owners.get(id)
989
+ if (concurrentOwner) {
990
+ const state = runtime.instances.get(concurrentOwner)
991
+ if (!state?.enabled || state.controls.get(id) === false) return
992
+ await concurrentOwner.recordRequest(runtime, input, init, undefined, stringBody, parsed)
993
+ return
994
+ }
995
+ const child = runtime.childSessions.has(id)
996
+ const liveCandidates = candidates.filter(
997
+ ([instance, state]) =>
998
+ runtime.instances.has(instance) &&
999
+ state.enabled &&
1000
+ state.controls.get(id) !== false,
1001
+ )
1002
+ if (!liveCandidates.length) return
1003
+ const fullyEligible = liveCandidates.filter(([, state]) => {
1004
+ if (method !== "POST" || !apiFor(url.pathname)) return false
1005
+ if (child && !state.config.includeChildSessions) return false
1006
+ const model = typeof parsed?.model === "string" ? parsed.model.toLowerCase() : ""
1007
+ return (
1008
+ Object.entries(state.config.intervals).some(([key]) =>
1009
+ model.includes(key.toLowerCase()),
1010
+ ) &&
1011
+ (!child || state.config.includeChildSessions)
1012
+ )
1013
+ })
1014
+ if (!fullyEligible.length) return
1015
+ owner = fullyEligible[0][0]
1016
+ // Reserve an eligible unknown session so concurrent observations cannot cross-route it.
1017
+ runtime.owners.set(id, owner)
1018
+ await owner.recordRequest(runtime, input, init, undefined, stringBody, parsed)
1019
+ return
1020
+ }
1021
+
1022
+ if (!runtime.instances.has(owner) || runtime.owners.get(id) !== owner) return
1023
+ await owner.recordRequest(runtime, input, init, bodySource)
1024
+ }
1025
+
1026
+ function bodyBytes(body: string): number {
1027
+ return new TextEncoder().encode(body).byteLength
1028
+ }
1029
+
1030
+ function pruneOrphanSessions(runtime: Runtime): void {
1031
+ const cutoff = runtime.now() - 60 * 60_000
1032
+ for (const [id, session] of runtime.sessions) {
1033
+ if (session.request.body || session.touchedAt >= cutoff) continue
1034
+ session.timer && clearTimeout(session.timer)
1035
+ session.controller?.abort()
1036
+ const owner = runtime.owners.get(id)
1037
+ if (owner) runtime.instances.get(owner)?.sink.remove(id)
1038
+ runtime.owners.delete(id)
1039
+ runtime.sessions.delete(id)
1040
+ }
1041
+ }
1042
+
1043
+ function cheapEligibilityRejection(
1044
+ runtime: Runtime,
1045
+ input: string | URL | Request,
1046
+ init?: RequestInit,
1047
+ ): string | undefined {
1048
+ const request = input instanceof Request ? input : undefined
1049
+ const method = (init?.method ?? request?.method ?? "GET").toUpperCase()
1050
+ const rawUrl = input instanceof URL ? input.href : typeof input === "string" ? input : input.url
1051
+ let url: URL
1052
+ try {
1053
+ url = new URL(rawUrl)
1054
+ } catch {
1055
+ return "invalid-url"
1056
+ }
1057
+ const copilotHost = isCopilotHost(url.hostname)
1058
+ const headers = new Headers(request?.headers)
1059
+ if (init?.headers) new Headers(init.headers).forEach((value, key) => headers.set(key, value))
1060
+ const id =
1061
+ headers.get("X-Session-Id") ??
1062
+ headers.get("x-session-affinity") ??
1063
+ headers.get("X-Interaction-Id")
1064
+ if (!id) return "missing-session-id"
1065
+ if (/agent-session-(name|summary)-generation/i.test(headers.get("X-Interaction-Type") ?? ""))
1066
+ return "utility-interaction"
1067
+ const hostname = url.hostname.toLowerCase()
1068
+ const owner = runtime.owners.get(id)
1069
+ const allowedHost = owner
1070
+ ? (() => {
1071
+ const state = runtime.instances.get(owner)
1072
+ return (
1073
+ !!state?.enabled &&
1074
+ state.controls.get(id) !== false &&
1075
+ [...state.hosts].some(
1076
+ (allowed) => hostname === allowed || hostname.endsWith(`.${allowed}`),
1077
+ )
1078
+ )
1079
+ })()
1080
+ : [...runtime.instances.values()].some(
1081
+ (state) =>
1082
+ state.enabled &&
1083
+ state.controls.get(id) !== false &&
1084
+ [...state.hosts].some(
1085
+ (allowed) => hostname === allowed || hostname.endsWith(`.${allowed}`),
1086
+ ),
1087
+ )
1088
+ if (!allowedHost) return "host-not-allowed"
1089
+ if (runtime.owners.has(id) || (method === "POST" && !!apiFor(url.pathname))) return undefined
1090
+ if (!copilotHost) return "ineligible"
1091
+ return method !== "POST" ? "method-not-post" : "unsupported-path"
1092
+ }
1093
+
1094
+ function isReplayRequest(input: string | URL | Request, init?: RequestInit): boolean {
1095
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase()
1096
+ const rawUrl = input instanceof URL ? input.href : typeof input === "string" ? input : input.url
1097
+ try {
1098
+ return method === "POST" && !!apiFor(new URL(rawUrl).pathname)
1099
+ } catch {
1100
+ return false
1101
+ }
1102
+ }
1103
+
1104
+ function isCopilotHost(hostname: string): boolean {
1105
+ const host = hostname.toLowerCase()
1106
+ return (
1107
+ host === "githubcopilot.com" ||
1108
+ host.endsWith(".githubcopilot.com") ||
1109
+ host.startsWith("copilot-api.")
1110
+ )
1111
+ }
1112
+
1113
+ function isCopilotRequest(input: string | URL | Request): boolean {
1114
+ const rawUrl = input instanceof URL ? input.href : typeof input === "string" ? input : input.url
1115
+ try {
1116
+ return isCopilotHost(new URL(rawUrl).hostname)
1117
+ } catch {
1118
+ return false
1119
+ }
1120
+ }
1121
+
1122
+ function apiFor(path: string): WarmApi | undefined {
1123
+ if (/\/v1\/messages\/?$/.test(path)) return "messages"
1124
+ if (/^\/(?:v1\/)?responses\/?$/.test(path)) return "responses"
1125
+ if (/\/chat\/completions\/?$/.test(path)) return "chat"
1126
+ }
1127
+
1128
+ async function readAbortPoint(
1129
+ response: Response,
1130
+ api: WarmApi,
1131
+ controller: AbortController,
1132
+ ): Promise<{ cacheRead?: number; cacheWrite?: number; abortEvent: string }> {
1133
+ const reader = response.body?.getReader()
1134
+ if (!reader) {
1135
+ controller.abort()
1136
+ return { abortEvent: "first-chunk" }
1137
+ }
1138
+ let buffer = ""
1139
+ try {
1140
+ while (true) {
1141
+ const { value, done } = await reader.read()
1142
+ if (done) break
1143
+ if (!value) continue
1144
+ if (api === "chat") {
1145
+ controller.abort()
1146
+ return { abortEvent: "first-chunk" }
1147
+ }
1148
+ buffer += new TextDecoder().decode(value, { stream: true })
1149
+ const events = buffer.split(/\r?\n\r?\n/)
1150
+ buffer = events.pop() ?? ""
1151
+ for (const event of events) {
1152
+ const data = event
1153
+ .split(/\r?\n/)
1154
+ .filter((line) => line.startsWith("data:"))
1155
+ .map((line) => line.slice(5).trim())
1156
+ .join("\n")
1157
+ if (!data || data === "[DONE]") continue
1158
+ let parsed: any
1159
+ try {
1160
+ parsed = JSON.parse(data)
1161
+ } catch {
1162
+ continue
1163
+ }
1164
+ if (
1165
+ api === "responses" &&
1166
+ ["response.created", "response.in_progress"].includes(parsed.type)
1167
+ )
1168
+ continue
1169
+ if (api === "messages") {
1170
+ if (parsed.type !== "message_start") continue
1171
+ const usage = parsed.message?.usage ?? {}
1172
+ controller.abort()
1173
+ return {
1174
+ abortEvent: "message_start",
1175
+ cacheRead: numeric(usage.cache_read_input_tokens),
1176
+ cacheWrite: numeric(usage.cache_creation_input_tokens),
1177
+ }
1178
+ }
1179
+ controller.abort()
1180
+ return { abortEvent: String(parsed.type ?? "first-event") }
1181
+ }
1182
+ }
1183
+ } finally {
1184
+ controller.abort()
1185
+ await reader.cancel().catch(() => {})
1186
+ }
1187
+ return { abortEvent: "stream-end" }
1188
+ }
1189
+
1190
+ function replayLogLine(session: Session, result: ReplayResult, abortEvent?: string): string {
1191
+ const usage =
1192
+ result.cacheRead === undefined && result.cacheWrite === undefined
1193
+ ? ""
1194
+ : ` cacheRead=${result.cacheRead ?? 0} cacheWrite=${result.cacheWrite ?? 0}`
1195
+ return `replay session=${session.entry.sessionID} model=${session.entry.model} api=${session.entry.api} gap=${session.entry.gapId} n=${session.entry.gapReplays}/${session.entry.cap} status=${result.status} latency=${result.latencyMs}ms abort=${abortEvent ?? "first-chunk"}${usage}`
1196
+ }
1197
+
1198
+ function numeric(value: unknown): number | undefined {
1199
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined
1200
+ }