faberun 0.12.0 → 0.13.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.
@@ -13,7 +13,9 @@
13
13
  *
14
14
  * Transcript, the only thing this process writes to stdout:
15
15
  * {"type":"dsh.started","sessionId":string}
16
- * {"type":"dsh.message","text":string} one per non-empty assistant message
16
+ * {"type":"dsh.tool","name":string,"target":string|null} one per tool call
17
+ * {"type":"dsh.message","text":string,"usage"?:Usage} one per non-empty assistant message,
18
+ * with that message's own usage when the harness sent it
17
19
  * {"type":"dsh.completed","sessionId":string,"result":string,"usage":Usage}
18
20
  * {"type":"dsh.failed","sessionId":string,"kind":string,"error":{...},"usage":Usage}
19
21
  * where Usage is `{inputTokens, outputTokens, cacheReadInputTokens}` with
@@ -99,6 +101,27 @@ function addUsage(totals, usage) {
99
101
  return observed;
100
102
  }
101
103
 
104
+ /**
105
+ * The file, path, command or pattern a tool call names, bounded: the
106
+ * controller's live meter counts the call and the run page shows the target.
107
+ *
108
+ * @param {unknown} rawArguments the harness's JSON-encoded tool arguments
109
+ * @returns {string|null}
110
+ */
111
+ function toolTarget(rawArguments) {
112
+ if (typeof rawArguments !== "string" || !rawArguments) return null;
113
+ let parsed;
114
+ try {
115
+ parsed = JSON.parse(rawArguments);
116
+ } catch {
117
+ return null;
118
+ }
119
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
120
+ const record = /** @type {Record<string, unknown>} */ (parsed);
121
+ const value = record.file_path ?? record.path ?? record.command ?? record.pattern ?? null;
122
+ return typeof value === "string" ? value.slice(0, 200) : null;
123
+ }
124
+
102
125
  /** @param {unknown} reason @returns {{kind: string, error: {code: string, message: string, retryAfterMs?: number} | null}} */
103
126
  function describeReason(reason) {
104
127
  const record = reason && typeof reason === "object" ? /** @type {Record<string, unknown>} */ (reason) : {};
@@ -149,9 +172,21 @@ function fail(kind, error, exitCode = 1) {
149
172
 
150
173
  /** @param {Record<string, any>} event */
151
174
  function fold(event) {
175
+ if (event.type === "tool/call") {
176
+ // Forwarded, not folded away: a tool call is the liveness evidence the
177
+ // controller's stall detector counts, and the step the run page renders.
178
+ const data = event.data && typeof event.data === "object" ? /** @type {Record<string, unknown>} */ (event.data) : {};
179
+ emit({ type: "dsh.tool", name: typeof data.name === "string" ? data.name : "tool", target: toolTarget(data.arguments) });
180
+ return;
181
+ }
152
182
  if (event.type === "assistant/message") {
153
183
  const data = event.data && typeof event.data === "object" ? /** @type {Record<string, unknown>} */ (event.data) : {};
154
- sawUsage = addUsage(usage, data.usage) || sawUsage;
184
+ const messageUsage = { inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0 };
185
+ const observed = addUsage(messageUsage, data.usage);
186
+ if (observed) {
187
+ addUsage(usage, data.usage);
188
+ sawUsage = true;
189
+ }
155
190
  const message = data.message && typeof data.message === "object" ? /** @type {Record<string, unknown>} */ (data.message) : {};
156
191
  const blocks = Array.isArray(message.content) ? message.content : [];
157
192
  const text = blocks.map((block) => (block && typeof block === "object" && typeof block.text === "string" ? block.text : "")).join("");
@@ -159,7 +194,7 @@ function fold(event) {
159
194
  texts.push(text);
160
195
  // Every message, not only the last: a judge that returns two verdicts
161
196
  // must be visible as two, and the adapter counts them here.
162
- emit({ type: "dsh.message", text });
197
+ emit({ type: "dsh.message", text, ...(observed ? { usage: messageUsage } : {}) });
163
198
  }
164
199
  return;
165
200
  }
@@ -22,8 +22,6 @@
22
22
 
23
23
  import {
24
24
  canonicalUsage,
25
- eventItem,
26
- extractJson,
27
25
  failed,
28
26
  parseJsonLines,
29
27
  parseVersion,
@@ -259,503 +257,6 @@ function validateError(value, label) {
259
257
  if (typeof record.message !== "string" || !record.message.trim()) throw new TypeError(`${label}.message must be a non-empty string`);
260
258
  }
261
259
 
262
- /**
263
- * Best-effort input-token meter over a still-growing transcript. The
264
- * controller never owns the provider stream (the gate writes stdout straight
265
- * to the log fd), so budget enforcement polls this instead. Lenient by
266
- * design: unparsable or partial lines count as zero, and providers that only
267
- * report usage at completion (agy, exec-jsonl, replay) meter as 0 mid-run.
268
- *
269
- * @param {string} harness
270
- * @param {string} stdout bounded transcript tail
271
- * @returns {{inputTokens: number|null, cacheReadInputTokens: number|null}}
272
- */
273
- export function liveUsage(harness, stdout) {
274
- if (harness === "exec-jsonl" || harness === "replay") {
275
- // Completion-only harnesses: usage arrives in the terminal envelope, which
276
- // the close path normalizes, never in a mid-run live observation.
277
- return { inputTokens: null, cacheReadInputTokens: null };
278
- }
279
- const events = parsedEvents(stdout);
280
- if (harness === "codex") {
281
- // turn.completed usage is cumulative for the session; the last one wins.
282
- // Codex counts input_tokens with their cached portion included, so the
283
- // uncached total is what the ledger calls `inputTokens`.
284
- const records = events
285
- .filter((event) => event?.type === "turn.completed" && event.usage && typeof event.usage === "object")
286
- .map((event) => {
287
- const rawInput = finite(event.usage.input_tokens ?? event.usage.inputTokens);
288
- if (rawInput === null) return null;
289
- const cacheReadInputTokens = finite(
290
- event.usage.cached_input_tokens ?? event.usage.cacheReadInputTokens ?? event.usage.cache_read_tokens,
291
- ) ?? 0;
292
- return { inputTokens: Math.max(0, rawInput - cacheReadInputTokens), cacheReadInputTokens };
293
- })
294
- .filter((record) => record !== null);
295
- if (!records.length) return { inputTokens: null, cacheReadInputTokens: null };
296
- return records.reduce((best, record) => (
297
- record.inputTokens + record.cacheReadInputTokens > best.inputTokens + best.cacheReadInputTokens ? record : best
298
- ));
299
- }
300
- if (harness === "claude") {
301
- // The terminal result event carries the session total; before it lands,
302
- // sum per-request assistant usage (each request re-reads full context).
303
- // Claude's input_tokens already exclude cache reads.
304
- const resultEvent = events.findLast((event) => event?.type === "result");
305
- const resultUsage = resultEvent?.usage && typeof resultEvent.usage === "object"
306
- ? finite(resultEvent.usage.input_tokens ?? resultEvent.usage.inputTokens)
307
- : null;
308
- if (resultUsage !== null) {
309
- return {
310
- inputTokens: resultUsage,
311
- cacheReadInputTokens: finite(resultEvent.usage.cache_read_input_tokens ?? resultEvent.usage.cacheReadInputTokens) ?? null,
312
- };
313
- }
314
- return {
315
- inputTokens: events.reduce((sum, event) => {
316
- if (event?.type !== "assistant") return sum;
317
- const usage = event.message?.usage;
318
- const value = usage && typeof usage === "object" ? finite(usage.input_tokens ?? usage.inputTokens) : null;
319
- return sum + (value ?? 0);
320
- }, 0) || null,
321
- cacheReadInputTokens: null,
322
- };
323
- }
324
- return { inputTokens: null, cacheReadInputTokens: null };
325
- }
326
-
327
- /**
328
- * Budgeted live meter: one number, with cache reads weighted by the campaign
329
- * policy so it is comparable with the persisted ledger. A provider that only
330
- * reports usage at completion (agy, exec-jsonl, replay) meters as 0 mid-run.
331
- *
332
- * @param {string} harness
333
- * @param {string} stdout bounded transcript tail
334
- * @param {number} [cacheReadWeight] cached-to-uncached rate ratio, default 1
335
- * @returns {number}
336
- */
337
- export function liveInputTokens(harness, stdout, cacheReadWeight = 1) {
338
- const usage = liveUsage(harness, stdout);
339
- if (usage.inputTokens === null) return 0;
340
- const weighted = usage.inputTokens + (usage.cacheReadInputTokens ?? 0) * cacheReadWeight;
341
- return Math.round(weighted * 1000) / 1000;
342
- }
343
-
344
- /**
345
- * Parse each JSONL line independently. A bounded transcript tail can start or
346
- * end mid-line, so unparsable lines are skipped rather than failing the live
347
- * observation.
348
- *
349
- * @param {string} stdout
350
- */
351
- function parsedEvents(stdout) {
352
- return String(stdout).split(/\r?\n/u).flatMap((line) => {
353
- try {
354
- return [JSON.parse(line)];
355
- } catch {
356
- return [];
357
- }
358
- });
359
- }
360
-
361
- /** Codex item types whose completion proves one tool invocation. */
362
- const CODEX_TOOL_ITEM_TYPES = new Set(["tool_call", "command_execution", "mcp_tool_call", "web_search", "file_change"]);
363
-
364
- /**
365
- * Session evidence from a bounded live transcript: completed turns, cache-read
366
- * input, tool invocations, and whether the harness's terminal record has been
367
- * folded. Each harness exposes only what its own events prove, and anything
368
- * unparsable or unsupported meters as zero — a live observation never throws.
369
- *
370
- * @param {string} harness
371
- * @param {string} stdout bounded transcript tail
372
- * @returns {{turns: number, cacheReadInputTokens: number, toolCalls: number, completed: boolean}}
373
- */
374
- export function liveSessionMetrics(harness, stdout) {
375
- const parser = new SessionMetricsParser(harness);
376
- parser.push(String(stdout));
377
- parser.flush();
378
- return parser.metrics();
379
- }
380
-
381
- /** Retention bound for one streamed record: records at or below it parse whole. */
382
- const SESSION_RECORD_MAX_BYTES = 64 * 1024;
383
-
384
- /** Fragment evidence kept for a record that outgrew the retention bound. */
385
- const SESSION_FRAGMENT_BYTES = SESSION_RECORD_MAX_BYTES / 2;
386
-
387
- /** Claude-family content-block needle proving one tool invocation. */
388
- const TOOL_USE_NEEDLE = Buffer.from('"type":"tool_use"', "utf8");
389
-
390
- /** Cache-read evidence spellings across harness streams. */
391
- const CACHE_READ_PATTERN = /"(?:cache_read_input_tokens|cached_input_tokens|cacheReadInputTokens)":(\d+)/gu;
392
-
393
- /**
394
- * Bounded incremental session-metrics parser: fold fixed-size chunks into
395
- * running rotation totals without ever holding a buffer that scales with the
396
- * unread transcript. Records within `SESSION_RECORD_MAX_BYTES` parse whole;
397
- * a larger record keeps head and tail fragments plus streamed needle counts,
398
- * so its turn and usage evidence still lands in the totals instead of being
399
- * silently skipped.
400
- */
401
- export class SessionMetricsParser {
402
- /**
403
- * @param {string} harness
404
- * @param {{turns?: number, cacheReadInputTokens?: number, toolCalls?: number, completed?: boolean}} [previous]
405
- */
406
- constructor(harness, previous = {}) {
407
- this.harness = harness;
408
- this.totals = {
409
- turns: previous.turns ?? 0,
410
- cacheReadInputTokens: previous.cacheReadInputTokens ?? 0,
411
- toolCalls: previous.toolCalls ?? 0,
412
- completed: previous.completed === true,
413
- };
414
- /** @type {string|null} */
415
- this.continuationId = null;
416
- /** @type {string|null} Most recent folded item-completed type, for the codex completion rule. */
417
- this.lastItemType = null;
418
- /** @type {string|null} Text of the most recent folded agent message, for the codex completion rule. */
419
- this.lastAgentText = null;
420
- /** @type {Buffer} */
421
- this.pending = Buffer.alloc(0);
422
- /** @type {{head: Buffer, tail: Buffer, streamedToolUse: number, carry: Buffer}|null} */
423
- this.oversized = null;
424
- }
425
-
426
- /**
427
- * Fold every newline-terminated record in one chunk. A trailing partial
428
- * record stays buffered (bounded) for the next chunk.
429
- *
430
- * @param {string|Buffer} chunk
431
- */
432
- push(chunk) {
433
- let data = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
434
- while (data.length > 0) {
435
- const newline = data.indexOf(10);
436
- if (newline < 0) {
437
- this.absorb(data);
438
- return;
439
- }
440
- this.absorb(data.subarray(0, newline));
441
- this.completeRecord();
442
- data = data.subarray(newline + 1);
443
- }
444
- }
445
-
446
- /** Fold the buffered partial record as if a newline had ended it. */
447
- flush() {
448
- if (this.pending.length > 0 || this.oversized) this.completeRecord();
449
- }
450
-
451
- /** @returns {{turns: number, cacheReadInputTokens: number, toolCalls: number, completed: boolean}} */
452
- metrics() {
453
- return {
454
- turns: this.totals.turns,
455
- cacheReadInputTokens: this.totals.cacheReadInputTokens,
456
- toolCalls: this.totals.toolCalls,
457
- completed: this.totals.completed === true,
458
- };
459
- }
460
-
461
- /**
462
- * Retain one piece of a record still under assembly. Once the record
463
- * outgrows the retention bound, only its head and a rolling tail are kept;
464
- * the bytes leaving the tail are scanned for tool_use evidence instead of
465
- * being buffered.
466
- *
467
- * @param {Buffer} piece
468
- */
469
- absorb(piece) {
470
- if (this.oversized) {
471
- const window = Buffer.concat([this.oversized.tail, piece]);
472
- const keep = window.subarray(Math.max(0, window.length - SESSION_FRAGMENT_BYTES));
473
- const dropped = window.subarray(0, window.length - keep.length);
474
- if (this.harness === "claude") {
475
- const counted = countWithCarry(dropped, this.oversized.carry, TOOL_USE_NEEDLE);
476
- this.oversized.streamedToolUse += counted.hits;
477
- this.oversized.carry = counted.carry;
478
- }
479
- this.oversized.tail = keep;
480
- return;
481
- }
482
- if (this.pending.length + piece.length <= SESSION_RECORD_MAX_BYTES) {
483
- // Copy: `piece` may be a view of a scratch buffer the caller reuses for
484
- // the next read, which would corrupt a record buffered mid-chunk.
485
- this.pending = this.pending.length > 0 ? Buffer.concat([this.pending, piece]) : Buffer.from(piece);
486
- return;
487
- }
488
- const whole = Buffer.concat([this.pending, piece]);
489
- this.pending = Buffer.alloc(0);
490
- const head = whole.subarray(0, Math.min(SESSION_FRAGMENT_BYTES, whole.length));
491
- const tail = whole.subarray(Math.max(0, whole.length - SESSION_FRAGMENT_BYTES));
492
- /** @type {{head: Buffer, tail: Buffer, streamedToolUse: number, carry: Buffer}} */
493
- const oversized = { head, tail, streamedToolUse: 0, carry: Buffer.alloc(0) };
494
- if (this.harness === "claude") {
495
- // Count from the record start up to where the rolling tail takes over,
496
- // so a needle straddling any region boundary is counted exactly once.
497
- const counted = countWithCarry(whole.subarray(0, Math.max(0, whole.length - tail.length)), oversized.carry, TOOL_USE_NEEDLE);
498
- oversized.streamedToolUse = counted.hits;
499
- oversized.carry = counted.carry;
500
- }
501
- this.oversized = oversized;
502
- }
503
-
504
- /** Fold the assembled record into the running totals. */
505
- completeRecord() {
506
- const oversized = this.oversized;
507
- if (oversized) {
508
- this.oversized = null;
509
- /** @type {{head: string, tail: string, toolUse: number}} */
510
- let fragments;
511
- if (this.harness === "claude") {
512
- const counted = countWithCarry(oversized.tail, oversized.carry, TOOL_USE_NEEDLE);
513
- fragments = {
514
- head: decodeFragment(oversized.head),
515
- tail: decodeFragment(oversized.tail),
516
- toolUse: oversized.streamedToolUse + counted.hits,
517
- };
518
- } else {
519
- fragments = { head: decodeFragment(oversized.head), tail: decodeFragment(oversized.tail), toolUse: 0 };
520
- }
521
- foldFragmentRecord(this.harness, this.totals, fragments);
522
- this.continuationId ??= fragmentContinuationId(this.harness, fragments);
523
- return;
524
- }
525
- const line = this.pending.toString("utf8");
526
- this.pending = Buffer.alloc(0);
527
- let event;
528
- try {
529
- event = JSON.parse(line);
530
- } catch {
531
- return;
532
- }
533
- if (!event || typeof event !== "object" || Array.isArray(event)) return;
534
- const record = /** @type {Record<string, unknown>} */ (event);
535
- foldRecord(this.harness, this.totals, record);
536
- this.continuationId ??= recordContinuationId(this.harness, record);
537
- this.foldCompletionEvidence(record);
538
- }
539
-
540
- /**
541
- * Fold the completion evidence one parsed record proves into the sticky
542
- * totals. A harness is completed when its terminal record was folded; for
543
- * codex that means a turn.completed that ends the turn with the
544
- * result-carrying final agent message, so a live observation never treats a
545
- * still-working or already-answered session ambiguously.
546
- *
547
- * @param {Record<string, unknown>} record
548
- */
549
- foldCompletionEvidence(record) {
550
- const totals = this.totals;
551
- if (this.harness === "codex") {
552
- if (record.type === "turn.completed" && this.lastItemType === "agent_message"
553
- && extractJson(this.lastAgentText) !== null) {
554
- totals.completed = true;
555
- }
556
- } else if (this.harness === "claude" && record.type === "result") {
557
- totals.completed = true;
558
- } else if (this.harness === "exec-jsonl" && record.type === "run.completed") {
559
- totals.completed = true;
560
- } else if (this.harness === "replay" && typeof record.status === "string") {
561
- // The replay envelope is the terminal record: the bin emits exactly one
562
- // envelope line per invocation, so folding one proves completion.
563
- totals.completed = true;
564
- }
565
- const item = eventItem(record);
566
- if (record.type === "item.completed" && item) {
567
- this.lastItemType = String(item.type ?? "");
568
- this.lastAgentText = this.lastItemType === "agent_message" && typeof item.text === "string"
569
- ? item.text
570
- : null;
571
- } else {
572
- this.lastItemType = null;
573
- this.lastAgentText = null;
574
- }
575
- }
576
- }
577
-
578
- /**
579
- * Fold one parsed record into the running totals. Turn and tool counts are
580
- * additive; cache-read is a running max for Codex (each turn.completed
581
- * counter is already cumulative) and additive for claude-style streams until
582
- * a terminal result event carries the authoritative session total.
583
- *
584
- * @param {string} harness
585
- * @param {{turns: number, cacheReadInputTokens: number, toolCalls: number, completed: boolean}} totals
586
- * @param {Record<string, unknown>} record
587
- */
588
- function foldRecord(harness, totals, record) {
589
- if (harness === "codex") {
590
- if (record.type === "turn.completed") {
591
- totals.turns += 1;
592
- totals.cacheReadInputTokens = Math.max(totals.cacheReadInputTokens, canonicalUsage(record.usage).cacheReadInputTokens ?? 0);
593
- } else if (record.type === "item.completed" && CODEX_TOOL_ITEM_TYPES.has(String(eventItem(record)?.type))) {
594
- totals.toolCalls += 1;
595
- }
596
- return;
597
- }
598
- if (harness === "claude") {
599
- if (record.type === "assistant") {
600
- const message = /** @type {Record<string, unknown>} */ (record.message ?? {});
601
- totals.turns += 1;
602
- totals.cacheReadInputTokens += canonicalUsage(message.usage).cacheReadInputTokens ?? 0;
603
- totals.toolCalls += Array.isArray(message.content)
604
- ? message.content.filter((/** @type {{type?: unknown}} */ block) => block?.type === "tool_use").length
605
- : 0;
606
- } else if (record.type === "result") {
607
- const sessionTotal = canonicalUsage(record.usage).cacheReadInputTokens;
608
- if (sessionTotal !== null) totals.cacheReadInputTokens = sessionTotal;
609
- }
610
- return;
611
- }
612
- if (harness === "exec-jsonl" && record.type === "run.completed") {
613
- // The protocol carries no tool events; only a completed run proves a turn.
614
- totals.turns += 1;
615
- totals.cacheReadInputTokens += canonicalUsage(record.usage).cacheReadInputTokens ?? 0;
616
- }
617
- if (harness === "replay" && typeof record.status === "string") {
618
- // A replayed envelope is the whole invocation: one completed turn, no
619
- // tool events, usage only in the terminal record.
620
- totals.turns += 1;
621
- totals.cacheReadInputTokens += canonicalUsage(record.usage).cacheReadInputTokens ?? 0;
622
- }
623
- }
624
-
625
- /**
626
- * Fold the head-plus-tail fragments of one record that outgrew the retention
627
- * bound: the same evidence foldRecord extracts, read as fragments so an
628
- * oversized record is never silently skipped.
629
- *
630
- * @param {string} harness
631
- * @param {{turns: number, cacheReadInputTokens: number, toolCalls: number, completed: boolean}} totals
632
- * @param {{head: string, tail: string, toolUse: number}} fragments
633
- */
634
- function foldFragmentRecord(harness, totals, fragments) {
635
- const text = `${fragments.head}\n${fragments.tail}`;
636
- if (harness === "claude") {
637
- if (text.includes('"type":"assistant"')) {
638
- totals.turns += 1;
639
- totals.toolCalls += fragments.toolUse;
640
- const cacheRead = lastCacheRead(text);
641
- if (cacheRead !== null) totals.cacheReadInputTokens += cacheRead;
642
- } else if (text.includes('"type":"result"')) {
643
- const sessionTotal = lastCacheRead(text);
644
- if (sessionTotal !== null) totals.cacheReadInputTokens = sessionTotal;
645
- totals.completed = true;
646
- }
647
- return;
648
- }
649
- if (harness === "codex") {
650
- if (text.includes('"type":"turn.completed"')) {
651
- totals.turns += 1;
652
- const cacheRead = lastCacheRead(text);
653
- totals.cacheReadInputTokens = Math.max(totals.cacheReadInputTokens, cacheRead ?? 0);
654
- // Fragment approximation of the parsed-record completion rule: the
655
- // turn ends with the final agent message when that message appears
656
- // before the completed marker in the retained head and tail.
657
- const agentAt = text.indexOf('"type":"agent_message"');
658
- if (agentAt >= 0 && agentAt < text.indexOf('"type":"turn.completed"')) totals.completed = true;
659
- } else if (text.includes('"type":"item.completed"') && [...CODEX_TOOL_ITEM_TYPES].some((type) => text.includes(`"type":"${type}"`))) {
660
- totals.toolCalls += 1;
661
- }
662
- return;
663
- }
664
- if (harness === "exec-jsonl" && text.includes('"type":"run.completed"')) {
665
- totals.turns += 1;
666
- const cacheRead = lastCacheRead(text);
667
- if (cacheRead !== null) totals.cacheReadInputTokens += cacheRead;
668
- totals.completed = true;
669
- }
670
- }
671
-
672
- /**
673
- * The provider session identity one record proves.
674
- *
675
- * @param {string} harness
676
- * @param {Record<string, unknown>} record
677
- * @returns {string|null}
678
- */
679
- function recordContinuationId(harness, record) {
680
- if (harness === "codex") {
681
- return record.type === "thread.started" && typeof record.thread_id === "string" ? record.thread_id : null;
682
- }
683
- if (harness === "claude") {
684
- return record.type === "result" && typeof record.session_id === "string" ? record.session_id : null;
685
- }
686
- if (harness === "exec-jsonl" && (record.type === "run.started" || record.type === "run.completed")) {
687
- return typeof record.continuationId === "string" ? record.continuationId : null;
688
- }
689
- return null;
690
- }
691
-
692
- /**
693
- * The provider session identity one record's fragments prove.
694
- *
695
- * @param {string} harness
696
- * @param {{head: string, tail: string}} fragments
697
- * @returns {string|null}
698
- */
699
- function fragmentContinuationId(harness, fragments) {
700
- const text = `${fragments.head}\n${fragments.tail}`;
701
- const pattern = harness === "codex"
702
- ? /"thread_id":"([^"]+)"/u
703
- : harness === "claude"
704
- ? /"session_id":"([^"]+)"/u
705
- : /"continuationId":"([^"]+)"/u;
706
- const match = pattern.exec(text);
707
- return match ? match[1] : null;
708
- }
709
-
710
- /**
711
- * Count needle occurrences in one region, keeping the trailing bytes that
712
- * could complete a needle in the next region so a straddling needle is
713
- * counted exactly once.
714
- *
715
- * @param {Buffer} region
716
- * @param {Buffer} carry
717
- * @param {Buffer} needle
718
- * @returns {{hits: number, carry: Buffer}}
719
- */
720
- function countWithCarry(region, carry, needle) {
721
- const stream = carry.length > 0 ? Buffer.concat([carry, region]) : region;
722
- return { hits: countNeedle(stream, needle), carry: stream.subarray(Math.max(0, stream.length - (needle.length - 1))) };
723
- }
724
-
725
- /**
726
- * @param {Buffer} haystack
727
- * @param {Buffer} needle
728
- * @returns {number}
729
- */
730
- function countNeedle(haystack, needle) {
731
- let hits = 0;
732
- for (let at = haystack.indexOf(needle); at >= 0; at = haystack.indexOf(needle, at + needle.length)) hits += 1;
733
- return hits;
734
- }
735
-
736
- /**
737
- * Decode a retained fragment without splitting a UTF-8 sequence.
738
- *
739
- * @param {Buffer} fragment
740
- * @returns {string}
741
- */
742
- function decodeFragment(fragment) {
743
- let start = 0;
744
- while (start < fragment.length && (fragment[start] & 0xc0) === 0x80) start += 1;
745
- return fragment.toString("utf8", start);
746
- }
747
-
748
- /**
749
- * The last cache-read number in a fragment text, or null.
750
- *
751
- * @param {string} text
752
- * @returns {number|null}
753
- */
754
- function lastCacheRead(text) {
755
- const matches = [...text.matchAll(CACHE_READ_PATTERN)];
756
- return matches.length > 0 ? Number(matches.at(-1)?.[1]) : null;
757
- }
758
-
759
260
  /**
760
261
  * Bound one tool result to at most `maxBytes` UTF-8 bytes, keeping the head
761
262
  * and the tail around an omission marker. This is the reference head+tail