@usepowerplant/cli 0.4.18 → 0.4.19

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.
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="7730a7ff-ca8b-5911-9406-4ee9329a7ea8")}catch(e){}}();
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="5ecc7184-ee4b-53d3-858b-4d0790456d70")}catch(e){}}();
4
4
  const __cliNodeMajor = Number(process.versions.node.split('.')[0]);
5
5
  if (__cliNodeMajor < 20) {
6
6
  console.error(`powerplant needs Node 20 or newer — this is Node ${process.versions.node}.`);
@@ -13562,16 +13562,16 @@ var require_connect = __commonJS({
13562
13562
  }
13563
13563
  });
13564
13564
  }
13565
- get(sessionKey) {
13566
- const ref = this._sessionCache.get(sessionKey);
13565
+ get(sessionKey2) {
13566
+ const ref = this._sessionCache.get(sessionKey2);
13567
13567
  return ref ? ref.deref() : null;
13568
13568
  }
13569
- set(sessionKey, session) {
13569
+ set(sessionKey2, session) {
13570
13570
  if (this._maxCachedSessions === 0) {
13571
13571
  return;
13572
13572
  }
13573
- if (this._sessionCache.has(sessionKey)) {
13574
- this._sessionCache.delete(sessionKey);
13573
+ if (this._sessionCache.has(sessionKey2)) {
13574
+ this._sessionCache.delete(sessionKey2);
13575
13575
  } else if (this._sessionCache.size >= this._maxCachedSessions) {
13576
13576
  for (const [key, ref] of this._sessionCache) {
13577
13577
  if (ref.deref() === void 0) {
@@ -13584,8 +13584,8 @@ var require_connect = __commonJS({
13584
13584
  this._sessionCache.delete(oldest.value);
13585
13585
  }
13586
13586
  }
13587
- this._sessionCache.set(sessionKey, new WeakRef(session));
13588
- this._sessionRegistry.register(session, sessionKey);
13587
+ this._sessionCache.set(sessionKey2, new WeakRef(session));
13588
+ this._sessionRegistry.register(session, sessionKey2);
13589
13589
  }
13590
13590
  };
13591
13591
  function buildConnector({ allowH2, useH2c, maxCachedSessions, socketPath, timeout, session: customSession, ...opts }) {
@@ -13603,9 +13603,9 @@ var require_connect = __commonJS({
13603
13603
  tls2 = __require("node:tls");
13604
13604
  }
13605
13605
  servername = servername || options.servername || util2.getServerName(host) || null;
13606
- const sessionKey = servername || hostname4;
13607
- assert2(sessionKey);
13608
- const session = customSession || sessionCache.get(sessionKey) || null;
13606
+ const sessionKey2 = servername || hostname4;
13607
+ assert2(sessionKey2);
13608
+ const session = customSession || sessionCache.get(sessionKey2) || null;
13609
13609
  port = port || 443;
13610
13610
  socket = tls2.connect({
13611
13611
  highWaterMark: 16384,
@@ -13621,7 +13621,7 @@ var require_connect = __commonJS({
13621
13621
  host: hostname4
13622
13622
  });
13623
13623
  socket.on("session", function(session2) {
13624
- sessionCache.set(sessionKey, session2);
13624
+ sessionCache.set(sessionKey2, session2);
13625
13625
  });
13626
13626
  } else {
13627
13627
  assert2(!httpSocket, "httpSocket can only be sent on TLS update");
@@ -36589,11 +36589,11 @@ ${captureLines}` : capture2.stack;
36589
36589
  });
36590
36590
 
36591
36591
  // src/hookPreferences.ts
36592
- import { mkdirSync as mkdirSync12, readFileSync as readFileSync12, renameSync as renameSync6, writeFileSync as writeFileSync10 } from "node:fs";
36592
+ import { mkdirSync as mkdirSync12, readFileSync as readFileSync13, renameSync as renameSync7, writeFileSync as writeFileSync9 } from "node:fs";
36593
36593
  import path24 from "node:path";
36594
36594
 
36595
36595
  // ../../apps/syncservice/src/credentials.ts
36596
- import { chmodSync as chmodSync4, existsSync as existsSync8, mkdirSync as mkdirSync9, readFileSync as readFileSync8, unlinkSync as unlinkSync3, writeFileSync as writeFileSync8 } from "node:fs";
36596
+ import { chmodSync as chmodSync3, existsSync as existsSync8, mkdirSync as mkdirSync9, readFileSync as readFileSync9, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7 } from "node:fs";
36597
36597
  import os12 from "node:os";
36598
36598
  import path14 from "node:path";
36599
36599
 
@@ -56395,6 +56395,35 @@ function codexPromptOrigin(keptBlockOrigins) {
56395
56395
  return keptBlockOrigins.every((origin) => origin === "human") ? "human" : "unknown";
56396
56396
  }
56397
56397
 
56398
+ // ../core/src/rawTranscriptBundle.ts
56399
+ var RAW_TRANSCRIPT_BUNDLE_CONTENT_TYPE = "application/vnd.powerplant.raw-transcript-bundle";
56400
+ var RAW_TRANSCRIPT_PART_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
56401
+ var MAX_RAW_TRANSCRIPT_PARTS = 500;
56402
+ var MAX_HEADER_BYTES = 128 * 1024;
56403
+ var LENGTH_PREFIX_BYTES = 4;
56404
+ var headerSchema = external_exports.object({
56405
+ version: external_exports.literal(1),
56406
+ mainBytes: external_exports.number().int().positive(),
56407
+ parts: external_exports.array(external_exports.object({ part: external_exports.string().regex(RAW_TRANSCRIPT_PART_PATTERN), bytes: external_exports.number().int().positive() })).max(MAX_RAW_TRANSCRIPT_PARTS),
56408
+ subagents: external_exports.object({ found: external_exports.number().int().nonnegative(), skipped: external_exports.number().int().nonnegative() })
56409
+ }).refine((header) => new Set(header.parts.map((entry) => entry.part)).size === header.parts.length, {
56410
+ message: "part ids must be unique"
56411
+ }).refine((header) => header.subagents.found === header.parts.length + header.subagents.skipped, {
56412
+ message: "subagents.found must equal the parts carried plus the files skipped"
56413
+ });
56414
+ function encodeRawTranscriptBundle(bundle) {
56415
+ const fields = headerSchema.parse({
56416
+ version: 1,
56417
+ mainBytes: bundle.main.byteLength,
56418
+ parts: bundle.parts.map((entry) => ({ part: entry.part, bytes: entry.body.byteLength })),
56419
+ subagents: bundle.subagents
56420
+ });
56421
+ const header = Buffer.from(JSON.stringify(fields), "utf8");
56422
+ const prefix = Buffer.alloc(LENGTH_PREFIX_BYTES);
56423
+ prefix.writeUInt32BE(header.byteLength);
56424
+ return Buffer.concat([prefix, header, bundle.main, ...bundle.parts.map((entry) => entry.body)]);
56425
+ }
56426
+
56398
56427
  // ../core/src/encodedImages.ts
56399
56428
  var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
56400
56429
  function isEncodedImage(data, mediaType) {
@@ -56766,8 +56795,90 @@ var CONFIG_LINE_SECRET_VALUE = (
56766
56795
  // eslint-disable-next-line security/detect-unsafe-regex
56767
56796
  /^([+-]?(?:(?:[^\s:]{1,256}:)?\d+[:-])?[ \t]*(?:\d+(?:\t|\u2192)[ \t]*)?(?:(?:export|-)[ \t]+)?)([A-Za-z_][\w.-]{0,63})([ \t]*[=:][ \t]*)([^\s"'\\,;()[\]{}<>]{8,})(?=[ \t]*(?:#.*)?\r?$)/gm
56768
56797
  );
56798
+ function jsonTokens(text) {
56799
+ const tokens = [];
56800
+ for (const { index } of text.matchAll(/["\r\n]/g)) {
56801
+ if (text[index] !== '"') {
56802
+ tokens.push({ index, level: -1 });
56803
+ continue;
56804
+ }
56805
+ let backslashes = 0;
56806
+ while (text[index - backslashes - 1] === "\\") backslashes += 1;
56807
+ let level = 0;
56808
+ for (let run = backslashes + 1; run % 2 === 0; run /= 2) level += 1;
56809
+ tokens.push({ index, level });
56810
+ }
56811
+ return tokens;
56812
+ }
56813
+ function stringEnds(tokens) {
56814
+ const ends = [];
56815
+ const pending = [];
56816
+ for (let i = tokens.length - 1; i >= 0; i -= 1) {
56817
+ const { level } = tokens[i];
56818
+ while (pending.length > 0 && tokens[pending.at(-1)].level > level) pending.pop();
56819
+ ends[i] = pending.at(-1) ?? -1;
56820
+ pending.push(i);
56821
+ }
56822
+ return ends;
56823
+ }
56824
+ function stringTextEnd(text, end) {
56825
+ if (end === void 0) return text.length;
56826
+ return end.level < 0 ? end.index : end.index - (2 ** end.level - 1);
56827
+ }
56828
+ function skipJsonSpace(text, from, level) {
56829
+ const escapeLength = level === 0 ? 0 : 2 ** (level - 1);
56830
+ let at = from;
56831
+ for (; ; ) {
56832
+ const char = text[at];
56833
+ if (char === " " || level === 0 && (char === " " || char === "\r" || char === "\n")) at += 1;
56834
+ else if (char === "\\" && level > 0 && text.startsWith("\\".repeat(escapeLength), at) && /^[nrt]$/.test(text[at + escapeLength] ?? "")) {
56835
+ at += escapeLength + 1;
56836
+ } else return at;
56837
+ }
56838
+ }
56839
+ function decodeJsonKey(raw, level) {
56840
+ if (!raw.includes("\\")) return raw;
56841
+ let key = raw;
56842
+ for (let round = 0; round <= level; round += 1) {
56843
+ try {
56844
+ const decoded = JSON.parse(`"${key}"`);
56845
+ if (typeof decoded !== "string") return key;
56846
+ key = decoded;
56847
+ } catch {
56848
+ return key;
56849
+ }
56850
+ }
56851
+ return key;
56852
+ }
56853
+ function redactJsonSecretValues(text) {
56854
+ if (!text.includes('"')) return text;
56855
+ const tokens = jsonTokens(text);
56856
+ const ends = stringEnds(tokens);
56857
+ let out = "";
56858
+ let copied = 0;
56859
+ for (const [i, { index, level }] of tokens.entries()) {
56860
+ if (level < 0 || index < copied) continue;
56861
+ const keyEnd = ends[i] ?? -1;
56862
+ const keyClose = tokens[keyEnd];
56863
+ if (keyClose?.level !== level) continue;
56864
+ const colon = skipJsonSpace(text, keyClose.index + 1, level);
56865
+ if (text[colon] !== ":") continue;
56866
+ const valueOpen = skipJsonSpace(text, colon + 1, level);
56867
+ let valueQuote = keyEnd + 1;
56868
+ while (tokens[valueQuote]?.level === -1 && (tokens[valueQuote]?.index ?? Infinity) < valueOpen) valueQuote += 1;
56869
+ const quote = tokens[valueQuote];
56870
+ if (quote?.level !== level || quote.index !== valueOpen + 2 ** level - 1) continue;
56871
+ if (!isSecretName(decodeJsonKey(text.slice(index + 1, stringTextEnd(text, keyClose)), level))) continue;
56872
+ const valueStart = quote.index + 1;
56873
+ const valueEnd = stringTextEnd(text, tokens[ends[valueQuote] ?? -1]);
56874
+ if (valueEnd <= valueStart) continue;
56875
+ out += text.slice(copied, valueStart) + REDACTED;
56876
+ copied = valueEnd;
56877
+ }
56878
+ return out + text.slice(copied);
56879
+ }
56769
56880
  function redactSecretNamedValues(text) {
56770
- return text.replace(
56881
+ return redactJsonSecretValues(text).replace(
56771
56882
  QUOTED_SECRET_VALUE,
56772
56883
  (match, name, separator, quote, value) => isSecretName(name) && isSecretValue(value) ? `${name}${separator}${quote}${REDACTED}${quote}` : match
56773
56884
  ).replace(
@@ -57354,6 +57465,44 @@ var TOOL_CALL_INPUT_MAX_BYTES = 65536;
57354
57465
  function boundedToolInput(value) {
57355
57466
  return boundRedactedText(JSON.stringify(value), TOOL_CALL_INPUT_MAX_BYTES);
57356
57467
  }
57468
+ var tokenCount = external_exports.number().nonnegative();
57469
+ var costStateSchema = external_exports.object({
57470
+ sessionId: external_exports.string(),
57471
+ startTime: external_exports.number().positive(),
57472
+ hasUnknownModelCost: external_exports.boolean().optional(),
57473
+ modelUsage: external_exports.record(
57474
+ external_exports.string(),
57475
+ external_exports.object({
57476
+ costUSD: external_exports.number().nonnegative(),
57477
+ inputTokens: tokenCount,
57478
+ outputTokens: tokenCount,
57479
+ cacheReadInputTokens: tokenCount,
57480
+ cacheCreationInputTokens: tokenCount
57481
+ })
57482
+ )
57483
+ });
57484
+ function runCostOf(record2) {
57485
+ const parsed = costStateSchema.safeParse(record2);
57486
+ if (!parsed.success) return null;
57487
+ const runStartedAt = new Date(parsed.data.startTime);
57488
+ if (Number.isNaN(runStartedAt.getTime())) return null;
57489
+ return {
57490
+ sessionId: parsed.data.sessionId,
57491
+ runStartedAt: runStartedAt.toISOString(),
57492
+ // Claude Code's own restore reads a missing flag as false.
57493
+ unknownModelCost: parsed.data.hasUnknownModelCost ?? false,
57494
+ byModel: Object.entries(parsed.data.modelUsage).map(([model, usage]) => ({
57495
+ model,
57496
+ costUsd: usage.costUSD,
57497
+ tokens: {
57498
+ inputTokens: usage.inputTokens,
57499
+ outputTokens: usage.outputTokens,
57500
+ cacheReadTokens: usage.cacheReadInputTokens,
57501
+ cacheWriteTokens: usage.cacheCreationInputTokens
57502
+ }
57503
+ })).sort((a, b2) => a.model < b2.model ? -1 : a.model > b2.model ? 1 : 0)
57504
+ };
57505
+ }
57357
57506
  function createDigestTurnParser(options = {}) {
57358
57507
  const turns = [];
57359
57508
  let pendingTools = [];
@@ -57363,12 +57512,37 @@ function createDigestTurnParser(options = {}) {
57363
57512
  let currentCarrier = null;
57364
57513
  let currentBase = null;
57365
57514
  let currentCounted = false;
57515
+ const countedEnvelopes = /* @__PURE__ */ new Map();
57516
+ let currentReplay = null;
57517
+ let transcriptStartedAt = null;
57518
+ let transcriptSessionId = null;
57519
+ let lastRecordAt = null;
57520
+ let runCost = null;
57521
+ let recordedThrough = null;
57522
+ let laterEnvelopes = [];
57523
+ let laterSet = /* @__PURE__ */ new Set();
57524
+ let usageRevisedAfter = false;
57366
57525
  const envelopes = [];
57367
57526
  let pendingEnvelopes = [];
57368
57527
  let currentEnvelope = null;
57369
57528
  const evidence = new PrEvidenceCollector();
57370
57529
  const backgroundTasks = { started: /* @__PURE__ */ new Map(), reads: /* @__PURE__ */ new Map() };
57371
57530
  let line = -1;
57531
+ const resum = (envelope) => {
57532
+ const landed = envelope.turn === -1 ? pendingEnvelopes : envelopes.filter((e) => e.turn === envelope.turn);
57533
+ const usage = landed.reduce((sum2, e) => mergeUsage(sum2, e.usage), null);
57534
+ if (envelope.turn === -1) pendingUsage = usage;
57535
+ else turns[envelope.turn].usage = usage;
57536
+ };
57537
+ const noteRevision = (envelope, recorded, at) => {
57538
+ if (sameUsage(recorded, envelope.usage)) return;
57539
+ const time3 = at === null ? Number.NaN : Date.parse(at);
57540
+ if (Number.isNaN(time3)) envelope.revisedAt = null;
57541
+ else if (envelope.revisedAt !== null && (envelope.revisedAt === void 0 || Date.parse(envelope.revisedAt) < time3)) {
57542
+ envelope.revisedAt = at;
57543
+ }
57544
+ if (runCost !== null && !laterSet.has(envelope)) usageRevisedAfter = true;
57545
+ };
57372
57546
  const landPending = (turn) => {
57373
57547
  for (const envelope of pendingEnvelopes) {
57374
57548
  envelope.turn = turn;
@@ -57389,6 +57563,18 @@ function createDigestTurnParser(options = {}) {
57389
57563
  } catch {
57390
57564
  return;
57391
57565
  }
57566
+ transcriptStartedAt ??= recordTime(record2);
57567
+ if (record2.type === "cost-state") {
57568
+ runCost = runCostOf(record2);
57569
+ recordedThrough = lastRecordAt;
57570
+ laterEnvelopes = [];
57571
+ laterSet = /* @__PURE__ */ new Set();
57572
+ usageRevisedAfter = false;
57573
+ return;
57574
+ }
57575
+ if (typeof record2.sessionId === "string") transcriptSessionId ??= record2.sessionId;
57576
+ const recordAt = recordTime(record2);
57577
+ if (recordAt !== null && (lastRecordAt === null || Date.parse(recordAt) > Date.parse(lastRecordAt))) lastRecordAt = recordAt;
57392
57578
  if (record2.type === "pr-link") {
57393
57579
  const ref = pullRequestRefFromUrl(typeof record2.prUrl === "string" ? record2.prUrl : "");
57394
57580
  if (ref !== null) evidence.linked(ref, recordTime(record2), line);
@@ -57414,32 +57600,43 @@ function createDigestTurnParser(options = {}) {
57414
57600
  currentId = id;
57415
57601
  currentCarrier = null;
57416
57602
  currentBase = pendingUsage;
57417
- currentCounted = false;
57603
+ currentReplay = id === null ? null : countedEnvelopes.get(id) ?? null;
57604
+ currentCounted = currentReplay !== null;
57418
57605
  }
57419
57606
  const parsedUsage = recordTurnUsage(message2);
57420
57607
  if (parsedUsage) {
57421
- if (currentCounted) {
57608
+ if (currentReplay) {
57609
+ const recorded = currentReplay.usage;
57610
+ keepRecordedTierAndSplit(parsedUsage, recorded);
57611
+ currentReplay.usage = parsedUsage;
57612
+ noteRevision(currentReplay, recorded, at);
57613
+ resum(currentReplay);
57614
+ } else if (currentCounted) {
57422
57615
  const recorded = currentEnvelope?.usage;
57423
- if (parsedUsage.serviceTier === void 0 && recorded?.serviceTier !== void 0) {
57424
- parsedUsage.serviceTier = recorded.serviceTier;
57425
- }
57426
- if (parsedUsage.cacheWrite1hTokens === null && recorded?.cacheWriteTokens === parsedUsage.cacheWriteTokens) {
57427
- parsedUsage.cacheWrite1hTokens = recorded.cacheWrite1hTokens;
57428
- }
57616
+ if (recorded !== void 0) keepRecordedTierAndSplit(parsedUsage, recorded);
57429
57617
  const updated = mergeUsage(currentBase, parsedUsage);
57430
57618
  if (currentCarrier) currentCarrier.usage = updated;
57431
57619
  else pendingUsage = updated;
57432
- if (currentEnvelope) currentEnvelope.usage = parsedUsage;
57620
+ if (currentEnvelope !== null && recorded !== void 0) {
57621
+ currentEnvelope.usage = parsedUsage;
57622
+ noteRevision(currentEnvelope, recorded, at);
57623
+ }
57433
57624
  } else if (currentCarrier) {
57434
57625
  currentCarrier.usage = mergeUsage(currentCarrier.usage, parsedUsage);
57435
57626
  currentCounted = true;
57436
57627
  currentEnvelope = { turn: turns.indexOf(currentCarrier), at, usage: parsedUsage, branch: recordBranch(record2) };
57437
57628
  envelopes.push(currentEnvelope);
57629
+ laterEnvelopes.push(currentEnvelope);
57630
+ laterSet.add(currentEnvelope);
57631
+ if (id !== null) countedEnvelopes.set(id, currentEnvelope);
57438
57632
  } else {
57439
57633
  pendingUsage = mergeUsage(pendingUsage, parsedUsage);
57440
57634
  currentCounted = true;
57441
57635
  currentEnvelope = { turn: -1, at, usage: parsedUsage, branch: recordBranch(record2) };
57442
57636
  pendingEnvelopes.push(currentEnvelope);
57637
+ laterEnvelopes.push(currentEnvelope);
57638
+ laterSet.add(currentEnvelope);
57639
+ if (id !== null) countedEnvelopes.set(id, currentEnvelope);
57443
57640
  }
57444
57641
  }
57445
57642
  const model = typeof message2.model === "string" ? message2.model : null;
@@ -57465,7 +57662,9 @@ function createDigestTurnParser(options = {}) {
57465
57662
  text: part.text,
57466
57663
  origin,
57467
57664
  toolCalls: pendingTools,
57468
- usage: pendingUsage,
57665
+ // A message written again carries zero of its own: its money
57666
+ // landed where it was first counted.
57667
+ usage: pendingUsage ?? (currentReplay ? zeroUsage(model) : null),
57469
57668
  at: pendingAt
57470
57669
  };
57471
57670
  turns.push(turn);
@@ -57501,6 +57700,12 @@ function createDigestTurnParser(options = {}) {
57501
57700
  usageEnvelopes() {
57502
57701
  return envelopes;
57503
57702
  },
57703
+ harnessCost() {
57704
+ if (runCost === null || runCost.sessionId !== transcriptSessionId) return null;
57705
+ const { runStartedAt, unknownModelCost, byModel } = runCost;
57706
+ const laterUsage = laterEnvelopes.map((envelope) => envelope.usage);
57707
+ return { runStartedAt, transcriptStartedAt, recordedThrough, unknownModelCost, byModel, usageRevisedAfter, laterUsage };
57708
+ },
57504
57709
  prEvidence() {
57505
57710
  return evidence.finish();
57506
57711
  },
@@ -57581,6 +57786,14 @@ function messageIdOf(record2, message2) {
57581
57786
  if (typeof record2.uuid === "string") return record2.uuid;
57582
57787
  return null;
57583
57788
  }
57789
+ function keepRecordedTierAndSplit(later, recorded) {
57790
+ if ((later.serviceTier === void 0 || later.serviceTier === "unknown") && recorded.serviceTier !== void 0) {
57791
+ later.serviceTier = recorded.serviceTier;
57792
+ }
57793
+ if (later.cacheWrite1hTokens === null && recorded.cacheWriteTokens === later.cacheWriteTokens) {
57794
+ later.cacheWrite1hTokens = recorded.cacheWrite1hTokens;
57795
+ }
57796
+ }
57584
57797
  function recordTurnUsage(message2) {
57585
57798
  const usage = message2.usage;
57586
57799
  if (!usage || typeof usage !== "object") return null;
@@ -57608,6 +57821,9 @@ function recordTurnUsage(message2) {
57608
57821
  function zeroUsage(model) {
57609
57822
  return { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cacheWrite1hTokens: 0, model };
57610
57823
  }
57824
+ function sameUsage(a, b2) {
57825
+ return a.inputTokens === b2.inputTokens && a.outputTokens === b2.outputTokens && a.cacheReadTokens === b2.cacheReadTokens && a.cacheWriteTokens === b2.cacheWriteTokens && a.cacheWrite1hTokens === b2.cacheWrite1hTokens && a.model === b2.model && a.serviceTier === b2.serviceTier;
57826
+ }
57611
57827
  function mergeUsage(a, b2) {
57612
57828
  if (!a) return b2;
57613
57829
  if (!b2) return a;
@@ -58021,7 +58237,7 @@ function createCodexUsageReader(options = {}) {
58021
58237
  const startPage = () => {
58022
58238
  recordSeenOnPage = false;
58023
58239
  previousTotal = null;
58024
- onFirstPage = false;
58240
+ onFirstPage = onFirstPage && counted === 0;
58025
58241
  };
58026
58242
  return {
58027
58243
  startPage,
@@ -58655,6 +58871,141 @@ function parseCursorRunEventsDetailed(runs, options = {}) {
58655
58871
  return { turns, turnTokens };
58656
58872
  }
58657
58873
 
58874
+ // ../core/src/cursorRunRedaction.ts
58875
+ var REDACTED2 = "[REDACTED]";
58876
+ var SECRET_REST = /^[^\s\\"',;]+/;
58877
+ var CUT_PRIVATE_KEY_BODY = /\[REDACTED\][A-Za-z0-9+/=\s\\]*-----END [A-Z0-9 ]*PRIVATE KEY-----/g;
58878
+ function asRecord4(value) {
58879
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
58880
+ }
58881
+ function nonEmptyString(value) {
58882
+ return typeof value === "string" && value !== "";
58883
+ }
58884
+ function streamedText(event) {
58885
+ const data = asRecord4(event.data);
58886
+ if (data === null) return null;
58887
+ if (event.event === "assistant" || event.event === "thinking") {
58888
+ if (typeof data.text !== "string") return null;
58889
+ return {
58890
+ stream: event.event,
58891
+ served: true,
58892
+ text: data.text,
58893
+ withText: (text) => ({ ...event, data: { ...data, text } })
58894
+ };
58895
+ }
58896
+ if (event.event !== "interaction_update") return null;
58897
+ if (data.type === "text-delta" || data.type === "thinking-delta") {
58898
+ if (typeof data.text !== "string") return null;
58899
+ return {
58900
+ stream: data.type,
58901
+ served: false,
58902
+ text: data.text,
58903
+ withText: (text) => ({ ...event, data: { ...data, text } })
58904
+ };
58905
+ }
58906
+ const update = asRecord4(data.taskUpdate);
58907
+ if (data.type !== "tool-call-delta" || !nonEmptyString(data.callId) || update?.type !== "text-delta" && update?.type !== "thinking-delta" || typeof update.text !== "string") {
58908
+ return null;
58909
+ }
58910
+ return {
58911
+ stream: `${data.callId}/${update.type}`,
58912
+ served: false,
58913
+ text: update.text,
58914
+ withText: (text) => ({ ...event, data: { ...data, taskUpdate: { ...update, text } } })
58915
+ };
58916
+ }
58917
+ function endsTurnText(event, callIds) {
58918
+ const data = asRecord4(event.data);
58919
+ if (event.event === "tool_call") {
58920
+ if (!nonEmptyString(data?.callId) || !nonEmptyString(data.name) || callIds.has(data.callId)) return false;
58921
+ callIds.add(data.callId);
58922
+ return true;
58923
+ }
58924
+ return event.event === "interaction_update" && data?.type === "user-message-appended" && nonEmptyString(asRecord4(data.userMessage)?.text);
58925
+ }
58926
+ function splitLikeParts(parts, redacted) {
58927
+ const joined = parts.join("");
58928
+ if (joined === redacted) return [...parts];
58929
+ const shorter = Math.min(joined.length, redacted.length);
58930
+ let start = 0;
58931
+ while (start < shorter && joined[start] === redacted[start]) start += 1;
58932
+ let same = 0;
58933
+ while (same < shorter - start && joined.at(-1 - same) === redacted.at(-1 - same)) same += 1;
58934
+ const changedEnd = joined.length - same;
58935
+ const changed = redacted.slice(start, redacted.length - same);
58936
+ let at = 0;
58937
+ let placed = false;
58938
+ return parts.map((part, index) => {
58939
+ const end = at + part.length;
58940
+ let text = joined.slice(at, Math.min(end, start));
58941
+ if (!placed && (end > start || index === parts.length - 1)) {
58942
+ text += changed;
58943
+ placed = true;
58944
+ }
58945
+ text += joined.slice(Math.max(at, changedEnd), end);
58946
+ at = end;
58947
+ return text;
58948
+ });
58949
+ }
58950
+ function removeCutSecretRests(texts) {
58951
+ let cut = false;
58952
+ return texts.map((text) => {
58953
+ const part = cut ? text.replace(SECRET_REST, "") : text;
58954
+ if (part !== "") cut = part.endsWith(REDACTED2);
58955
+ return part;
58956
+ });
58957
+ }
58958
+ function keepTextAfterRedactions(parts) {
58959
+ const out = [...parts];
58960
+ let open3 = null;
58961
+ for (const k2 of out.keys()) {
58962
+ if (open3 !== null) {
58963
+ const rest = SECRET_REST.exec(out[k2])?.[0] ?? "";
58964
+ out[open3] += rest;
58965
+ out[k2] = out[k2].slice(rest.length);
58966
+ if (out[k2] === "") continue;
58967
+ }
58968
+ open3 = out[k2].endsWith(REDACTED2) ? k2 : null;
58969
+ }
58970
+ return out;
58971
+ }
58972
+ function redactStreamTexts(texts) {
58973
+ const parts = removeCutSecretRests(texts);
58974
+ const joined = parts.join("");
58975
+ const redacted = redactText(joined.replace(CUT_PRIVATE_KEY_BODY, REDACTED2));
58976
+ const alone = redacted === joined ? parts : parts.map(redactText);
58977
+ const split = alone.join("") === redacted ? alone : splitLikeParts(parts, redacted);
58978
+ return keepTextAfterRedactions(split);
58979
+ }
58980
+ function mapStreams(events, servedOnly, map2) {
58981
+ const out = [...events];
58982
+ const callIds = /* @__PURE__ */ new Set();
58983
+ let streams = /* @__PURE__ */ new Map();
58984
+ const mapStreamsSoFar = () => {
58985
+ for (const indices of streams.values()) {
58986
+ const deltas = indices.map((index) => streamedText(out[index]));
58987
+ const texts = map2(deltas.map((delta) => delta.text));
58988
+ for (const [k2, delta] of deltas.entries()) {
58989
+ if (texts[k2] !== delta.text) out[indices[k2]] = delta.withText(texts[k2]);
58990
+ }
58991
+ }
58992
+ streams = /* @__PURE__ */ new Map();
58993
+ };
58994
+ for (const [index, event] of events.entries()) {
58995
+ if (endsTurnText(event, callIds)) mapStreamsSoFar();
58996
+ const delta = streamedText(event);
58997
+ if (delta === null || servedOnly && !delta.served) continue;
58998
+ const indices = streams.get(delta.stream);
58999
+ if (indices) indices.push(index);
59000
+ else streams.set(delta.stream, [index]);
59001
+ }
59002
+ mapStreamsSoFar();
59003
+ return out;
59004
+ }
59005
+ function redactCursorRunEvents(events, { servedOnly = false } = {}) {
59006
+ return mapStreams(events, servedOnly, redactStreamTexts);
59007
+ }
59008
+
58658
59009
  // ../core/src/previewLinks.ts
58659
59010
  var SKILL_LINE = /^(?:[-*+]\s+)?[*`]*PREVIEW:\s+(\S+)\s+(https?:\/\/\S+)\s*$/gm;
58660
59011
  var MARKDOWN_LINE = (
@@ -58761,7 +59112,7 @@ function envelopeRuns(transcript) {
58761
59112
  for (const item of parsed.runs) {
58762
59113
  const run = item;
58763
59114
  if (typeof run?.runId !== "string" || typeof run.sse !== "string") continue;
58764
- runs.push({ runId: run.runId, events: parseSseEvents(run.sse) });
59115
+ runs.push({ runId: run.runId, events: redactCursorRunEvents(parseSseEvents(run.sse), { servedOnly: true }) });
58765
59116
  }
58766
59117
  return runs;
58767
59118
  }
@@ -58774,7 +59125,7 @@ function safeJson(text) {
58774
59125
  }
58775
59126
 
58776
59127
  // ../core/src/scrubSentryEvent.ts
58777
- var REDACTED2 = "[Filtered]";
59128
+ var REDACTED3 = "[Filtered]";
58778
59129
  var SECRET_KEY_PATTERN = /^(?:authorization|proxy-authorization|cookie|set-cookie|x-api-key|x-auth-token|x-csrf-token|x-supabase-auth|api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|session[_-]?token|client[_-]?secret|password|passwd|secret|signature|token|jwt)$/i;
58779
59130
  var SECRET_QUERY_PARAM_PATTERN = /^(?:token|access_token|refresh_token|id_token|api_key|apikey|key|code|signature|password|secret|client_secret|state)$/i;
58780
59131
  var BEARER_TOKEN_PATTERN = /\bBearer\s+[\w\-._~+/=]+/gi;
@@ -58783,7 +59134,7 @@ var EMBEDDED_QUERY_SECRET_PATTERN = /([?&](?:token|access_token|refresh_token|id
58783
59134
  var URL_FIELD_NAMES = /* @__PURE__ */ new Set(["url", "to", "from", "href", "request_url"]);
58784
59135
  var MAX_DEPTH = 10;
58785
59136
  function scrubString(value) {
58786
- return value.replace(POWERPLANT_TOKEN_PATTERN, REDACTED2).replace(BEARER_TOKEN_PATTERN, `Bearer ${REDACTED2}`).replace(JWT_PATTERN, REDACTED2).replace(EMBEDDED_QUERY_SECRET_PATTERN, `$1${REDACTED2}`);
59137
+ return value.replace(POWERPLANT_TOKEN_PATTERN, REDACTED3).replace(BEARER_TOKEN_PATTERN, `Bearer ${REDACTED3}`).replace(JWT_PATTERN, REDACTED3).replace(EMBEDDED_QUERY_SECRET_PATTERN, `$1${REDACTED3}`);
58787
59138
  }
58788
59139
  function scrubUrl(url2) {
58789
59140
  if (!url2) return url2;
@@ -58797,7 +59148,7 @@ function scrubUrl(url2) {
58797
59148
  let mutated = false;
58798
59149
  for (const key of [...parsed.searchParams.keys()]) {
58799
59150
  if (SECRET_QUERY_PARAM_PATTERN.test(key)) {
58800
- parsed.searchParams.set(key, REDACTED2);
59151
+ parsed.searchParams.set(key, REDACTED3);
58801
59152
  mutated = true;
58802
59153
  }
58803
59154
  }
@@ -58813,7 +59164,7 @@ function scrubValue(value, depth = 0) {
58813
59164
  const out = {};
58814
59165
  for (const [key, child] of Object.entries(value)) {
58815
59166
  if (SECRET_KEY_PATTERN.test(key) || SECRET_QUERY_PARAM_PATTERN.test(key)) {
58816
- out[key] = REDACTED2;
59167
+ out[key] = REDACTED3;
58817
59168
  } else if (URL_FIELD_NAMES.has(key.toLowerCase()) && typeof child === "string") {
58818
59169
  out[key] = scrubUrl(child);
58819
59170
  } else {
@@ -58834,7 +59185,7 @@ function scrubSentryEvent(event) {
58834
59185
  event.request.headers = scrubValue(event.request.headers);
58835
59186
  }
58836
59187
  if (event.request.cookies !== void 0) {
58837
- event.request.cookies = REDACTED2;
59188
+ event.request.cookies = REDACTED3;
58838
59189
  }
58839
59190
  if (event.request.data !== void 0) {
58840
59191
  event.request.data = scrubValue(event.request.data);
@@ -58970,35 +59321,6 @@ function describeSyncSignals(member, now, noUpload = "no sessions uploaded yet")
58970
59321
  return `${line.charAt(0).toUpperCase()}${line.slice(1)}.`;
58971
59322
  }
58972
59323
 
58973
- // ../core/src/rawTranscriptBundle.ts
58974
- var RAW_TRANSCRIPT_BUNDLE_CONTENT_TYPE = "application/vnd.powerplant.raw-transcript-bundle";
58975
- var RAW_TRANSCRIPT_PART_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
58976
- var MAX_RAW_TRANSCRIPT_PARTS = 500;
58977
- var MAX_HEADER_BYTES = 128 * 1024;
58978
- var LENGTH_PREFIX_BYTES = 4;
58979
- var headerSchema = external_exports.object({
58980
- version: external_exports.literal(1),
58981
- mainBytes: external_exports.number().int().positive(),
58982
- parts: external_exports.array(external_exports.object({ part: external_exports.string().regex(RAW_TRANSCRIPT_PART_PATTERN), bytes: external_exports.number().int().positive() })).max(MAX_RAW_TRANSCRIPT_PARTS),
58983
- subagents: external_exports.object({ found: external_exports.number().int().nonnegative(), skipped: external_exports.number().int().nonnegative() })
58984
- }).refine((header) => new Set(header.parts.map((entry) => entry.part)).size === header.parts.length, {
58985
- message: "part ids must be unique"
58986
- }).refine((header) => header.subagents.found === header.parts.length + header.subagents.skipped, {
58987
- message: "subagents.found must equal the parts carried plus the files skipped"
58988
- });
58989
- function encodeRawTranscriptBundle(bundle) {
58990
- const fields = headerSchema.parse({
58991
- version: 1,
58992
- mainBytes: bundle.main.byteLength,
58993
- parts: bundle.parts.map((entry) => ({ part: entry.part, bytes: entry.body.byteLength })),
58994
- subagents: bundle.subagents
58995
- });
58996
- const header = Buffer.from(JSON.stringify(fields), "utf8");
58997
- const prefix = Buffer.alloc(LENGTH_PREFIX_BYTES);
58998
- prefix.writeUInt32BE(header.byteLength);
58999
- return Buffer.concat([prefix, header, bundle.main, ...bundle.parts.map((entry) => entry.body)]);
59000
- }
59001
-
59002
59324
  // ../core/src/version.ts
59003
59325
  function parseReleaseVersion(version2) {
59004
59326
  const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version2);
@@ -59026,6 +59348,8 @@ var HARNESS_SESSION_FLAG_RULES = {
59026
59348
  evidence: external_exports.object({ thread_source: external_exports.enum(["subagent", "guardian_review"]) })
59027
59349
  },
59028
59350
  { flag: "subagent", evidence: external_exports.object({ source: external_exports.object({ subagent: external_exports.object({}) }) }) },
59351
+ // A review run's reviewer names its kind as a string: {subagent: "review"}.
59352
+ { flag: "subagent", evidence: external_exports.object({ source: external_exports.object({ subagent: external_exports.string().min(1) }) }) },
59029
59353
  {
59030
59354
  flag: "standaloneReview",
59031
59355
  evidence: external_exports.object({
@@ -59058,6 +59382,36 @@ function parseHarnessSessionFlags(source, evidence) {
59058
59382
  }
59059
59383
  return flags;
59060
59384
  }
59385
+ var CODEX_CHILD_THREAD_RULES = [
59386
+ { kind: "review", evidence: external_exports.object({ source: external_exports.object({ subagent: external_exports.literal("review") }) }) },
59387
+ {
59388
+ kind: "spawned-agent",
59389
+ evidence: external_exports.object({ source: external_exports.object({ subagent: external_exports.object({ thread_spawn: external_exports.object({}) }) }) })
59390
+ },
59391
+ { kind: "judge", evidence: external_exports.object({ thread_source: external_exports.literal("guardian_review") }) },
59392
+ { kind: "judge", evidence: external_exports.object({ source: external_exports.object({ subagent: external_exports.object({ other: external_exports.literal("guardian") }) }) }) }
59393
+ ];
59394
+ var codexThreadId = external_exports.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i).transform((threadId2) => threadId2.toLowerCase());
59395
+ var CODEX_PARENT_THREAD_RULES = [
59396
+ external_exports.object({ parent_thread_id: codexThreadId }).transform((meta3) => meta3.parent_thread_id),
59397
+ external_exports.object({ source: external_exports.object({ subagent: external_exports.object({ thread_spawn: external_exports.object({ parent_thread_id: codexThreadId }) }) }) }).transform((meta3) => meta3.source.subagent.thread_spawn.parent_thread_id)
59398
+ ];
59399
+ function parseCodexChildThread(sessionMeta) {
59400
+ if (parseHarnessSessionFlags("codex", sessionMeta).subagent !== true) return {};
59401
+ const kind = CODEX_CHILD_THREAD_RULES.find((rule) => rule.evidence.safeParse(sessionMeta).success)?.kind;
59402
+ let parentThreadId;
59403
+ for (const rule of CODEX_PARENT_THREAD_RULES) {
59404
+ const parsed = rule.safeParse(sessionMeta);
59405
+ if (parsed.success) {
59406
+ parentThreadId = parsed.data;
59407
+ break;
59408
+ }
59409
+ }
59410
+ return {
59411
+ ...kind !== void 0 && { kind },
59412
+ ...parentThreadId !== void 0 && { parentThreadId }
59413
+ };
59414
+ }
59061
59415
 
59062
59416
  // ../core/src/recentSessionPage.ts
59063
59417
  var RECENT_SESSION_WINDOW_MS = 72 * 60 * 60 * 1e3;
@@ -78691,11 +79045,11 @@ function enrichInvokeAgentFromStream(callId, final, recordOutputs) {
78691
79045
  }
78692
79046
  const usage = isObjectLike(final.usage) ? final.usage : void 0;
78693
79047
  if (usage) {
78694
- const input2 = tokenCount(usage.inputTokens) ?? tokenCount(usage.promptTokens) ?? tokenCount(usage.tokens);
78695
- const output2 = tokenCount(usage.outputTokens) ?? tokenCount(usage.completionTokens);
79048
+ const input2 = tokenCount2(usage.inputTokens) ?? tokenCount2(usage.promptTokens) ?? tokenCount2(usage.tokens);
79049
+ const output2 = tokenCount2(usage.outputTokens) ?? tokenCount2(usage.completionTokens);
78696
79050
  addTokensToSpan(span, Ra, input2);
78697
79051
  addTokensToSpan(span, Pa, output2);
78698
- addTokensToSpan(span, Ma, tokenCount(usage.totalTokens) ?? sum(input2, output2));
79052
+ addTokensToSpan(span, Ma, tokenCount2(usage.totalTokens) ?? sum(input2, output2));
78699
79053
  }
78700
79054
  if (recordOutputs) {
78701
79055
  const parts = partsFromTextAndToolCalls(final.text, final.toolCalls);
@@ -78823,9 +79177,9 @@ function enrichSpanOnEnd(span, data, channelOptions) {
78823
79177
  }
78824
79178
  const usage = isObjectLike(result.usage) ? result.usage : void 0;
78825
79179
  if (usage) {
78826
- const inputTokens = tokenCount(usage.inputTokens) ?? tokenCount(usage.promptTokens) ?? tokenCount(usage.tokens);
78827
- const outputTokens = tokenCount(usage.outputTokens) ?? tokenCount(usage.completionTokens);
78828
- const totalTokens = tokenCount(usage.totalTokens) ?? sum(inputTokens, outputTokens);
79180
+ const inputTokens = tokenCount2(usage.inputTokens) ?? tokenCount2(usage.promptTokens) ?? tokenCount2(usage.tokens);
79181
+ const outputTokens = tokenCount2(usage.outputTokens) ?? tokenCount2(usage.completionTokens);
79182
+ const totalTokens = tokenCount2(usage.totalTokens) ?? sum(inputTokens, outputTokens);
78829
79183
  if (inputTokens !== void 0) {
78830
79184
  span.setAttribute(Ra, inputTokens);
78831
79185
  }
@@ -78874,7 +79228,7 @@ function getFinishReason(result) {
78874
79228
  }
78875
79229
  return isObjectLike(finishReason) ? asString(finishReason.unified) : void 0;
78876
79230
  }
78877
- function tokenCount(value) {
79231
+ function tokenCount2(value) {
78878
79232
  return asNumber(value) ?? (isObjectLike(value) ? asNumber(value.total) : void 0);
78879
79233
  }
78880
79234
  function buildOutputMessages2(parts, finishReason) {
@@ -85910,9 +86264,9 @@ import {
85910
86264
  fsyncSync,
85911
86265
  mkdirSync as mkdirSync8,
85912
86266
  openSync as openSync6,
85913
- renameSync as renameSync4,
86267
+ renameSync as renameSync5,
85914
86268
  unlinkSync as unlinkSync2,
85915
- writeFileSync as writeFileSync7
86269
+ writeFileSync as writeFileSync6
85916
86270
  } from "node:fs";
85917
86271
  import os11 from "node:os";
85918
86272
  import path13 from "node:path";
@@ -86173,7 +86527,7 @@ function xmlEscape(value) {
86173
86527
  }
86174
86528
 
86175
86529
  // ../../apps/syncservice/src/status.ts
86176
- import { existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync5, readFileSync as readFileSync7, renameSync as renameSync3, rmSync as rmSync6, writeFileSync as writeFileSync6 } from "node:fs";
86530
+ import { existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync5, readFileSync as readFileSync8, renameSync as renameSync4, rmSync as rmSync7, writeFileSync as writeFileSync5 } from "node:fs";
86177
86531
  import os10 from "node:os";
86178
86532
  import path12 from "node:path";
86179
86533
 
@@ -86270,7 +86624,7 @@ import { createHmac } from "node:crypto";
86270
86624
  // ../../apps/syncservice/src/http.ts
86271
86625
  var import_undici = __toESM(require_undici(), 1);
86272
86626
  var CLI_VERSION_HEADER = "x-powerplant-cli-version";
86273
- var cliVersion = true ? "0.4.18" : "0.0.0-checkout";
86627
+ var cliVersion = true ? "0.4.19" : "0.0.0-checkout";
86274
86628
  function cliVersionHeaders() {
86275
86629
  return { [CLI_VERSION_HEADER]: cliVersion, "user-agent": `powerplant-cli/${cliVersion}` };
86276
86630
  }
@@ -86604,7 +86958,8 @@ function bindDiagnosticEnrollment(diagnostic, token) {
86604
86958
  }
86605
86959
 
86606
86960
  // ../../apps/syncservice/src/codexSessions.ts
86607
- import { closeSync as closeSync3, mkdirSync as mkdirSync2, openSync as openSync3, renameSync, rmSync as rmSync2, utimesSync, writeSync } from "node:fs";
86961
+ import { createHash as createHash3 } from "node:crypto";
86962
+ import { closeSync as closeSync3, mkdirSync as mkdirSync2, openSync as openSync3, renameSync, rmSync as rmSync2, writeSync } from "node:fs";
86608
86963
  import os6 from "node:os";
86609
86964
  import path5 from "node:path";
86610
86965
  import { setImmediate as yieldToEventLoop } from "node:timers/promises";
@@ -86810,7 +87165,8 @@ function isPermissionError(cause) {
86810
87165
  }
86811
87166
 
86812
87167
  // ../../apps/syncservice/src/scan.ts
86813
- import { closeSync as closeSync2, openSync as openSync2, readdirSync as readdirSync2, readSync as readSync2, statSync as statSync2 } from "node:fs";
87168
+ import { createHash as createHash2 } from "node:crypto";
87169
+ import { closeSync as closeSync2, fstatSync, openSync as openSync2, readdirSync as readdirSync2, readFileSync as readFileSync5, readSync as readSync2, statSync as statSync2 } from "node:fs";
86814
87170
  import { StringDecoder } from "node:string_decoder";
86815
87171
  var SESSION_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
86816
87172
  var ACTIVE_GRACE_MS = SESSION_ACTIVE_WINDOW_MS;
@@ -86860,6 +87216,87 @@ function readFirstRecord(file2, matches) {
86860
87216
  closeSync2(fd);
86861
87217
  }
86862
87218
  }
87219
+ var TranscriptChangedError = class extends Error {
87220
+ };
87221
+ function sha256(bytes) {
87222
+ return createHash2("sha256").update(bytes).digest("hex");
87223
+ }
87224
+ function readTranscriptIfExists(file2) {
87225
+ let buffer;
87226
+ try {
87227
+ buffer = readFileSync5(file2);
87228
+ } catch (error63) {
87229
+ if (error63.code === "ENOENT") return null;
87230
+ throw error63;
87231
+ }
87232
+ const records = buffer.subarray(0, wholeRecordsEnd(buffer));
87233
+ return {
87234
+ text: records.toString("utf8"),
87235
+ prefix: { bytes: records.byteLength, sha256: sha256(records), fileBytes: buffer.byteLength }
87236
+ };
87237
+ }
87238
+ function wholeRecordsEnd(buffer) {
87239
+ const afterLastNewline = buffer.lastIndexOf(10) + 1;
87240
+ if (afterLastNewline === buffer.byteLength) return afterLastNewline;
87241
+ const tail = buffer.subarray(afterLastNewline).toString("utf8");
87242
+ return parseJsonlLine(tail) === null ? afterLastNewline : buffer.byteLength;
87243
+ }
87244
+ function readTranscriptPrefix(file2, prefix, unchangedSinceRead) {
87245
+ let fd;
87246
+ try {
87247
+ fd = openSync2(file2, "r");
87248
+ } catch (error63) {
87249
+ if (error63.code === "ENOENT") {
87250
+ throw new TranscriptChangedError("The transcript is gone since the collector read it. The next pass reads it again.");
87251
+ }
87252
+ throw error63;
87253
+ }
87254
+ try {
87255
+ if (unchangedSinceRead && fstatSync(fd).size !== prefix.fileBytes) {
87256
+ throw new TranscriptChangedError("The transcript changed after the collector read it. The next pass reads it again.");
87257
+ }
87258
+ const buffer = Buffer.alloc(prefix.bytes);
87259
+ let filled = 0;
87260
+ while (filled < prefix.bytes) {
87261
+ const read = readSync2(fd, buffer, filled, prefix.bytes - filled, filled);
87262
+ if (read === 0) break;
87263
+ filled += read;
87264
+ }
87265
+ if (filled < prefix.bytes || sha256(buffer) !== prefix.sha256) {
87266
+ throw new TranscriptChangedError("The transcript changed after the collector read it. The next pass reads it again.");
87267
+ }
87268
+ return buffer.toString("utf8");
87269
+ } finally {
87270
+ closeSync2(fd);
87271
+ }
87272
+ }
87273
+ var FirstLineTooLongError = class extends Error {
87274
+ };
87275
+ function readFirstLine(file2, maxBytes) {
87276
+ let fd;
87277
+ try {
87278
+ fd = openSync2(file2, "r");
87279
+ } catch (error63) {
87280
+ if (error63.code === "ENOENT") return null;
87281
+ throw error63;
87282
+ }
87283
+ const chunk = Buffer.alloc(READ_CHUNK_BYTES);
87284
+ const read = [];
87285
+ let total = 0;
87286
+ try {
87287
+ for (; ; ) {
87288
+ const bytes = readSync2(fd, chunk, 0, chunk.length, null);
87289
+ if (bytes === 0) return { line: Buffer.concat(read).toString("utf8"), complete: false };
87290
+ const newline = chunk.subarray(0, bytes).indexOf(10);
87291
+ read.push(Buffer.from(chunk.subarray(0, newline === -1 ? bytes : newline)));
87292
+ if (newline !== -1) return { line: Buffer.concat(read).toString("utf8"), complete: true };
87293
+ total += bytes;
87294
+ if (total >= maxBytes) throw new FirstLineTooLongError(`the first line is longer than ${maxBytes} bytes`);
87295
+ }
87296
+ } finally {
87297
+ closeSync2(fd);
87298
+ }
87299
+ }
86863
87300
  function listDirs(dir) {
86864
87301
  return listEntries(dir).filter((e) => e.isDirectory()).map((e) => `${dir}/${e.name}`);
86865
87302
  }
@@ -86890,6 +87327,10 @@ function dominantKey(counts) {
86890
87327
  }
86891
87328
  return best;
86892
87329
  }
87330
+ function laterTimestamp(a, b2) {
87331
+ if (a === void 0 || b2 === void 0) return a ?? b2;
87332
+ return Date.parse(b2) > Date.parse(a) ? b2 : a;
87333
+ }
86893
87334
  function describeReadError(error63) {
86894
87335
  return error63 instanceof Error ? error63.message : String(error63);
86895
87336
  }
@@ -87143,33 +87584,58 @@ function rolloutIdentity(rolloutPath) {
87143
87584
  function codexMergedTranscriptsDir(home) {
87144
87585
  return path5.join(home, ".config", "powerplant", "codex");
87145
87586
  }
87146
- function mergedTranscriptIsFresh(mergedTranscriptPath, mtimeMs) {
87147
- const existing = statIfExists(mergedTranscriptPath);
87148
- return existing !== null && Math.abs(existing.mtimeMs - mtimeMs) < 1;
87587
+ var MERGED_VERSION_HEX = 32;
87588
+ var MERGED_TRANSCRIPT_NAME = new RegExp(`^(?<threadId>.+)\\.[0-9a-f]{${MERGED_VERSION_HEX}}\\.jsonl$`);
87589
+ function mergedTranscriptPath(mergedTranscriptsDir, threadId2, pagesKey) {
87590
+ const version2 = createHash3("sha256").update(pagesKey).digest("hex").slice(0, MERGED_VERSION_HEX);
87591
+ return path5.join(mergedTranscriptsDir, `${threadId2}.${version2}.jsonl`);
87592
+ }
87593
+ function mergedThreadIds(mergedTranscriptsDir) {
87594
+ const threadIds = /* @__PURE__ */ new Set();
87595
+ for (const name of listFiles(mergedTranscriptsDir, ".jsonl")) {
87596
+ threadIds.add(MERGED_TRANSCRIPT_NAME.exec(name)?.groups?.threadId ?? name.slice(0, -".jsonl".length));
87597
+ }
87598
+ return threadIds;
87599
+ }
87600
+ function removeOtherMergedTranscripts(keep) {
87601
+ const dir = path5.dirname(keep);
87602
+ const name = path5.basename(keep);
87603
+ const threadId2 = MERGED_TRANSCRIPT_NAME.exec(name)?.groups?.threadId;
87604
+ if (threadId2 === void 0) return;
87605
+ for (const other of listFiles(dir, "")) {
87606
+ if (other === name || !other.startsWith(`${threadId2}.`) || other.endsWith(".tmp")) continue;
87607
+ rmSync2(path5.join(dir, other), { force: true });
87608
+ }
87609
+ }
87610
+ function mergedPage(pageText2) {
87611
+ return pageText2.endsWith("\n") ? pageText2 : `${pageText2}
87612
+ `;
87149
87613
  }
87150
87614
  var MergedTranscriptWriter = class {
87151
- constructor(mergedTranscriptPath) {
87152
- this.mergedTranscriptPath = mergedTranscriptPath;
87153
- mkdirSync2(path5.dirname(mergedTranscriptPath), { recursive: true, mode: 448 });
87154
- this.tmpPath = `${mergedTranscriptPath}.${process.pid}.tmp`;
87615
+ constructor(mergedTranscriptPath2) {
87616
+ this.mergedTranscriptPath = mergedTranscriptPath2;
87617
+ mkdirSync2(path5.dirname(mergedTranscriptPath2), { recursive: true, mode: 448 });
87618
+ this.tmpPath = `${mergedTranscriptPath2}.${process.pid}.tmp`;
87155
87619
  this.fd = openSync3(this.tmpPath, "w", 384);
87156
87620
  }
87157
87621
  mergedTranscriptPath;
87158
87622
  tmpPath;
87159
87623
  fd;
87160
- bytes = 0;
87161
- append(pageText2) {
87624
+ /** Writes one page as mergedPage returned it. */
87625
+ append(page) {
87162
87626
  if (this.fd === void 0) throw new Error("merged transcript already closed");
87163
- const text = pageText2.endsWith("\n") ? pageText2 : `${pageText2}
87164
- `;
87165
- this.bytes += writeSync(this.fd, text);
87166
- }
87167
- /** Stamps the newest page's mtime and publishes the file; returns its size. */
87168
- finish(mtimeMs) {
87627
+ writeSync(this.fd, page);
87628
+ }
87629
+ /** Publishes the file under its version's name in one rename, then
87630
+ * removes the thread's other merged transcripts. A pass that saw older
87631
+ * pages can publish after this one and remove this file: this version is
87632
+ * then missing, so the next pass writes it again, and an upload from it
87633
+ * waits for that pass (TranscriptChangedError). No pass reads another
87634
+ * version's bytes under this name. */
87635
+ finish() {
87169
87636
  this.close();
87170
- utimesSync(this.tmpPath, mtimeMs / 1e3, mtimeMs / 1e3);
87171
87637
  renameSync(this.tmpPath, this.mergedTranscriptPath);
87172
- return this.bytes;
87638
+ removeOtherMergedTranscripts(this.mergedTranscriptPath);
87173
87639
  }
87174
87640
  abandon() {
87175
87641
  this.close();
@@ -87181,13 +87647,40 @@ var MergedTranscriptWriter = class {
87181
87647
  this.fd = void 0;
87182
87648
  }
87183
87649
  };
87184
- function rolloutLocation(rolloutPath) {
87185
- const record2 = readFirstRecord(rolloutPath, (candidate) => candidate.type === "session_meta");
87186
- const meta3 = record2?.payload;
87187
- if (!meta3 || typeof meta3 !== "object" || Array.isArray(meta3)) return { kind: "session", codexClient: "unknown", totals: null, fastTotals: null };
87650
+ var MAX_SESSION_META_BYTES = 1024 * 1024;
87651
+ function readFirstSessionMeta(rolloutPath) {
87652
+ let first;
87653
+ try {
87654
+ first = readFirstLine(rolloutPath, MAX_SESSION_META_BYTES);
87655
+ } catch (error63) {
87656
+ if (error63 instanceof FirstLineTooLongError) return { state: "fault", reason: error63.message };
87657
+ throw error63;
87658
+ }
87659
+ if (first === null) return { state: "pending" };
87660
+ let record2;
87661
+ try {
87662
+ record2 = JSON.parse(first.line);
87663
+ } catch {
87664
+ record2 = void 0;
87665
+ }
87666
+ if (!isRecord2(record2)) {
87667
+ return first.complete ? { state: "fault", reason: "the first line is not a JSON object (expected a session_meta record)" } : { state: "pending" };
87668
+ }
87669
+ if (record2.type !== "session_meta" || !isRecord2(record2.payload)) {
87670
+ return { state: "fault", reason: "the first record is not a session_meta record with a payload object" };
87671
+ }
87672
+ return { state: "session_meta", timestamp: record2.timestamp, meta: record2.payload };
87673
+ }
87674
+ function rolloutLocation(rolloutPath, first = readFirstSessionMeta(rolloutPath)) {
87675
+ const unknown2 = { kind: "session", codexClient: "unknown", totals: null, fastTotals: null };
87676
+ if (first.state === "session_meta") return locationOf(first.timestamp, first.meta);
87677
+ if (first.state === "pending") return unknown2;
87678
+ const record2 = readFirstRecord(rolloutPath, (candidate) => candidate.type === "session_meta" && isRecord2(candidate.payload));
87679
+ return record2 === null ? unknown2 : locationOf(record2.timestamp, record2.payload);
87680
+ }
87681
+ function locationOf(startedAt, meta3) {
87188
87682
  const { cwd, id, session_id: fallbackId } = meta3;
87189
87683
  const sessionId = id ?? fallbackId;
87190
- const startedAt = record2?.timestamp;
87191
87684
  return {
87192
87685
  codexClient: classifyCodexClient(meta3),
87193
87686
  kind: isSidecarThread(meta3) ? "subagent" : "session",
@@ -87198,9 +87691,314 @@ function rolloutLocation(rolloutPath) {
87198
87691
  ...typeof cwd === "string" && cwd !== "" && { cwd }
87199
87692
  };
87200
87693
  }
87694
+ var FirstLineFormatError = class extends Error {
87695
+ };
87696
+ function readThreadLink(rolloutPath) {
87697
+ const first = readFirstSessionMeta(rolloutPath);
87698
+ if (first.state === "fault") throw new FirstLineFormatError(first.reason);
87699
+ return first.state === "pending" ? void 0 : parseCodexChildThread(first.meta);
87700
+ }
87201
87701
  function isSidecarThread(meta3) {
87202
87702
  return parseHarnessSessionFlags("codex", meta3).subagent === true;
87203
87703
  }
87704
+ var ATTACHED_CHILD_KINDS = /* @__PURE__ */ new Set(["review"]);
87705
+ function attachedParentOf(link) {
87706
+ return link?.kind !== void 0 && ATTACHED_CHILD_KINDS.has(link.kind) ? link.parentThreadId : void 0;
87707
+ }
87708
+ function threadIdTimeMs(threadId2) {
87709
+ if (!SESSION_UUID.test(threadId2) || threadId2[14] !== "7") return void 0;
87710
+ return Number.parseInt(threadId2.slice(0, 8) + threadId2.slice(9, 13), 16);
87711
+ }
87712
+ var THREAD_ID_TIME_MARGIN_MS = 24 * 60 * 60 * 1e3;
87713
+ var PARENT_BRING_IN_MARGIN_MS = 60 * 1e3;
87714
+ function mayBeParentOf(parent, childMadeMs) {
87715
+ return (parent.madeMs === void 0 || parent.madeMs <= childMadeMs + PARENT_BRING_IN_MARGIN_MS) && parent.writtenMs >= childMadeMs;
87716
+ }
87717
+ function mayBeChildOf(threadTimeMs, parentTimeMs) {
87718
+ return threadTimeMs === void 0 || parentTimeMs === void 0 || threadTimeMs + THREAD_ID_TIME_MARGIN_MS >= parentTimeMs;
87719
+ }
87720
+ var CodexThreadLinks = class {
87721
+ /** `readLink` is a test seam for the first-record read. */
87722
+ constructor(readLink = readThreadLink) {
87723
+ this.readLink = readLink;
87724
+ }
87725
+ readLink;
87726
+ byPath = /* @__PURE__ */ new Map();
87727
+ /** Rollouts whose read failed, with the file's changed time at that
87728
+ * failure: a failure is reported once for each changed time. */
87729
+ failedAt = /* @__PURE__ */ new Map();
87730
+ /** Rollouts whose first line was not whole yet, with the file's changed
87731
+ * time and how many reads found it so. */
87732
+ incomplete = /* @__PURE__ */ new Map();
87733
+ /** Rollouts whose first line is a format fault. Codex writes a rollout's
87734
+ * first line once and only appends after it, so the fault belongs to the
87735
+ * file, not to one of its changed times: it is reported once. */
87736
+ faulted = /* @__PURE__ */ new Set();
87737
+ /** Unlinked rollouts counted as an unreadable child of their plausible
87738
+ * parents, by thread id, with the time in that id. */
87739
+ unresolved = /* @__PURE__ */ new Map();
87740
+ /** Threads whose capture counted such a rollout that has since linked or
87741
+ * gone: each comes into every pass until its corrected capture is
87742
+ * uploaded, refused, or has failed CORRECTION_UPLOAD_ATTEMPTS times
87743
+ * (settleRecoveries), with the session the last pass made of it and how
87744
+ * many uploads of it failed. */
87745
+ recovering = /* @__PURE__ */ new Map();
87746
+ /** Keeps what another read of the same first record found. */
87747
+ remember(rolloutPath, link) {
87748
+ this.byPath.set(rolloutPath, link);
87749
+ this.failedAt.delete(rolloutPath);
87750
+ this.incomplete.delete(rolloutPath);
87751
+ this.faulted.delete(rolloutPath);
87752
+ }
87753
+ /** What an earlier read of this rollout's first record found, if any. */
87754
+ known(rolloutPath) {
87755
+ return this.byPath.get(rolloutPath);
87756
+ }
87757
+ /** Unlinked rollouts that made their plausible parents partial: thread
87758
+ * id to the time in that id. */
87759
+ unresolvedRollouts() {
87760
+ return [...this.unresolved];
87761
+ }
87762
+ /** The rollout linked or is gone: its plausible parents are to be sent
87763
+ * again (recover). */
87764
+ resolve(threadId2) {
87765
+ this.unresolved.delete(threadId2);
87766
+ }
87767
+ /**
87768
+ * Reads again the first record of each unlinked rollout still on this
87769
+ * machine (unresolved), in a pass's window or not, and whether or not a
87770
+ * possible parent uploads now: only a read can link it, and until it links
87771
+ * its guessed parents stay partial. A read failure that clears with no
87772
+ * file changing (EMFILE, EIO, a permission restored) so clears on the next
87773
+ * pass. A read that failed earlier this pass (`unread`) is not repeated, a
87774
+ * first line found torn is left to its rule (isTorn), and a format fault
87775
+ * is final for the file. Returns the failures to report, once for each
87776
+ * change to a file, and every thread whose read failed this call, so the
87777
+ * pass does not read it again (childrenOf).
87778
+ */
87779
+ async readUnresolved(threads, unread) {
87780
+ const failed2 = [];
87781
+ const stillUnread = [];
87782
+ for (const threadId2 of this.unresolved.keys()) {
87783
+ const thread = threads.get(threadId2);
87784
+ if (thread === void 0 || unread.has(threadId2)) continue;
87785
+ const { rolloutPath, changedMs } = thread;
87786
+ if (this.byPath.has(rolloutPath) || this.faulted.has(rolloutPath) || this.isTorn(rolloutPath, changedMs)) continue;
87787
+ if (!await this.read(rolloutPath, changedMs, failed2)) stillUnread.push(threadId2);
87788
+ }
87789
+ return { failed: failed2, unread: stillUnread };
87790
+ }
87791
+ recover(threadId2) {
87792
+ if (!this.recovering.has(threadId2)) this.recovering.set(threadId2, { session: null, failedUploads: 0 });
87793
+ }
87794
+ /** The threads owed a corrected capture. */
87795
+ recoveringThreads() {
87796
+ return [...this.recovering.keys()];
87797
+ }
87798
+ /** The session this pass made of a thread owed a corrected capture. */
87799
+ noteCollected(threadId2, session) {
87800
+ const owed = this.recovering.get(threadId2);
87801
+ if (owed !== void 0) owed.session = session;
87802
+ }
87803
+ /** Drops a thread owed a corrected capture that has nothing to upload
87804
+ * (gone, out of scope, not a session, or its own read failed): nothing
87805
+ * corrects it until it changes, and a change admits it through the
87806
+ * window. */
87807
+ forgetRecovery(threadId2) {
87808
+ this.recovering.delete(threadId2);
87809
+ }
87810
+ /**
87811
+ * After a pass's uploads, from what became of each owed thread's session
87812
+ * this pass: "shipped" (the server holds it) and "refused" (this machine
87813
+ * will never ship it: no raw storage, another machine owns it, its
87814
+ * metadata is out of policy or deferred) settle it. "failed" counts one
87815
+ * failed upload; a thread whose uploads failed CORRECTION_UPLOAD_ATTEMPTS
87816
+ * times is settled too, and returned so the pass can name it once. A
87817
+ * thread the pass left out ("waiting") stays owed at no cost.
87818
+ */
87819
+ settleRecoveries(outcome) {
87820
+ const givenUp = [];
87821
+ for (const [threadId2, owed] of this.recovering) {
87822
+ const result = owed.session === null ? "waiting" : outcome(owed.session);
87823
+ owed.session = null;
87824
+ if (result === "waiting") continue;
87825
+ if (result === "failed") {
87826
+ owed.failedUploads += 1;
87827
+ if (owed.failedUploads < CORRECTION_UPLOAD_ATTEMPTS) continue;
87828
+ givenUp.push(threadId2);
87829
+ }
87830
+ this.recovering.delete(threadId2);
87831
+ }
87832
+ return givenUp;
87833
+ }
87834
+ /** Records that a read of the rollout failed at the thread's changed time;
87835
+ * true when the failure is new and so is to be reported: the first for
87836
+ * the rollout, or the first since the thread changed, as a read error
87837
+ * (EACCES, EIO) can clear when the file changes. Every reader of a Codex
87838
+ * rollout reports through this or noteFormatFault, so a failure is named
87839
+ * once whether the thread is in the pass's window or not. */
87840
+ noteFailure(rolloutPath, changedMs) {
87841
+ const known = this.failedAt.get(rolloutPath) === changedMs;
87842
+ this.failedAt.set(rolloutPath, changedMs);
87843
+ return !known;
87844
+ }
87845
+ /** Records that the rollout's first line is a format fault; true the
87846
+ * first time for the file, whatever Codex appends to it later. */
87847
+ noteFormatFault(rolloutPath) {
87848
+ const known = this.faulted.has(rolloutPath);
87849
+ this.faulted.add(rolloutPath);
87850
+ return !known;
87851
+ }
87852
+ /**
87853
+ * The child threads of each parent uploading now. `threads` maps each
87854
+ * thread on this machine to its lead rollout, the one its first record is
87855
+ * read from, and the thread's newest change; what is known about other
87856
+ * paths is forgotten. Reads the first record of each thread not yet known
87857
+ * that could be such a child: a thread whose id time is more than
87858
+ * THREAD_ID_TIME_MARGIN_MS older than every parent's is not read.
87859
+ * `parentsWrittenMs` gives each uploading thread's newest write, and each
87860
+ * entry of `threads` its own (`writtenMs`). A rollout whose read fails, or
87861
+ * that is in `unread` (its read failed earlier this pass, already
87862
+ * reported), counts as an unreadable child of each uploading thread that
87863
+ * may be its parent, or whose attached child, or a thread that child
87864
+ * started, may be (mayBeParentOf), so no capture is recorded complete
87865
+ * while one of its children may be missing. Only a Codex rollout
87866
+ * named by a UUIDv7 thread id can be such a child: a failed read of any
87867
+ * other file in the store is reported and counts for no parent. A failed
87868
+ * read is tried again on every pass and returned in `failed` once for each
87869
+ * change to the file, so one bad file never ends the pass or repeats in
87870
+ * every pass's report.
87871
+ */
87872
+ async childrenOf(threads, parentsWrittenMs, unread = /* @__PURE__ */ new Set()) {
87873
+ const leadPaths = new Set([...threads.values()].map((thread) => thread.rolloutPath));
87874
+ for (const known of [this.byPath, this.failedAt, this.incomplete, this.faulted]) {
87875
+ for (const rolloutPath of known.keys()) if (!leadPaths.has(rolloutPath)) known.delete(rolloutPath);
87876
+ }
87877
+ const children = /* @__PURE__ */ new Map();
87878
+ const failed2 = [];
87879
+ const parents = new Map([...parentsWrittenMs].map(([threadId2, writtenMs]) => [threadId2, { madeMs: threadIdTimeMs(threadId2), writtenMs }]));
87880
+ if (parents.size === 0) return { children, failed: failed2 };
87881
+ const parentTimes = [...parents];
87882
+ const childrenOfParent = (parentThreadId) => {
87883
+ const found = children.get(parentThreadId) ?? { attached: [], leftOut: [], unreadable: [] };
87884
+ children.set(parentThreadId, found);
87885
+ return found;
87886
+ };
87887
+ const unreadable = [];
87888
+ for (const [threadId2, { rolloutPath, changedMs }] of threads) {
87889
+ if (parents.has(threadId2)) continue;
87890
+ if (!this.byPath.has(rolloutPath)) {
87891
+ const rollout = SESSION_UUID.test(threadId2);
87892
+ const madeMs = threadIdTimeMs(threadId2);
87893
+ if (rollout && !parentTimes.some(([, parent]) => mayBeChildOf(madeMs, parent.madeMs))) continue;
87894
+ if (this.isTorn(rolloutPath, changedMs)) continue;
87895
+ if (unread.has(threadId2) || !await this.read(rolloutPath, changedMs, failed2)) {
87896
+ if (rollout && madeMs !== void 0) unreadable.push([threadId2, madeMs]);
87897
+ continue;
87898
+ }
87899
+ }
87900
+ const link = this.byPath.get(rolloutPath);
87901
+ if (link?.parentThreadId === void 0 || !parents.has(link.parentThreadId)) continue;
87902
+ const found = childrenOfParent(link.parentThreadId);
87903
+ (attachedParentOf(link) === void 0 ? found.leftOut : found.attached).push(threadId2);
87904
+ }
87905
+ const covered = this.addDescendantsOfAttached(threads, parents, children);
87906
+ for (const [threadId2, madeMs] of unreadable) {
87907
+ let counted = false;
87908
+ for (const [rootId, root] of parentTimes) {
87909
+ const possible = [
87910
+ root,
87911
+ ...(covered.get(rootId) ?? []).map((id) => ({ madeMs: threadIdTimeMs(id), writtenMs: threads.get(id).writtenMs }))
87912
+ ];
87913
+ if (!possible.some((parent) => mayBeParentOf(parent, madeMs))) continue;
87914
+ childrenOfParent(rootId).unreadable.push(threadId2);
87915
+ counted = true;
87916
+ }
87917
+ if (counted) this.unresolved.set(threadId2, madeMs);
87918
+ }
87919
+ return { children, failed: failed2 };
87920
+ }
87921
+ /** The threads an attached child started (a reviewer's own agents and
87922
+ * judges), and theirs in turn, name a thread that uploads as a part, not
87923
+ * as a session: they are left out of the root session's capture. A thread
87924
+ * met twice is counted once, so a malformed link cycle ends. Returns, for
87925
+ * each root, its attached children and the threads they started: the
87926
+ * threads whose own children count under the root. */
87927
+ addDescendantsOfAttached(threads, parents, children) {
87928
+ const childrenByParent = /* @__PURE__ */ new Map();
87929
+ for (const [threadId2, { rolloutPath }] of threads) {
87930
+ const parentThreadId = this.byPath.get(rolloutPath)?.parentThreadId;
87931
+ if (parentThreadId === void 0 || parents.has(parentThreadId)) continue;
87932
+ childrenByParent.set(parentThreadId, [...childrenByParent.get(parentThreadId) ?? [], threadId2]);
87933
+ }
87934
+ const covered = /* @__PURE__ */ new Map();
87935
+ for (const [rootId, found] of children) {
87936
+ const seen = new Set(found.attached);
87937
+ const pending = [...found.attached];
87938
+ for (let threadId2 = pending.pop(); threadId2 !== void 0; threadId2 = pending.pop()) {
87939
+ for (const descendant of childrenByParent.get(threadId2) ?? []) {
87940
+ if (seen.has(descendant)) continue;
87941
+ seen.add(descendant);
87942
+ found.leftOut.push(descendant);
87943
+ pending.push(descendant);
87944
+ }
87945
+ }
87946
+ covered.set(rootId, [...seen]);
87947
+ }
87948
+ return covered;
87949
+ }
87950
+ /** Whether the rollout's first line stayed unwhole through
87951
+ * INCOMPLETE_FIRST_LINE_READS reads and the file has not changed since:
87952
+ * Codex left it torn, so reading it again would find the same. Such a file
87953
+ * is part of one line and holds no turns or usage, so no capture misses
87954
+ * anything without it: it is reported, never counted as a child. */
87955
+ isTorn(rolloutPath, changedMs) {
87956
+ const seen = this.incomplete.get(rolloutPath);
87957
+ return seen?.changedMs === changedMs && seen.reads >= INCOMPLETE_FIRST_LINE_READS;
87958
+ }
87959
+ /** One read of a rollout's first record; false when it failed. */
87960
+ async read(rolloutPath, changedMs, failed2) {
87961
+ await yieldToEventLoop();
87962
+ let link;
87963
+ try {
87964
+ link = this.readLink(rolloutPath);
87965
+ } catch (error63) {
87966
+ const fault = error63 instanceof FirstLineFormatError;
87967
+ if (fault ? this.noteFormatFault(rolloutPath) : this.noteFailure(rolloutPath, changedMs)) failed2.push({ rolloutPath, error: error63 });
87968
+ return false;
87969
+ }
87970
+ if (link !== void 0) {
87971
+ this.remember(rolloutPath, link);
87972
+ return true;
87973
+ }
87974
+ const seen = this.incomplete.get(rolloutPath);
87975
+ const reads = seen?.changedMs === changedMs ? seen.reads + 1 : 1;
87976
+ this.incomplete.set(rolloutPath, { changedMs, reads });
87977
+ if (reads >= INCOMPLETE_FIRST_LINE_READS && this.noteFailure(rolloutPath, changedMs)) {
87978
+ failed2.push({ rolloutPath, error: new Error(`the first line is still not whole after ${reads} reads of the unchanged file`) });
87979
+ }
87980
+ return true;
87981
+ }
87982
+ };
87983
+ var CodexThreadReads = class {
87984
+ byThread = /* @__PURE__ */ new Map();
87985
+ /** What a full read found when the thread's files were `files`
87986
+ * (threadFilesKey): its summary and prefix, null for a thread holding no
87987
+ * session; undefined when none is kept for those files. */
87988
+ get(threadId2, files) {
87989
+ const kept = this.byThread.get(threadId2);
87990
+ return kept?.files === files ? kept.read : void 0;
87991
+ }
87992
+ set(threadId2, files, read) {
87993
+ this.byThread.set(threadId2, { files, read });
87994
+ }
87995
+ /** Forgets the threads no longer on this machine. */
87996
+ keepOnly(threadIds) {
87997
+ for (const threadId2 of this.byThread.keys()) if (!threadIds.has(threadId2)) this.byThread.delete(threadId2);
87998
+ }
87999
+ };
88000
+ var CORRECTION_UPLOAD_ATTEMPTS = 3;
88001
+ var INCOMPLETE_FIRST_LINE_READS = 3;
87204
88002
  function repoFullNameFrom(url2) {
87205
88003
  const fullName = parseGithubRepo(url2);
87206
88004
  return fullName ? { repoFullName: fullName } : {};
@@ -87240,8 +88038,9 @@ async function collectCodexSessions(options = {}) {
87240
88038
  const capture2 = options.capture ?? captureEverythingUnresolved;
87241
88039
  const maxBytes = options.maxTranscriptBytes ?? MAX_TRANSCRIPT_FILE_BYTES;
87242
88040
  const mergedTranscriptsDir = options.mergedTranscriptsDir ?? codexMergedTranscriptsDir(options.homeDir ?? os6.homedir());
88041
+ const links = options.threadLinks ?? new CodexThreadLinks();
87243
88042
  const items = [];
87244
- const candidates = [];
88043
+ const candidates = /* @__PURE__ */ new Map();
87245
88044
  const unreadable = [];
87246
88045
  const skips = { skippedOutOfScope: 0, skippedNoRepo: 0 };
87247
88046
  let skippedSubagentThreads = 0;
@@ -87287,80 +88086,135 @@ async function collectCodexSessions(options = {}) {
87287
88086
  if (thread === void 0) byThread.set(file2.identity.threadId, [file2]);
87288
88087
  else thread.push(file2);
87289
88088
  }
87290
- for (const [threadId2, observed] of byThread) {
88089
+ const threadOf = (threadId2, stored) => {
88090
+ const named = stored.filter((file2) => {
88091
+ if (file2.discoverySessionIds === void 0) return true;
88092
+ const { sessionId } = rolloutLocation(file2.rolloutPath);
88093
+ return sessionId !== void 0 && file2.discoverySessionIds.includes(sessionId);
88094
+ });
88095
+ if (named.length === 0) return null;
88096
+ const threadFiles = named.length > 1 ? orderPages(named) : named;
88097
+ return {
88098
+ threadId: threadId2,
88099
+ harnessSessionId: harnessSessionIdOf(threadFiles[0].rolloutPath),
88100
+ files: threadFiles,
88101
+ ...threadFiles.length > 1 && {
88102
+ mergedTranscriptPath: mergedTranscriptPath(mergedTranscriptsDir, threadId2, threadFilesKey(threadFiles))
88103
+ },
88104
+ mtimeMs: Math.max(...threadFiles.map((file2) => file2.mtimeMs)),
88105
+ sizeBytes: threadFiles.reduce((sum2, file2) => sum2 + file2.sizeBytes, 0),
88106
+ archived: threadFiles.every((file2) => file2.archived)
88107
+ };
88108
+ };
88109
+ const countedTooLarge = /* @__PURE__ */ new Set();
88110
+ const tooLarge = (thread) => {
88111
+ if (thread.sizeBytes <= maxBytes) return false;
88112
+ if (countedTooLarge.has(thread.threadId)) return true;
88113
+ countedTooLarge.add(thread.threadId);
88114
+ skippedTooLarge += 1;
88115
+ observeFileDiagnostic(options.onFileDiagnostic, {
88116
+ path: thread.files[0].rolloutPath,
88117
+ reason: "too_large",
88118
+ sizeBytes: thread.sizeBytes
88119
+ });
88120
+ return true;
88121
+ };
88122
+ const mergedBefore = options.locationsOnly ? /* @__PURE__ */ new Set() : mergedThreadIds(mergedTranscriptsDir);
88123
+ const evaluated = /* @__PURE__ */ new Set();
88124
+ const failedThreads = /* @__PURE__ */ new Set();
88125
+ const report = (rolloutPath, name, error63) => {
88126
+ unreadable.push(`${name}: ${describeReadError(error63)}`);
88127
+ observeFileDiagnostic(options.onFileDiagnostic, {
88128
+ path: rolloutPath,
88129
+ reason: "unreadable",
88130
+ sizeBytes: null,
88131
+ errorCode: diagnosticErrorCode(error63)
88132
+ });
88133
+ };
88134
+ const reportOnce = (files2, name, error63) => {
88135
+ const lead = leadRollout(files2);
88136
+ if (links.noteFailure(lead.rolloutPath, newestChange(files2))) report(lead.rolloutPath, name, error63);
88137
+ };
88138
+ const namedParents = [];
88139
+ const bringInParentOf = (link) => {
88140
+ if (link?.parentThreadId !== void 0) namedParents.push(link.parentThreadId);
88141
+ };
88142
+ const parentsOfAttachedChildren = [];
88143
+ const unlinkedInWindow = [];
88144
+ const admit = async (threadId2, observed, admission) => {
88145
+ const applyWindow = admission === "window";
88146
+ const guess = admission === "possible-parent";
87291
88147
  const stored = dedupeStoreCopies(observed);
87292
- let harnessSessionId = harnessSessionIdOf((stored.find((file2) => !file2.identity.page) ?? stored[0]).rolloutPath);
88148
+ const lead = leadRollout(stored);
88149
+ let harnessSessionId = harnessSessionIdOf(lead.rolloutPath);
87293
88150
  try {
87294
- const mergedTranscriptPath = stored.length > 1 ? path5.join(mergedTranscriptsDir, `${threadId2}.jsonl`) : void 0;
87295
- const changedMs = Math.max(...stored.map((file2) => file2.changedMs));
87296
- if (options.sinceMs !== void 0 && changedMs < options.sinceMs) {
87297
- const neverMerged = mergedTranscriptPath !== void 0 && !options.locationsOnly && statIfExists(mergedTranscriptPath) === null;
87298
- if (!neverMerged) continue;
88151
+ if (applyWindow && options.sinceMs !== void 0 && newestChange(stored) < options.sinceMs) {
88152
+ const neverMerged = stored.length > 1 && !options.locationsOnly && !mergedBefore.has(threadId2);
88153
+ if (!neverMerged) return;
87299
88154
  }
88155
+ evaluated.add(threadId2);
87300
88156
  await yieldToEventLoop();
87301
- const named = stored.filter((file2) => {
87302
- if (file2.discoverySessionIds === void 0) return true;
87303
- const { sessionId } = rolloutLocation(file2.rolloutPath);
87304
- return sessionId !== void 0 && file2.discoverySessionIds.includes(sessionId);
87305
- });
87306
- if (named.length === 0) continue;
87307
- const threadFiles = named.length > 1 ? orderPages(named) : named;
87308
- const base = threadFiles[0];
87309
- harnessSessionId = harnessSessionIdOf(base.rolloutPath);
87310
- const mtimeMs = Math.max(...threadFiles.map((file2) => file2.mtimeMs));
87311
- const sizeBytes = threadFiles.reduce((sum2, file2) => sum2 + file2.sizeBytes, 0);
87312
- if (sizeBytes > maxBytes) {
87313
- skippedTooLarge += 1;
87314
- observeFileDiagnostic(options.onFileDiagnostic, {
87315
- path: base.rolloutPath,
87316
- reason: "too_large",
87317
- sizeBytes
87318
- });
87319
- continue;
87320
- }
87321
- const location = rolloutLocation(base.rolloutPath);
88157
+ const thread = threadOf(threadId2, stored);
88158
+ if (thread === null) return;
88159
+ harnessSessionId = thread.harnessSessionId;
88160
+ const base = thread.files[0];
88161
+ const first = readFirstSessionMeta(base.rolloutPath);
88162
+ const link = first.state === "session_meta" ? parseCodexChildThread(first.meta) : void 0;
88163
+ if (link !== void 0) links.remember(base.rolloutPath, link);
88164
+ if (guess && (first.state === "pending" || link?.parentThreadId !== void 0)) return;
88165
+ bringInParentOf(link);
88166
+ if (first.state === "fault") {
88167
+ if (applyWindow) unlinkedInWindow.push(threadId2);
88168
+ if (links.noteFormatFault(lead.rolloutPath)) {
88169
+ report(lead.rolloutPath, `${harnessSessionId} (child-thread link)`, new Error(first.reason));
88170
+ }
88171
+ }
88172
+ if (guess ? thread.sizeBytes > maxBytes : tooLarge(thread)) return;
88173
+ const location = rolloutLocation(base.rolloutPath, first);
88174
+ if (guess && location.kind === "subagent") return;
87322
88175
  if (location.kind === "subagent") {
87323
- skippedSubagentThreads += 1;
87324
- continue;
88176
+ const parentThreadId = attachedParentOf(link);
88177
+ if (!options.locationsOnly && parentThreadId !== void 0) parentsOfAttachedChildren.push(parentThreadId);
88178
+ else skippedSubagentThreads += 1;
88179
+ return;
87325
88180
  }
87326
88181
  const decision = captureDecision(location.cwd, capture2);
87327
88182
  if (!decision.capture) {
87328
- countCaptureSkip(skips, decision.reason);
87329
- continue;
88183
+ if (!guess) countCaptureSkip(skips, decision.reason);
88184
+ return;
87330
88185
  }
87331
- const thread = {
87332
- harnessSessionId,
87333
- files: threadFiles,
87334
- ...threadFiles.length > 1 && mergedTranscriptPath !== void 0 && { mergedTranscriptPath },
87335
- mtimeMs,
87336
- archived: threadFiles.every((file2) => file2.archived)
87337
- };
87338
88186
  if (options.locationsOnly) {
87339
88187
  items.push({
87340
- mtimeMs,
88188
+ mtimeMs: thread.mtimeMs,
87341
88189
  transcriptPath: base.rolloutPath,
87342
88190
  ...location.cwd && { cwd: location.cwd },
87343
88191
  item: {
87344
88192
  harness: "codex-local",
87345
88193
  harnessSessionId,
87346
- status: now - mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
88194
+ status: now - thread.mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
87347
88195
  archived: thread.archived,
87348
88196
  capturedVia: decision.via
87349
88197
  }
87350
88198
  });
87351
- continue;
88199
+ return;
87352
88200
  }
87353
- candidates.push({ thread, ...location.cwd && { cwd: location.cwd }, decision });
87354
- } catch (error63) {
87355
- unreadable.push(`${harnessSessionId}: ${describeReadError(error63)}`);
87356
- observeFileDiagnostic(options.onFileDiagnostic, {
87357
- path: stored[0].rolloutPath,
87358
- reason: "unreadable",
87359
- sizeBytes: null,
87360
- errorCode: diagnosticErrorCode(error63)
88201
+ candidates.set(threadId2, {
88202
+ thread,
88203
+ ...location.cwd && { cwd: location.cwd },
88204
+ decision,
88205
+ children: [],
88206
+ leftOutChildren: 0,
88207
+ unreadableChildren: 0
87361
88208
  });
88209
+ } catch (error63) {
88210
+ const known = links.known(lead.rolloutPath);
88211
+ bringInParentOf(known);
88212
+ if (applyWindow && known === void 0) unlinkedInWindow.push(threadId2);
88213
+ failedThreads.add(threadId2);
88214
+ reportOnce(stored, harnessSessionId, error63);
87362
88215
  }
87363
- }
88216
+ };
88217
+ for (const [threadId2, observed] of byThread) await admit(threadId2, observed, "window");
87364
88218
  if (options.locationsOnly) {
87365
88219
  return {
87366
88220
  sessions: items.sort((a, b2) => b2.mtimeMs - a.mtimeMs),
@@ -87373,91 +88227,259 @@ async function collectCodexSessions(options = {}) {
87373
88227
  largestMergedBytes: 0
87374
88228
  };
87375
88229
  }
87376
- candidates.sort((a, b2) => b2.thread.mtimeMs - a.thread.mtimeMs);
87377
- const chosen = options.maxSessions === void 0 ? candidates : candidates.slice(0, options.maxSessions);
88230
+ for (let index = 0; index < namedParents.length; index += 1) {
88231
+ const parentThreadId = namedParents[index];
88232
+ const observed = byThread.get(parentThreadId);
88233
+ if (!evaluated.has(parentThreadId) && observed !== void 0) await admit(parentThreadId, observed, "named");
88234
+ }
88235
+ const plausibleParentsOf = (childId, childMadeMs) => [...byThread].filter(
88236
+ ([threadId2, observed]) => threadId2 !== childId && mayBeParentOf({ madeMs: threadIdTimeMs(threadId2), writtenMs: Math.max(...observed.map((file2) => file2.mtimeMs)) }, childMadeMs)
88237
+ );
88238
+ const rootThroughAttached = (threadId2) => {
88239
+ const seen = /* @__PURE__ */ new Set();
88240
+ let current = threadId2;
88241
+ let lastLink;
88242
+ while (!seen.has(current)) {
88243
+ seen.add(current);
88244
+ const observed = byThread.get(current);
88245
+ if (observed === void 0) return void 0;
88246
+ const link = links.known(leadRollout(dedupeStoreCopies(observed)).rolloutPath);
88247
+ if (link?.parentThreadId === void 0) return lastLink === void 0 || attachedParentOf(lastLink) !== void 0 ? current : void 0;
88248
+ lastLink = link;
88249
+ current = link.parentThreadId;
88250
+ }
88251
+ return void 0;
88252
+ };
88253
+ for (const childId of unlinkedInWindow) {
88254
+ const childMadeMs = threadIdTimeMs(childId);
88255
+ if (childMadeMs === void 0 || candidates.has(childId)) continue;
88256
+ for (const [threadId2, observed] of plausibleParentsOf(childId, childMadeMs)) {
88257
+ if (!evaluated.has(threadId2)) await admit(threadId2, observed, "possible-parent");
88258
+ const root = rootThroughAttached(threadId2);
88259
+ const rootObserved = root === void 0 ? void 0 : byThread.get(root);
88260
+ if (root !== void 0 && rootObserved !== void 0 && !evaluated.has(root)) await admit(root, rootObserved, "possible-parent");
88261
+ }
88262
+ }
88263
+ const leads = /* @__PURE__ */ new Map();
88264
+ for (const [threadId2, observed] of byThread) {
88265
+ const vouched = dedupeStoreCopies(observed).filter((file2) => file2.discoverySessionIds === void 0);
88266
+ if (vouched.length === 0) continue;
88267
+ leads.set(threadId2, {
88268
+ rolloutPath: leadRollout(vouched).rolloutPath,
88269
+ changedMs: newestChange(vouched),
88270
+ writtenMs: Math.max(...vouched.map((file2) => file2.mtimeMs))
88271
+ });
88272
+ }
88273
+ const retried = await links.readUnresolved(leads, failedThreads);
88274
+ for (const { rolloutPath, error: error63 } of retried.failed) report(rolloutPath, harnessSessionIdOf(rolloutPath), error63);
88275
+ for (const threadId2 of retried.unread) failedThreads.add(threadId2);
88276
+ for (const [childId, childMadeMs] of links.unresolvedRollouts()) {
88277
+ const observed = byThread.get(childId);
88278
+ if (observed !== void 0 && links.known(leadRollout(dedupeStoreCopies(observed)).rolloutPath) === void 0) continue;
88279
+ links.resolve(childId);
88280
+ for (const [threadId2] of plausibleParentsOf(childId, childMadeMs)) {
88281
+ const root = rootThroughAttached(threadId2);
88282
+ if (root !== void 0) links.recover(root);
88283
+ }
88284
+ }
88285
+ for (const threadId2 of links.recoveringThreads()) {
88286
+ const observed = byThread.get(threadId2);
88287
+ if (observed !== void 0 && !evaluated.has(threadId2)) await admit(threadId2, observed, "possible-parent");
88288
+ if (!candidates.has(threadId2)) links.forgetRecovery(threadId2);
88289
+ }
88290
+ for (const parentThreadId of parentsOfAttachedChildren) {
88291
+ if (!candidates.has(parentThreadId)) skippedSubagentThreads += 1;
88292
+ }
88293
+ const countReviewersLeftBehind = (parentThreadId) => {
88294
+ skippedSubagentThreads += parentsOfAttachedChildren.filter((threadId2) => threadId2 === parentThreadId).length;
88295
+ };
88296
+ const parentsWrittenMs = new Map([...candidates].map(([threadId2, candidate]) => [threadId2, candidate.thread.mtimeMs]));
88297
+ const { children, failed: failed2 } = await links.childrenOf(leads, parentsWrittenMs, failedThreads);
88298
+ for (const { rolloutPath, error: error63 } of failed2) report(rolloutPath, harnessSessionIdOf(rolloutPath), error63);
88299
+ for (const [parentThreadId, found] of children) {
88300
+ const parent = candidates.get(parentThreadId);
88301
+ parent.children = found.attached;
88302
+ parent.leftOutChildren = found.leftOut.length;
88303
+ parent.unreadableChildren = found.unreadable.length;
88304
+ }
88305
+ options.threadReads?.keepOnly(new Set(byThread.keys()));
88306
+ const rankOf = (candidate) => {
88307
+ let rank = candidate.thread.mtimeMs;
88308
+ for (const childId of candidate.children) {
88309
+ if (failedThreads.has(childId) || !RAW_TRANSCRIPT_PART_PATTERN.test(childId)) continue;
88310
+ const files2 = dedupeStoreCopies(byThread.get(childId));
88311
+ if (files2.reduce((sum2, file2) => sum2 + file2.sizeBytes, 0) > maxBytes) continue;
88312
+ rank = Math.max(rank, ...files2.map((file2) => file2.mtimeMs));
88313
+ }
88314
+ return rank;
88315
+ };
88316
+ const ranks = new Map([...candidates.values()].map((candidate) => [candidate, rankOf(candidate)]));
88317
+ const ordered = [...candidates.values()].sort((a, b2) => ranks.get(b2) - ranks.get(a));
88318
+ const chosen = options.maxSessions === void 0 ? ordered : ordered.slice(0, options.maxSessions);
88319
+ for (const candidate of ordered.slice(chosen.length)) countReviewersLeftBehind(candidate.thread.threadId);
87378
88320
  const threadNames = chosen.length > 0 ? loadThreadNames(sessionsDir) : /* @__PURE__ */ new Map();
87379
- for (const candidate of chosen) {
87380
- await yieldToEventLoop();
87381
- const { thread, cwd, decision } = candidate;
88321
+ const readThread = (thread) => {
88322
+ const files2 = threadFilesKey(thread.files);
88323
+ const merged = thread.mergedTranscriptPath;
88324
+ const mergedFresh = merged === void 0 || statIfExists(merged) !== null;
88325
+ let read = mergedFresh ? options.threadReads?.get(thread.threadId, files2) : void 0;
88326
+ if (read === void 0) read = readThreadInFull(thread, !mergedFresh);
88327
+ if (read !== null && read !== "vanished" && merged !== void 0 && statIfExists(merged)?.size !== read.prefix.bytes) {
88328
+ read = readThreadInFull(thread, true);
88329
+ }
88330
+ if (read === "vanished") return "vanished";
88331
+ options.threadReads?.set(thread.threadId, files2, read);
88332
+ if (read === null) return null;
88333
+ const { summary, prefix } = read;
88334
+ if (merged === void 0) return { summary, prefix, mtimeMs: thread.mtimeMs, transcriptPath: thread.files[0].rolloutPath };
88335
+ mergedThreads += 1;
88336
+ largestMergedBytes = Math.max(largestMergedBytes, prefix.bytes);
88337
+ return { summary, prefix, mtimeMs: thread.mtimeMs, transcriptPath: merged };
88338
+ };
88339
+ const readThreadInFull = (thread, rewrite) => {
87382
88340
  let writer;
87383
88341
  try {
87384
- const rewrite = thread.mergedTranscriptPath !== void 0 && !mergedTranscriptIsFresh(thread.mergedTranscriptPath, thread.mtimeMs);
87385
88342
  if (thread.mergedTranscriptPath !== void 0 && rewrite) writer = new MergedTranscriptWriter(thread.mergedTranscriptPath);
87386
88343
  const pages = [];
87387
88344
  const usage = createCodexUsageReader();
87388
- let vanished = false;
88345
+ let rolloutPrefix;
88346
+ const merged = { bytes: 0, sha256: createHash3("sha256") };
87389
88347
  for (const [index, file2] of thread.files.entries()) {
87390
- const text = readIfExists(file2.rolloutPath);
87391
- if (text === null) {
87392
- vanished = true;
87393
- break;
88348
+ const rollout = readTranscriptIfExists(file2.rolloutPath);
88349
+ if (rollout === null) {
88350
+ writer?.abandon();
88351
+ return "vanished";
87394
88352
  }
87395
88353
  if (index > 0) usage.startPage();
87396
- const page = parseCodexRollout(text, usage);
88354
+ const page = parseCodexRollout(rollout.text, usage);
87397
88355
  if (page === null) continue;
87398
88356
  pages.push(page);
87399
- writer?.append(text);
88357
+ if (thread.mergedTranscriptPath === void 0) {
88358
+ rolloutPrefix = rollout.prefix;
88359
+ } else {
88360
+ const text = mergedPage(rollout.text);
88361
+ merged.bytes += Buffer.byteLength(text);
88362
+ merged.sha256.update(text);
88363
+ writer?.append(text);
88364
+ }
87400
88365
  }
87401
- if (vanished || pages.length === 0) {
88366
+ const prefix = thread.mergedTranscriptPath === void 0 ? rolloutPrefix : { bytes: merged.bytes, sha256: merged.sha256.digest("hex"), fileBytes: merged.bytes };
88367
+ if (pages.length === 0 || prefix === void 0) {
87402
88368
  writer?.abandon();
87403
- continue;
88369
+ return null;
88370
+ }
88371
+ writer?.finish();
88372
+ return { summary: foldRolloutPages(pages, usage.finish()), prefix };
88373
+ } catch (error63) {
88374
+ writer?.abandon();
88375
+ throw error63;
88376
+ }
88377
+ };
88378
+ const readChildren = (candidate) => {
88379
+ const parent = candidate.thread;
88380
+ const reads = [];
88381
+ const subagents = {
88382
+ files: [],
88383
+ skipped: candidate.leftOutChildren + candidate.unreadableChildren,
88384
+ skippedUnreadable: candidate.unreadableChildren
88385
+ };
88386
+ for (const childId of candidate.children) {
88387
+ const stored = dedupeStoreCopies(byThread.get(childId));
88388
+ let childName = harnessSessionIdOf(stored[0].rolloutPath);
88389
+ try {
88390
+ const child = threadOf(childId, stored);
88391
+ if (child === null) {
88392
+ subagents.skipped += 1;
88393
+ continue;
88394
+ }
88395
+ childName = child.harnessSessionId;
88396
+ if (tooLarge(child) || !RAW_TRANSCRIPT_PART_PATTERN.test(childId)) {
88397
+ subagents.skipped += 1;
88398
+ continue;
88399
+ }
88400
+ const read = readThread(child);
88401
+ if (read === "vanished") return "vanished";
88402
+ if (read === null) {
88403
+ subagents.skipped += 1;
88404
+ subagents.skippedUnreadable += 1;
88405
+ continue;
88406
+ }
88407
+ reads.push(read);
88408
+ subagents.files.push({ part: childId, path: read.transcriptPath, prefix: read.prefix });
88409
+ } catch (error63) {
88410
+ subagents.skipped += 1;
88411
+ subagents.skippedUnreadable += 1;
88412
+ reportOnce(stored, `${parent.harnessSessionId} (subagent ${childName})`, error63);
87404
88413
  }
87405
- const summary = foldRolloutPages(pages, usage.finish());
87406
- let transcriptUploadSourcePath = thread.files[0].rolloutPath;
87407
- if (thread.mergedTranscriptPath !== void 0) {
87408
- const mergedBytes = writer ? writer.finish(thread.mtimeMs) : statIfExists(thread.mergedTranscriptPath)?.size ?? 0;
87409
- writer = void 0;
87410
- transcriptUploadSourcePath = thread.mergedTranscriptPath;
87411
- mergedThreads += 1;
87412
- largestMergedBytes = Math.max(largestMergedBytes, mergedBytes);
88414
+ }
88415
+ return { reads, capture: subagents };
88416
+ };
88417
+ for (const candidate of chosen) {
88418
+ await yieldToEventLoop();
88419
+ const { thread, cwd, decision } = candidate;
88420
+ try {
88421
+ const children2 = readChildren(candidate);
88422
+ if (children2 === "vanished") continue;
88423
+ const parent = readThread(thread);
88424
+ if (parent === "vanished") continue;
88425
+ if (parent === null) {
88426
+ countReviewersLeftBehind(thread.threadId);
88427
+ links.forgetRecovery(thread.threadId);
88428
+ continue;
87413
88429
  }
88430
+ const mtimeMs = Math.max(parent.mtimeMs, ...children2.reads.map((read) => read.mtimeMs));
88431
+ const summary = parent.summary;
88432
+ const latestChildFirst = [...children2.reads].sort((a, b2) => b2.mtimeMs - a.mtimeMs).map((read) => read.summary);
88433
+ const model = summary.model ?? latestChildFirst.find((child) => child.model !== void 0)?.model;
88434
+ const speedMode = summary.speedMode ?? latestChildFirst.find((child) => child.speedMode !== void 0)?.speedMode;
88435
+ const endedAt = latestChildFirst.reduce((latest2, child) => laterTimestamp(latest2, child.endedAt), summary.endedAt);
88436
+ const usage = sessionUsage([summary, ...latestChildFirst]);
87414
88437
  const repoDir = summary.repoName ?? (cwd ? path5.basename(cwd) : void 0);
87415
88438
  const repoFullName = decision.repoFullName ?? summary.repoFullName;
87416
88439
  const title = summary.sessionId ? threadNames.get(summary.sessionId) : void 0;
87417
- items.push({
87418
- mtimeMs: thread.mtimeMs,
88440
+ const collected = {
88441
+ mtimeMs,
87419
88442
  codexClient: summary.codexClient,
87420
88443
  ...summary.noninteractive && { codexNoninteractive: true },
87421
- transcriptPath: transcriptUploadSourcePath,
88444
+ transcriptPath: parent.transcriptPath,
88445
+ transcriptPrefix: parent.prefix,
88446
+ subagents: children2.capture,
87422
88447
  ...cwd && { cwd },
87423
88448
  item: {
87424
88449
  harness: "codex-local",
87425
88450
  harnessSessionId: thread.harnessSessionId,
87426
- status: now - thread.mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
88451
+ status: now - mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive",
87427
88452
  archived: thread.archived,
87428
88453
  ...summary.standaloneReview && { automation: true },
87429
88454
  ...summary.startedAt && { startedAt: summary.startedAt },
87430
- ...summary.endedAt && { endedAt: summary.endedAt },
87431
- ...summary.model && { model: summary.model },
87432
- ...summary.speedMode && { speedMode: summary.speedMode },
88455
+ ...endedAt && { endedAt },
88456
+ ...model && { model },
88457
+ ...speedMode && { speedMode },
87433
88458
  ...title && { title: redactText(title).slice(0, 500) },
87434
88459
  // input_tokens already includes the cached subset — it IS the
87435
88460
  // all-in input-class total. No cache-write figure exists, so
87436
88461
  // none is sent (unmeasured, never 0).
87437
- ...summary.totals && { inputTokens: summary.totals.input },
87438
- ...summary.totals && { outputTokens: summary.totals.output },
87439
- ...summary.totals && { cacheReadTokens: summary.totals.cached },
87440
- ...summary.fastTotals && { fastInputTokens: summary.fastTotals.input },
87441
- ...summary.fastTotals && { fastOutputTokens: summary.fastTotals.output },
87442
- ...summary.fastTotals && { fastCacheReadTokens: summary.fastTotals.cached },
88462
+ ...usage.totals && { inputTokens: usage.totals.input },
88463
+ ...usage.totals && { outputTokens: usage.totals.output },
88464
+ ...usage.totals && { cacheReadTokens: usage.totals.cached },
88465
+ ...usage.fastTotals && { fastInputTokens: usage.fastTotals.input },
88466
+ ...usage.fastTotals && { fastOutputTokens: usage.fastTotals.output },
88467
+ ...usage.fastTotals && { fastCacheReadTokens: usage.fastTotals.cached },
87443
88468
  // OpenAI prompt-cache writes have no separate price in the current
87444
88469
  // session cost model; zero is measured here, not an invented count.
87445
- ...summary.fastTotals && { fastCacheWriteTokens: 0 },
88470
+ ...usage.fastTotals && { fastCacheWriteTokens: 0 },
87446
88471
  ...repoDir && { repoDir },
87447
88472
  ...summary.gitBranch && { gitBranch: summary.gitBranch },
87448
88473
  ...repoFullName && { repoFullName },
87449
88474
  capturedVia: decision.via
87450
88475
  }
87451
- });
88476
+ };
88477
+ items.push(collected);
88478
+ links.noteCollected(thread.threadId, collected);
87452
88479
  } catch (error63) {
87453
- writer?.abandon();
87454
- unreadable.push(`${thread.harnessSessionId}: ${describeReadError(error63)}`);
87455
- observeFileDiagnostic(options.onFileDiagnostic, {
87456
- path: thread.files[0].rolloutPath,
87457
- reason: "unreadable",
87458
- sizeBytes: null,
87459
- errorCode: diagnosticErrorCode(error63)
87460
- });
88480
+ countReviewersLeftBehind(thread.threadId);
88481
+ links.forgetRecovery(thread.threadId);
88482
+ reportOnce(thread.files, thread.harnessSessionId, error63);
87461
88483
  }
87462
88484
  }
87463
88485
  return {
@@ -87466,11 +88488,33 @@ async function collectCodexSessions(options = {}) {
87466
88488
  skippedSubagentThreads,
87467
88489
  skippedTooLarge,
87468
88490
  unreadable,
87469
- heldBack: candidates.length - chosen.length,
88491
+ heldBack: ordered.length - chosen.length,
87470
88492
  mergedThreads,
87471
88493
  largestMergedBytes
87472
88494
  };
87473
88495
  }
88496
+ function sessionUsage(threads) {
88497
+ if (threads.some((thread) => thread.model !== void 0 && thread.totals === null)) return { totals: null, fastTotals: null };
88498
+ const measured = threads.filter((thread) => thread.totals !== null);
88499
+ if (measured.length === 0) return { totals: null, fastTotals: null };
88500
+ const add = (a, b2) => ({ input: a.input + b2.input, cached: a.cached + b2.cached, output: a.output + b2.output });
88501
+ const zero = { input: 0, cached: 0, output: 0 };
88502
+ return {
88503
+ totals: measured.map((thread) => thread.totals).reduce(add, zero),
88504
+ fastTotals: measured.every((thread) => thread.fastTotals !== null) ? measured.map((thread) => thread.fastTotals).reduce(add, zero) : null
88505
+ };
88506
+ }
88507
+ function leadRollout(threadFiles) {
88508
+ return threadFiles.find((file2) => !file2.identity.page) ?? threadFiles[0];
88509
+ }
88510
+ function newestChange(threadFiles) {
88511
+ return Math.max(...threadFiles.map((file2) => file2.changedMs));
88512
+ }
88513
+ function threadFilesKey(threadFiles) {
88514
+ return threadFiles.map((file2) => `${file2.rolloutPath}
88515
+ ${file2.mtimeMs}
88516
+ ${file2.sizeBytes}`).join("\n");
88517
+ }
87474
88518
  function dedupeStoreCopies(threadFiles) {
87475
88519
  const byName = /* @__PURE__ */ new Map();
87476
88520
  for (const file2 of threadFiles) {
@@ -87500,9 +88544,8 @@ function codexThreadCwd(threadId2, sessionsDir = path5.join(os6.homedir(), ".cod
87500
88544
  }
87501
88545
 
87502
88546
  // ../../apps/syncservice/src/cursorTranscriptFile.ts
87503
- import { createHash as createHash3 } from "node:crypto";
88547
+ import { createHash as createHash5 } from "node:crypto";
87504
88548
  import {
87505
- chmodSync as chmodSync3,
87506
88549
  closeSync as closeSync4,
87507
88550
  existsSync as existsSync3,
87508
88551
  lstatSync as lstatSync2,
@@ -87510,9 +88553,7 @@ import {
87510
88553
  openSync as openSync4,
87511
88554
  readSync as readSync3,
87512
88555
  readdirSync as readdirSync3,
87513
- realpathSync as realpathSync2,
87514
- utimesSync as utimesSync3,
87515
- writeFileSync as writeFileSync3
88556
+ realpathSync as realpathSync2
87516
88557
  } from "node:fs";
87517
88558
  import os8 from "node:os";
87518
88559
  import path7 from "node:path";
@@ -87569,8 +88610,8 @@ function unhandledObservations(shapes, externalId) {
87569
88610
  }
87570
88611
 
87571
88612
  // ../../apps/syncservice/src/cursorLocal.ts
87572
- import { createHash as createHash2 } from "node:crypto";
87573
- import { chmodSync as chmodSync2, existsSync as existsSync2, mkdirSync as mkdirSync3, utimesSync as utimesSync2, writeFileSync as writeFileSync2 } from "node:fs";
88613
+ import { createHash as createHash4 } from "node:crypto";
88614
+ import { chmodSync as chmodSync2, existsSync as existsSync2, mkdirSync as mkdirSync3, renameSync as renameSync2, rmSync as rmSync3, utimesSync, writeFileSync as writeFileSync2 } from "node:fs";
87574
88615
  import os7 from "node:os";
87575
88616
  import path6 from "node:path";
87576
88617
  import { fileURLToPath } from "node:url";
@@ -87644,9 +88685,20 @@ var cachedMetaSchema = external_exports.object({
87644
88685
  });
87645
88686
  function extractedTranscriptFileName(composerId) {
87646
88687
  const sanitized = composerId.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 80);
87647
- const hash2 = createHash2("sha256").update(composerId).digest("hex").slice(0, 8);
88688
+ const hash2 = createHash4("sha256").update(composerId).digest("hex").slice(0, 8);
87648
88689
  return `${sanitized}-${hash2}.jsonl`;
87649
88690
  }
88691
+ function replaceExtractedTranscript(transcriptPath, body, atime, mtime) {
88692
+ const tmpPath = `${transcriptPath}.${process.pid}.tmp`;
88693
+ try {
88694
+ writeFileSync2(tmpPath, body, { encoding: "utf8", mode: 384 });
88695
+ utimesSync(tmpPath, atime, mtime);
88696
+ renameSync2(tmpPath, transcriptPath);
88697
+ } catch (error63) {
88698
+ rmSync3(tmpPath, { force: true });
88699
+ throw error63;
88700
+ }
88701
+ }
87650
88702
  function workspaceFolderOf(cursorUserDir, workspaceId) {
87651
88703
  if (workspaceId === "" || workspaceId.includes(path6.sep) || workspaceId.includes("/") || workspaceId.includes("..")) {
87652
88704
  return void 0;
@@ -88143,9 +89195,7 @@ function collectFromStore(db, userDir, options, result, knownIds) {
88143
89195
  extractedTranscriptsDirReady = true;
88144
89196
  }
88145
89197
  const body = [JSON.stringify(meta3), ...transcript.serialized, ""].join("\n");
88146
- if (extractedTranscriptStat !== null) chmodSync2(transcriptUploadSourcePath, 384);
88147
- writeFileSync2(transcriptUploadSourcePath, body, { encoding: "utf8", mode: 384 });
88148
- utimesSync2(transcriptUploadSourcePath, new Date(lastUpdatedAt), new Date(lastUpdatedAt));
89198
+ replaceExtractedTranscript(transcriptUploadSourcePath, body, new Date(lastUpdatedAt), new Date(lastUpdatedAt));
88149
89199
  }
88150
89200
  if (hasCaptureGaps(omissions)) result.partiallyInterpreted += 1;
88151
89201
  for (const observation of unhandledObservations(unhandled, raw.composerId)) {
@@ -88358,7 +89408,7 @@ function cursorTranscriptRecordsDir(home) {
88358
89408
  function cursorTranscriptRecordPath(home, sessionId) {
88359
89409
  return path7.join(
88360
89410
  cursorTranscriptRecordsDir(home),
88361
- `${createHash3("sha256").update(sessionId).digest("hex")}.json`
89411
+ `${createHash5("sha256").update(sessionId).digest("hex")}.json`
88362
89412
  );
88363
89413
  }
88364
89414
  function collectCursorTranscriptFiles(options, isDatabaseSession, result, databaseExtractedTranscriptsDir, extractedTranscriptFileName2) {
@@ -88381,7 +89431,7 @@ function collectCursorTranscriptFiles(options, isDatabaseSession, result, databa
88381
89431
  if (!lstatSync2(recordPath).isFile()) continue;
88382
89432
  const text = readBounded(recordPath, 40 * 1024);
88383
89433
  const record2 = transcriptDiscoveryRecordSchema2.parse(text === null ? null : JSON.parse(text));
88384
- if (file2 !== `${createHash3("sha256").update(record2.sessionId).digest("hex")}.json`) continue;
89434
+ if (file2 !== `${createHash5("sha256").update(record2.sessionId).digest("hex")}.json`) continue;
88385
89435
  if (isDatabaseSession(record2.sessionId) || existsSync3(path7.join(databaseExtractedTranscriptsDir, extractedTranscriptFileName2(record2.sessionId))))
88386
89436
  continue;
88387
89437
  if (record2.transcriptPath === null || record2.workspaceRoots.length !== 1) continue;
@@ -88475,9 +89525,7 @@ function collectCursorTranscriptFiles(options, isDatabaseSession, result, databa
88475
89525
  continue;
88476
89526
  }
88477
89527
  mkdirSync4(nativeExtractedTranscriptsDir, { recursive: true, mode: 448 });
88478
- if (extractedTranscriptStat !== null) chmodSync3(transcriptUploadSourcePath, 384);
88479
- writeFileSync3(transcriptUploadSourcePath, body, { mode: 384 });
88480
- utimesSync3(transcriptUploadSourcePath, stat.atime, stat.mtime);
89528
+ replaceExtractedTranscript(transcriptUploadSourcePath, body, stat.atime, stat.mtime);
88481
89529
  omissions = Object.keys(written).length > 0 ? written : void 0;
88482
89530
  unhandled = prepared.unhandled;
88483
89531
  }
@@ -88504,7 +89552,7 @@ function collectCursorTranscriptFiles(options, isDatabaseSession, result, databa
88504
89552
  }
88505
89553
 
88506
89554
  // ../../apps/syncservice/src/hookInstall.ts
88507
- import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
89555
+ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "node:fs";
88508
89556
  import os9 from "node:os";
88509
89557
  import path8 from "node:path";
88510
89558
  import { fileURLToPath as fileURLToPath2 } from "node:url";
@@ -88597,7 +89645,7 @@ function installCollectorHooks(homeDir = os9.homedir()) {
88597
89645
  const hookDir = path8.join(homeDir, ".config", "powerplant", "hooks");
88598
89646
  mkdirSync5(hookDir, { recursive: true });
88599
89647
  for (const script of HOOK_SCRIPTS) {
88600
- writeFileSync4(path8.join(hookDir, script), readFileSync5(hookSourcePath(script), "utf8"), {
89648
+ writeFileSync3(path8.join(hookDir, script), readFileSync6(hookSourcePath(script), "utf8"), {
88601
89649
  mode: 493
88602
89650
  });
88603
89651
  }
@@ -88605,18 +89653,18 @@ function installCollectorHooks(homeDir = os9.homedir()) {
88605
89653
  const settingsPath = claudeSettingsPath(homeDir);
88606
89654
  let settingsText;
88607
89655
  try {
88608
- settingsText = readFileSync5(settingsPath, "utf8");
89656
+ settingsText = readFileSync6(settingsPath, "utf8");
88609
89657
  } catch {
88610
89658
  settingsText = null;
88611
89659
  }
88612
89660
  const merged = mergeCollectorHooks(settingsText, (script) => `node "${path8.join(hookDir, script)}"`);
88613
89661
  if (merged.action === "write") {
88614
89662
  mkdirSync5(settingsDir, { recursive: true });
88615
- writeFileSync4(settingsPath, merged.text);
89663
+ writeFileSync3(settingsPath, merged.text);
88616
89664
  }
88617
89665
  if (merged.action !== "skip") {
88618
89666
  for (const retired of RETIRED_HOOK_WIRINGS) {
88619
- rmSync3(path8.join(hookDir, retired.script), { force: true });
89667
+ rmSync4(path8.join(hookDir, retired.script), { force: true });
88620
89668
  }
88621
89669
  }
88622
89670
  return {
@@ -88627,7 +89675,7 @@ function installCollectorHooks(homeDir = os9.homedir()) {
88627
89675
  }
88628
89676
 
88629
89677
  // ../../apps/syncservice/src/lifecycleJournal.ts
88630
- import { closeSync as closeSync5, fstatSync, openSync as openSync5, readdirSync as readdirSync4, readSync as readSync4, rmSync as rmSync4 } from "node:fs";
89678
+ import { closeSync as closeSync5, fstatSync as fstatSync2, openSync as openSync5, readdirSync as readdirSync4, readSync as readSync4, rmSync as rmSync5 } from "node:fs";
88631
89679
  import path9 from "node:path";
88632
89680
  var LIFECYCLE_JOURNAL_DIRNAME = "lifecycle-journal";
88633
89681
  var LIFECYCLE_JOURNAL_RETENTION_DAYS = 7;
@@ -88694,7 +89742,7 @@ function readAppendedLines(file2, offset) {
88694
89742
  throw error63;
88695
89743
  }
88696
89744
  try {
88697
- const size = fstatSync(fd).size;
89745
+ const size = fstatSync2(fd).size;
88698
89746
  const start = size < offset ? 0 : offset;
88699
89747
  const buffer = Buffer.alloc(size - start);
88700
89748
  const bytes = readSync4(fd, buffer, 0, buffer.length, start);
@@ -88752,10 +89800,10 @@ function createLifecycleJournalReader(home, harnesses = COLLECTOR_SESSION_HARNES
88752
89800
  deleteExpired(nowMs = Date.now()) {
88753
89801
  const floorMs = lifecycleJournalRetentionFloorMs(nowMs);
88754
89802
  for (const file2 of allDayFiles()) {
88755
- if (Date.parse(`${path9.basename(file2).slice(0, 10)}T00:00:00Z`) < floorMs) rmSync4(file2, { force: true });
89803
+ if (Date.parse(`${path9.basename(file2).slice(0, 10)}T00:00:00Z`) < floorMs) rmSync5(file2, { force: true });
88756
89804
  }
88757
89805
  if (legacyFile !== null && (statIfExists(legacyFile)?.mtimeMs ?? floorMs) < floorMs) {
88758
- rmSync4(legacyFile, { force: true });
89806
+ rmSync5(legacyFile, { force: true });
88759
89807
  }
88760
89808
  for (const held of [latest2, latestBoundary]) {
88761
89809
  for (const [key, entry] of held) {
@@ -88779,10 +89827,10 @@ import {
88779
89827
  existsSync as existsSync5,
88780
89828
  lstatSync as lstatSync3,
88781
89829
  mkdirSync as mkdirSync6,
88782
- readFileSync as readFileSync6,
88783
- renameSync as renameSync2,
88784
- rmSync as rmSync5,
88785
- writeFileSync as writeFileSync5
89830
+ readFileSync as readFileSync7,
89831
+ renameSync as renameSync3,
89832
+ rmSync as rmSync6,
89833
+ writeFileSync as writeFileSync4
88786
89834
  } from "node:fs";
88787
89835
  import path10 from "node:path";
88788
89836
  import { randomUUID } from "node:crypto";
@@ -88887,7 +89935,7 @@ function installHarnessHook(harness, home) {
88887
89935
  return { provider: nativeName, settings: "skip", detail: NON_REGULAR_CONFIG };
88888
89936
  }
88889
89937
  mode = stat.mode & 511;
88890
- original = readFileSync6(configPath, "utf8");
89938
+ original = readFileSync7(configPath, "utf8");
88891
89939
  } catch (error63) {
88892
89940
  if (!(error63 instanceof Error && "code" in error63 && error63.code === "ENOENT")) throw error63;
88893
89941
  }
@@ -88903,9 +89951,9 @@ function installHarnessHook(harness, home) {
88903
89951
  return { provider: nativeName, settings: "skip", detail: merged.reason };
88904
89952
  }
88905
89953
  mkdirSync6(hookDir, { recursive: true });
88906
- writeAtomic(scriptPath, readFileSync6(hookSourcePath(script), "utf8"), 448);
89954
+ writeAtomic(scriptPath, readFileSync7(hookSourcePath(script), "utf8"), 448);
88907
89955
  if (merged.action === "write") {
88908
- const current = existsSync5(configPath) ? readFileSync6(configPath, "utf8") : null;
89956
+ const current = existsSync5(configPath) ? readFileSync7(configPath, "utf8") : null;
88909
89957
  if (current !== original)
88910
89958
  return {
88911
89959
  provider: nativeName,
@@ -88919,10 +89967,10 @@ function installHarnessHook(harness, home) {
88919
89967
  function writeAtomic(file2, text, mode) {
88920
89968
  const temporary = `${file2}.${randomUUID()}.tmp`;
88921
89969
  try {
88922
- writeFileSync5(temporary, text, { mode, flag: "wx" });
88923
- renameSync2(temporary, file2);
89970
+ writeFileSync4(temporary, text, { mode, flag: "wx" });
89971
+ renameSync3(temporary, file2);
88924
89972
  } finally {
88925
- rmSync5(temporary, { force: true });
89973
+ rmSync6(temporary, { force: true });
88926
89974
  }
88927
89975
  }
88928
89976
  function installHarnessHooks(home) {
@@ -89287,8 +90335,8 @@ function controlFilePath(appEnv, homeDir) {
89287
90335
  function writeAtomically(file2, payload) {
89288
90336
  mkdirSync7(path12.dirname(file2), { recursive: true });
89289
90337
  const tmp = `${file2}.${process.pid}.tmp`;
89290
- writeFileSync6(tmp, payload);
89291
- renameSync3(tmp, file2);
90338
+ writeFileSync5(tmp, payload);
90339
+ renameSync4(tmp, file2);
89292
90340
  }
89293
90341
  function writeCollectorStatus(appEnv, status, homeDir) {
89294
90342
  writeAtomically(statusFilePath(appEnv, homeDir), JSON.stringify(status, null, 2));
@@ -89339,7 +90387,7 @@ function menuOpenRequestPath(appEnv, homeDir) {
89339
90387
  function writeRefreshRequest(menuDir) {
89340
90388
  const dir = path12.join(menuDir, MENU_REFRESH_DIRNAME);
89341
90389
  if (!existsSync7(dir)) return;
89342
- writeFileSync6(path12.join(dir, "request"), (/* @__PURE__ */ new Date()).toISOString(), { mode: 384 });
90390
+ writeFileSync5(path12.join(dir, "request"), (/* @__PURE__ */ new Date()).toISOString(), { mode: 384 });
89343
90391
  }
89344
90392
  function menuShownStatus(status) {
89345
90393
  const passFailed = status.lastPass?.ok === false;
@@ -89408,7 +90456,7 @@ function carriedUploadClock(previous, machineId) {
89408
90456
  function readCollectorStatus(appEnv, homeDir) {
89409
90457
  let raw;
89410
90458
  try {
89411
- raw = readFileSync7(statusFilePath(appEnv, homeDir), "utf8");
90459
+ raw = readFileSync8(statusFilePath(appEnv, homeDir), "utf8");
89412
90460
  } catch {
89413
90461
  return { file: "missing" };
89414
90462
  }
@@ -89424,7 +90472,7 @@ function readCollectorStatus(appEnv, homeDir) {
89424
90472
  function readCollectorControl(appEnv, homeDir) {
89425
90473
  let raw;
89426
90474
  try {
89427
- raw = readFileSync7(controlFilePath(appEnv, homeDir), "utf8");
90475
+ raw = readFileSync8(controlFilePath(appEnv, homeDir), "utf8");
89428
90476
  } catch {
89429
90477
  return { paused: false };
89430
90478
  }
@@ -89441,10 +90489,10 @@ function writeCollectorControl(appEnv, control, homeDir) {
89441
90489
  writeAtomically(controlFilePath(appEnv, homeDir), JSON.stringify(control, null, 2));
89442
90490
  }
89443
90491
  function clearCollectorControl(appEnv, homeDir) {
89444
- rmSync6(controlFilePath(appEnv, homeDir), { force: true });
90492
+ rmSync7(controlFilePath(appEnv, homeDir), { force: true });
89445
90493
  }
89446
90494
  function clearCollectorStatus(appEnv, homeDir) {
89447
- rmSync6(statusFilePath(appEnv, homeDir), { force: true });
90495
+ rmSync7(statusFilePath(appEnv, homeDir), { force: true });
89448
90496
  }
89449
90497
  function describeDaemonState(read, probes = {}) {
89450
90498
  if (read.file === "invalid") return { state: "unreadable" };
@@ -89457,6 +90505,9 @@ function describeDaemonState(read, probes = {}) {
89457
90505
  }
89458
90506
  return { state: "running", status: read.status };
89459
90507
  }
90508
+ function backgroundSyncIsRunning(appEnv, homeDir) {
90509
+ return describeDaemonState(readCollectorStatus(appEnv, homeDir)).state === "running";
90510
+ }
89460
90511
  function isPidAlive(pid) {
89461
90512
  try {
89462
90513
  process.kill(pid, 0);
@@ -89584,12 +90635,12 @@ var MacosCredentialStore = class {
89584
90635
  try {
89585
90636
  const fileFd = openSync6(temporary, "wx", 384);
89586
90637
  try {
89587
- writeFileSync7(fileFd, envelope);
90638
+ writeFileSync6(fileFd, envelope);
89588
90639
  fsyncSync(fileFd);
89589
90640
  } finally {
89590
90641
  closeSync6(fileFd);
89591
90642
  }
89592
- renameSync4(temporary, this.file);
90643
+ renameSync5(temporary, this.file);
89593
90644
  const directoryFd = openSync6(this.directory, "r");
89594
90645
  try {
89595
90646
  fsyncSync(directoryFd);
@@ -89809,8 +90860,8 @@ function saveCredentials(appEnv, credentials) {
89809
90860
  }
89810
90861
  const file2 = fallbackFile(appEnv);
89811
90862
  mkdirSync9(path14.dirname(file2), { recursive: true });
89812
- writeFileSync8(file2, payload, { mode: 384 });
89813
- chmodSync4(file2, 384);
90863
+ writeFileSync7(file2, payload, { mode: 384 });
90864
+ chmodSync3(file2, 384);
89814
90865
  return file2;
89815
90866
  }
89816
90867
  function loadCredentials(appEnv) {
@@ -89820,7 +90871,7 @@ function loadCredentials(appEnv) {
89820
90871
  }
89821
90872
  let raw;
89822
90873
  try {
89823
- raw = readFileSync8(fallbackFile(appEnv), "utf8");
90874
+ raw = readFileSync9(fallbackFile(appEnv), "utf8");
89824
90875
  } catch (cause) {
89825
90876
  if (isCredentialStoreAbsent(cause)) return null;
89826
90877
  throw new Error(
@@ -89855,13 +90906,407 @@ function fallbackFile(appEnv) {
89855
90906
 
89856
90907
  // ../../apps/syncservice/src/enroll.ts
89857
90908
  import { spawn } from "node:child_process";
89858
- import { createHash as createHash4, randomBytes as randomBytes2 } from "node:crypto";
90909
+ import { createHash as createHash6, randomBytes as randomBytes2 } from "node:crypto";
89859
90910
  import http5 from "node:http";
89860
- import os13 from "node:os";
90911
+ import os14 from "node:os";
89861
90912
 
89862
90913
  // ../../apps/syncservice/src/upload.ts
89863
- import { readFileSync as readFileSync9 } from "node:fs";
90914
+ import { readFileSync as readFileSync10 } from "node:fs";
89864
90915
  import { gzipSync } from "node:zlib";
90916
+
90917
+ // ../../apps/syncservice/src/claudeCode.ts
90918
+ import { readdirSync as readdirSync6 } from "node:fs";
90919
+ import os13 from "node:os";
90920
+ import path15 from "node:path";
90921
+ import { setImmediate as yieldToEventLoop2 } from "node:timers/promises";
90922
+ function parseClaudeCodeTranscript(text) {
90923
+ const usageByMessage = /* @__PURE__ */ new Map();
90924
+ const modelCounts = /* @__PURE__ */ new Map();
90925
+ const timestamps = [];
90926
+ let title;
90927
+ let cwd;
90928
+ let gitBranch;
90929
+ let entrypoint;
90930
+ let anonCounter = 0;
90931
+ let speedMode;
90932
+ for (const record2 of parseJsonl(text)) {
90933
+ if (record2.type === "custom-title" && typeof record2.customTitle === "string") {
90934
+ title = record2.customTitle;
90935
+ continue;
90936
+ }
90937
+ if (record2.type === "ai-title" && typeof record2.aiTitle === "string") {
90938
+ title = record2.aiTitle;
90939
+ continue;
90940
+ }
90941
+ if (cwd === void 0 && typeof record2.cwd === "string" && record2.cwd !== "") cwd = record2.cwd;
90942
+ if (typeof record2.entrypoint === "string" && record2.entrypoint !== "") {
90943
+ entrypoint ??= record2.entrypoint;
90944
+ }
90945
+ if (typeof record2.gitBranch === "string" && record2.gitBranch !== "") {
90946
+ gitBranch ??= record2.gitBranch;
90947
+ }
90948
+ if (typeof record2.timestamp === "string") timestamps.push(record2.timestamp);
90949
+ if (record2.type !== "assistant") continue;
90950
+ const message2 = record2.message;
90951
+ if (!message2 || message2.model === "<synthetic>") continue;
90952
+ const id = message2.id ?? (typeof record2.requestId === "string" ? record2.requestId : void 0) ?? (typeof record2.uuid === "string" ? record2.uuid : void 0) ?? `anon-${anonCounter++}`;
90953
+ const previousMessage = usageByMessage.get(id);
90954
+ const isNewMessage = previousMessage === void 0;
90955
+ const speedValue = message2.usage?.speed;
90956
+ const observedSpeed = speedModeOf(speedValue);
90957
+ const messageSpeed = observedSpeed ?? previousMessage?.speedMode;
90958
+ if (speedValue !== void 0) speedMode = observedSpeed ?? "unknown";
90959
+ if (message2.usage) {
90960
+ usageByMessage.set(id, {
90961
+ usage: toUsage(message2.usage),
90962
+ ...messageSpeed && { speedMode: messageSpeed }
90963
+ });
90964
+ } else if (isNewMessage) {
90965
+ usageByMessage.set(id, {
90966
+ usage: { input: 0, cacheRead: 0, cacheCreation: 0, output: 0 }
90967
+ });
90968
+ }
90969
+ if (isNewMessage && typeof message2.model === "string") {
90970
+ modelCounts.set(message2.model, (modelCounts.get(message2.model) ?? 0) + 1);
90971
+ }
90972
+ }
90973
+ if (usageByMessage.size === 0 && timestamps.length === 0) return null;
90974
+ const summed = [...usageByMessage.values()].map((entry) => entry.usage).reduce(addUsage, {
90975
+ input: 0,
90976
+ cacheRead: 0,
90977
+ cacheCreation: 0,
90978
+ output: 0
90979
+ });
90980
+ const speedFullyMeasured = usageByMessage.size > 0 && [...usageByMessage.values()].every((entry) => entry.speedMode !== void 0);
90981
+ const fastUsage = speedFullyMeasured ? [...usageByMessage.values()].filter((entry) => entry.speedMode === "fast").map((entry) => entry.usage).reduce(addUsage, { input: 0, cacheRead: 0, cacheCreation: 0, output: 0 }) : null;
90982
+ return {
90983
+ startedAt: timestamps[0],
90984
+ endedAt: timestamps[timestamps.length - 1],
90985
+ model: dominantKey(modelCounts),
90986
+ usage: usageByMessage.size > 0 ? summed : null,
90987
+ fastUsage,
90988
+ ...speedMode && { speedMode },
90989
+ title,
90990
+ turnCount: usageByMessage.size,
90991
+ cwd,
90992
+ gitBranch,
90993
+ entrypoint
90994
+ };
90995
+ }
90996
+ var AGENT_SDK_ENTRYPOINTS = /* @__PURE__ */ new Set(["sdk-py", "sdk-ts"]);
90997
+ function isAutomationEntrypoint(entrypoint) {
90998
+ return entrypoint !== void 0 && AGENT_SDK_ENTRYPOINTS.has(entrypoint);
90999
+ }
91000
+ function sessionKey(identity) {
91001
+ return JSON.stringify([identity.harness, identity.harnessSessionId]);
91002
+ }
91003
+ async function collectClaudeCodeSessions(options = {}) {
91004
+ const claudeDir = options.claudeDir ?? path15.join(os13.homedir(), ".claude");
91005
+ const now = options.now ?? Date.now();
91006
+ const capture2 = options.capture ?? captureEverythingUnresolved;
91007
+ const maxBytes = options.maxTranscriptBytes ?? MAX_TRANSCRIPT_FILE_BYTES;
91008
+ const projectsDir = path15.join(claudeDir, "projects");
91009
+ const items = [];
91010
+ const skips = { skippedOutOfScope: 0, skippedNoRepo: 0 };
91011
+ const candidates = [];
91012
+ const unreadable = [];
91013
+ let skippedTooLarge = 0;
91014
+ const home = path15.dirname(claudeDir);
91015
+ const lifecycle = options.lifecycle ?? readLifecycleJournal(home, now, ["claude-local"]);
91016
+ for (const projectDir of listDirs(projectsDir)) {
91017
+ for (const entry of listFiles(projectDir, ".jsonl")) {
91018
+ await yieldToEventLoop2();
91019
+ const transcriptPath = path15.join(projectDir, entry);
91020
+ const stat = statIfExists(transcriptPath);
91021
+ if (!stat) continue;
91022
+ const sessionId = path15.basename(entry, ".jsonl");
91023
+ const subagents = options.locationsOnly ? NO_SUBAGENTS : listSubagents(path15.join(projectDir, sessionId, "subagents"));
91024
+ const mtimeMs = Math.max(stat.mtimeMs, subagents.workflows.mtimeMs, ...subagents.files.map((file2) => file2.mtimeMs ?? 0));
91025
+ if (options.sinceMs !== void 0 && mtimeMs < options.sinceMs) continue;
91026
+ if (stat.size > maxBytes) {
91027
+ skippedTooLarge += 1;
91028
+ observeFileDiagnostic(options.onFileDiagnostic, {
91029
+ path: transcriptPath,
91030
+ reason: "too_large",
91031
+ sizeBytes: stat.size
91032
+ });
91033
+ continue;
91034
+ }
91035
+ const endObservedAt = observedSessionEnd(lifecycle.latestBoundary("claude-local", sessionId), mtimeMs);
91036
+ const status = endObservedAt === null && now - mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive";
91037
+ try {
91038
+ const cwd = transcriptCwd(transcriptPath);
91039
+ const decision = captureDecision(cwd, capture2);
91040
+ if (!decision.capture) {
91041
+ countCaptureSkip(skips, decision.reason);
91042
+ continue;
91043
+ }
91044
+ if (options.locationsOnly) {
91045
+ items.push({
91046
+ mtimeMs: stat.mtimeMs,
91047
+ transcriptPath,
91048
+ ...cwd && { cwd },
91049
+ item: { harness: "claude-local", harnessSessionId: sessionId, status, capturedVia: decision.via }
91050
+ });
91051
+ continue;
91052
+ }
91053
+ candidates.push({
91054
+ transcriptPath,
91055
+ sessionId,
91056
+ mtimeMs,
91057
+ subagents,
91058
+ status,
91059
+ endObservedAt,
91060
+ ...cwd && { cwd },
91061
+ decision
91062
+ });
91063
+ } catch (error63) {
91064
+ unreadable.push(`${sessionId}: ${describeReadError(error63)}`);
91065
+ observeFileDiagnostic(options.onFileDiagnostic, {
91066
+ path: transcriptPath,
91067
+ reason: "unreadable",
91068
+ sizeBytes: null,
91069
+ errorCode: diagnosticErrorCode(error63)
91070
+ });
91071
+ }
91072
+ }
91073
+ }
91074
+ if (options.locationsOnly) {
91075
+ return {
91076
+ sessions: items.sort((a, b2) => b2.mtimeMs - a.mtimeMs),
91077
+ ...skips,
91078
+ skippedTooLarge,
91079
+ unreadable,
91080
+ heldBack: 0
91081
+ };
91082
+ }
91083
+ candidates.sort((a, b2) => b2.mtimeMs - a.mtimeMs);
91084
+ const chosen = options.maxSessions === void 0 ? candidates : candidates.slice(0, options.maxSessions);
91085
+ for (const candidate of chosen) {
91086
+ await yieldToEventLoop2();
91087
+ const { transcriptPath, sessionId, mtimeMs, subagents, status, endObservedAt, cwd, decision } = candidate;
91088
+ try {
91089
+ const transcript = readTranscriptIfExists(transcriptPath);
91090
+ if (transcript === null) continue;
91091
+ const summary = parseClaudeCodeTranscript(transcript.text);
91092
+ if (!summary) continue;
91093
+ const usage = addSubagentUsage(
91094
+ summary.usage,
91095
+ summary.fastUsage,
91096
+ subagents.files,
91097
+ { maxBytes, sessionId, onFileDiagnostic: options.onFileDiagnostic }
91098
+ );
91099
+ skippedTooLarge += usage.skippedTooLarge;
91100
+ unreadable.push(...usage.unreadable);
91101
+ const endedAt = endObservedAt ?? laterTimestamp(summary.endedAt, usage.endedAt);
91102
+ items.push({
91103
+ mtimeMs,
91104
+ transcriptPath,
91105
+ transcriptPrefix: transcript.prefix,
91106
+ subagents: {
91107
+ files: usage.capture.files,
91108
+ skipped: usage.capture.skipped + subagents.workflows.count,
91109
+ skippedUnreadable: usage.capture.skippedUnreadable + subagents.workflows.unreadable
91110
+ },
91111
+ ...cwd && { cwd },
91112
+ item: {
91113
+ harness: "claude-local",
91114
+ harnessSessionId: sessionId,
91115
+ status,
91116
+ ...summary.startedAt && { startedAt: summary.startedAt },
91117
+ ...endedAt && { endedAt },
91118
+ ...summary.model && { model: summary.model },
91119
+ ...summary.speedMode && { speedMode: summary.speedMode },
91120
+ // Total input-class tokens the source reported (uncached + cache
91121
+ // reads + cache writes) — a real reported total, not an estimate.
91122
+ // The cache breakdown rides along: pricing bills reads at 0.1×
91123
+ // and writes at 1.25×, so collapsing it would inflate estimates.
91124
+ ...usage.total && {
91125
+ inputTokens: usage.total.input + usage.total.cacheRead + usage.total.cacheCreation
91126
+ },
91127
+ ...usage.total && { outputTokens: usage.total.output },
91128
+ ...usage.total && { cacheReadTokens: usage.total.cacheRead },
91129
+ ...usage.total && { cacheWriteTokens: usage.total.cacheCreation },
91130
+ ...usage.fast && {
91131
+ fastInputTokens: usage.fast.input + usage.fast.cacheRead + usage.fast.cacheCreation,
91132
+ fastOutputTokens: usage.fast.output,
91133
+ fastCacheReadTokens: usage.fast.cacheRead,
91134
+ fastCacheWriteTokens: usage.fast.cacheCreation
91135
+ },
91136
+ ...summary.title && { title: redactText(summary.title).slice(0, 500) },
91137
+ ...isAutomationEntrypoint(summary.entrypoint) && { automation: true },
91138
+ // The PR-join coordinates: the checkout's directory name (bare
91139
+ // repo, not the local path) and the branch the work started on.
91140
+ ...cwd && { repoDir: path15.basename(cwd) },
91141
+ ...summary.gitBranch && { gitBranch: summary.gitBranch },
91142
+ ...decision.repoFullName && { repoFullName: decision.repoFullName },
91143
+ capturedVia: decision.via
91144
+ }
91145
+ });
91146
+ } catch (error63) {
91147
+ unreadable.push(`${sessionId}: ${describeReadError(error63)}`);
91148
+ observeFileDiagnostic(options.onFileDiagnostic, {
91149
+ path: transcriptPath,
91150
+ reason: "unreadable",
91151
+ sizeBytes: null,
91152
+ errorCode: diagnosticErrorCode(error63)
91153
+ });
91154
+ }
91155
+ }
91156
+ return {
91157
+ sessions: items.sort((a, b2) => b2.mtimeMs - a.mtimeMs),
91158
+ ...skips,
91159
+ skippedTooLarge,
91160
+ unreadable,
91161
+ heldBack: candidates.length - chosen.length
91162
+ };
91163
+ }
91164
+ function transcriptCwd(transcriptPath) {
91165
+ const record2 = readFirstRecord(
91166
+ transcriptPath,
91167
+ (candidate) => typeof candidate.cwd === "string" && candidate.cwd !== ""
91168
+ );
91169
+ return typeof record2?.cwd === "string" ? record2.cwd : void 0;
91170
+ }
91171
+ function toUsage(u) {
91172
+ const n = (value) => typeof value === "number" ? value : 0;
91173
+ return {
91174
+ input: n(u.input_tokens),
91175
+ cacheRead: n(u.cache_read_input_tokens),
91176
+ cacheCreation: n(u.cache_creation_input_tokens),
91177
+ output: n(u.output_tokens)
91178
+ };
91179
+ }
91180
+ function speedModeOf(value) {
91181
+ return value === "fast" || value === "standard" ? value : void 0;
91182
+ }
91183
+ function addUsage(a, b2) {
91184
+ return {
91185
+ input: a.input + b2.input,
91186
+ cacheRead: a.cacheRead + b2.cacheRead,
91187
+ cacheCreation: a.cacheCreation + b2.cacheCreation,
91188
+ output: a.output + b2.output
91189
+ };
91190
+ }
91191
+ var NO_SUBAGENTS = { files: [], workflows: { count: 0, unreadable: 0, mtimeMs: 0 } };
91192
+ function listSubagents(subagentsDir) {
91193
+ const entries = directoryEntries(subagentsDir);
91194
+ if (!Array.isArray(entries)) {
91195
+ return { ...NO_SUBAGENTS, files: [{ entry: "subagents/", path: subagentsDir, mtimeMs: null, size: null, statError: entries.error }] };
91196
+ }
91197
+ const names = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => entry.name).sort();
91198
+ const files = names.flatMap((entry) => {
91199
+ const filePath = path15.join(subagentsDir, entry);
91200
+ try {
91201
+ const stat = statIfExists(filePath);
91202
+ return stat === null ? [] : [{ entry, path: filePath, mtimeMs: stat.mtimeMs, size: stat.size, statError: null }];
91203
+ } catch (statError) {
91204
+ return [{ entry, path: filePath, mtimeMs: null, size: null, statError }];
91205
+ }
91206
+ });
91207
+ const hasWorkflows = entries.some((entry) => entry.isDirectory() && entry.name === "workflows");
91208
+ return { files, workflows: hasWorkflows ? listWorkflowTranscripts(path15.join(subagentsDir, "workflows")) : NO_SUBAGENTS.workflows };
91209
+ }
91210
+ function listWorkflowTranscripts(workflowsDir) {
91211
+ const found = { count: 0, unreadable: 0, mtimeMs: 0 };
91212
+ const failedToRead = () => {
91213
+ found.count += 1;
91214
+ found.unreadable += 1;
91215
+ };
91216
+ const list = (dir) => {
91217
+ const entries = directoryEntries(dir);
91218
+ if (Array.isArray(entries)) return entries;
91219
+ failedToRead();
91220
+ return [];
91221
+ };
91222
+ for (const run of list(workflowsDir)) {
91223
+ if (!run.isDirectory()) continue;
91224
+ const runDir = path15.join(workflowsDir, run.name);
91225
+ for (const file2 of list(runDir)) {
91226
+ if (!file2.isFile() || !file2.name.endsWith(".jsonl")) continue;
91227
+ try {
91228
+ const stat = statIfExists(path15.join(runDir, file2.name));
91229
+ if (stat === null) continue;
91230
+ found.count += 1;
91231
+ found.mtimeMs = Math.max(found.mtimeMs, stat.mtimeMs);
91232
+ } catch {
91233
+ failedToRead();
91234
+ }
91235
+ }
91236
+ }
91237
+ return found;
91238
+ }
91239
+ function directoryEntries(dir) {
91240
+ try {
91241
+ return readdirSync6(dir, { withFileTypes: true });
91242
+ } catch (error63) {
91243
+ const code = error63.code;
91244
+ return code === "ENOENT" || code === "ENOTDIR" ? [] : { error: error63 };
91245
+ }
91246
+ }
91247
+ function subagentPart(entry) {
91248
+ const match = /^agent-(.+)\.jsonl$/.exec(entry);
91249
+ return match !== null && RAW_TRANSCRIPT_PART_PATTERN.test(match[1]) ? match[1] : null;
91250
+ }
91251
+ function addSubagentUsage(main2, mainFast, subagentFiles, bounds) {
91252
+ let total = main2;
91253
+ let fast = mainFast;
91254
+ let fastCoverageComplete = main2 === null || mainFast !== null;
91255
+ let skippedTooLarge = 0;
91256
+ const unreadable = [];
91257
+ const capture2 = { files: [], skipped: 0, skippedUnreadable: 0 };
91258
+ let endedAt;
91259
+ for (const file2 of subagentFiles) {
91260
+ const { entry, path: sidecarPath } = file2;
91261
+ try {
91262
+ if (file2.size === null) throw file2.statError;
91263
+ const { size } = file2;
91264
+ if (size > bounds.maxBytes) {
91265
+ skippedTooLarge += 1;
91266
+ capture2.skipped += 1;
91267
+ observeFileDiagnostic(bounds.onFileDiagnostic, { path: sidecarPath, reason: "too_large", sizeBytes: size });
91268
+ continue;
91269
+ }
91270
+ const transcript = readTranscriptIfExists(sidecarPath);
91271
+ if (transcript === null) {
91272
+ continue;
91273
+ }
91274
+ const part = subagentPart(entry);
91275
+ if (part === null) capture2.skipped += 1;
91276
+ else capture2.files.push({ part, path: sidecarPath, prefix: transcript.prefix });
91277
+ const summary = parseClaudeCodeTranscript(transcript.text);
91278
+ endedAt = laterTimestamp(endedAt, summary?.endedAt);
91279
+ if (summary?.usage) {
91280
+ total = addUsage(total ?? { input: 0, cacheRead: 0, cacheCreation: 0, output: 0 }, summary.usage);
91281
+ if (summary.fastUsage === null) fastCoverageComplete = false;
91282
+ }
91283
+ if (summary?.fastUsage) {
91284
+ fast = addUsage(fast ?? { input: 0, cacheRead: 0, cacheCreation: 0, output: 0 }, summary.fastUsage);
91285
+ }
91286
+ } catch (error63) {
91287
+ capture2.skipped += 1;
91288
+ capture2.skippedUnreadable += 1;
91289
+ unreadable.push(`${bounds.sessionId} (subagent ${entry}): ${describeReadError(error63)}`);
91290
+ observeFileDiagnostic(bounds.onFileDiagnostic, {
91291
+ path: sidecarPath,
91292
+ reason: "unreadable",
91293
+ sizeBytes: null,
91294
+ errorCode: diagnosticErrorCode(error63)
91295
+ });
91296
+ }
91297
+ }
91298
+ return { capture: capture2, total, fast: fastCoverageComplete ? fast : null, skippedTooLarge, unreadable, endedAt };
91299
+ }
91300
+ function claudeCodeSessionCwd(sessionId, claudeDir = path15.join(os13.homedir(), ".claude")) {
91301
+ if (!SESSION_UUID.test(sessionId)) return void 0;
91302
+ for (const projectDir of listDirs(path15.join(claudeDir, "projects"))) {
91303
+ const cwd = transcriptCwd(path15.join(projectDir, `${sessionId}.jsonl`));
91304
+ if (cwd !== void 0) return cwd;
91305
+ }
91306
+ return void 0;
91307
+ }
91308
+
91309
+ // ../../apps/syncservice/src/upload.ts
89865
91310
  function parseApiResponse(schema, body, route) {
89866
91311
  const parsed = schema.safeParse(body);
89867
91312
  if (!parsed.success) {
@@ -90048,10 +91493,16 @@ async function uploadPreviewRegistration(apiUrl, machineToken, registration) {
90048
91493
  function encodeRawTranscript(text) {
90049
91494
  return gzipSync(redactJsonl(text));
90050
91495
  }
91496
+ function readCollectedTranscript(file2, prefix) {
91497
+ return prefix === void 0 ? readFileSync10(file2, "utf8") : readTranscriptPrefix(file2, prefix, false);
91498
+ }
91499
+ function readUnchangedTranscript(file2, prefix) {
91500
+ return prefix === void 0 ? readFileSync10(file2, "utf8") : readTranscriptPrefix(file2, prefix, true);
91501
+ }
90051
91502
  var BUNDLE_HEADER_BYTES = 256;
90052
91503
  var BUNDLE_HEADER_BYTES_PER_PART = 256;
90053
91504
  function rawUploadBody(session, readTranscript, maxBytes = MAX_RAW_UPLOAD_BYTES) {
90054
- const main2 = encodeRawTranscript(readTranscript(session.transcriptPath));
91505
+ const main2 = encodeRawTranscript(readTranscript(session.transcriptPath, session.transcriptPrefix));
90055
91506
  if (session.subagents === void 0) {
90056
91507
  return main2.byteLength > maxBytes ? "too-large" : { body: main2, main: main2, contentType: "application/gzip", skippedSubagentFiles: 0, unreadableSubagentFiles: 0 };
90057
91508
  }
@@ -90063,8 +91514,9 @@ function rawUploadBody(session, readTranscript, maxBytes = MAX_RAW_UPLOAD_BYTES)
90063
91514
  for (const file2 of session.subagents.files) {
90064
91515
  let body;
90065
91516
  try {
90066
- body = encodeRawTranscript(readTranscript(file2.path));
90067
- } catch {
91517
+ body = encodeRawTranscript(readTranscript(file2.path, file2.prefix));
91518
+ } catch (error63) {
91519
+ if (error63 instanceof TranscriptChangedError) throw error63;
90068
91520
  skipped += 1;
90069
91521
  unreadable += 1;
90070
91522
  continue;
@@ -90090,33 +91542,61 @@ function rawUploadBody(session, readTranscript, maxBytes = MAX_RAW_UPLOAD_BYTES)
90090
91542
  unreadableSubagentFiles: unreadable
90091
91543
  };
90092
91544
  }
91545
+ function shippedContent(session) {
91546
+ if (session.subagents === void 0 && session.transcriptPrefix === void 0) return void 0;
91547
+ const prefixOf = (prefix) => [prefix.bytes, prefix.sha256];
91548
+ const parts = session.subagents?.files.map((file2) => JSON.stringify([file2.part, ...prefixOf(file2.prefix)])).sort();
91549
+ return JSON.stringify({
91550
+ transcript: session.transcriptPrefix === void 0 ? null : prefixOf(session.transcriptPrefix),
91551
+ parts: parts ?? null,
91552
+ skipped: session.subagents?.skipped ?? null,
91553
+ skippedUnreadable: session.subagents?.skippedUnreadable ?? null
91554
+ });
91555
+ }
91556
+ function unchangedSince(acknowledged, session) {
91557
+ if (acknowledged.mtimeMs !== session.mtimeMs) return false;
91558
+ const now = shippedContent(session);
91559
+ return now === void 0 || acknowledged.content === now;
91560
+ }
91561
+ function isRawShipped(shipped, session) {
91562
+ const acknowledged = shipped.get(sessionKey(session.item));
91563
+ return acknowledged !== void 0 && unchangedSince(acknowledged, session);
91564
+ }
90093
91565
  var UNREADABLE_SUBAGENT_RETRIES = 3;
90094
- async function uploadRawSessions(apiUrl, machineToken, transcriptUploadSources, readTranscript = (p) => readFileSync9(p, "utf8"), shipped, progress) {
91566
+ async function uploadRawSessions(apiUrl, machineToken, transcriptUploadSources, readTranscript = readCollectedTranscript, shipped, progress) {
90095
91567
  const summary = {
90096
91568
  uploaded: 0,
90097
91569
  skippedTooLarge: 0,
90098
91570
  skippedSubagentFiles: 0,
90099
91571
  skippedUnchanged: 0,
90100
91572
  missingMetadata: 0,
91573
+ changedSinceRead: 0,
90101
91574
  failed: [],
90102
- unconfigured: false
91575
+ unconfigured: false,
91576
+ outcomes: /* @__PURE__ */ new Map()
90103
91577
  };
90104
91578
  const initiallyShipped = new Map(shipped);
90105
91579
  let rateLimited;
90106
91580
  for (const session of transcriptUploadSources) {
90107
91581
  summary.uploaded += await progress?.beforeUpload?.() ?? 0;
90108
- const transcriptUploadSourcePath = session.transcriptPath;
90109
- const acknowledged = shipped?.get(transcriptUploadSourcePath);
90110
- const unchanged = acknowledged?.mtimeMs === session.mtimeMs;
90111
- if (unchanged && acknowledged.unreadableRetries === 0 || acknowledged !== void 0 && acknowledged !== initiallyShipped.get(transcriptUploadSourcePath)) {
91582
+ const key = sessionKey(session.item);
91583
+ const acknowledged = shipped?.get(key);
91584
+ const unchanged = acknowledged !== void 0 && unchangedSince(acknowledged, session);
91585
+ if (unchanged && acknowledged.unreadableRetries === 0 || acknowledged !== void 0 && acknowledged !== initiallyShipped.get(key)) {
90112
91586
  summary.skippedUnchanged += 1;
91587
+ summary.outcomes.set(key, "unchanged");
90113
91588
  continue;
90114
91589
  }
90115
91590
  try {
90116
91591
  const encoded = rawUploadBody(session, readTranscript);
90117
91592
  if (encoded === "too-large") {
90118
91593
  summary.skippedTooLarge += 1;
90119
- shipped?.set(transcriptUploadSourcePath, { mtimeMs: session.mtimeMs, unreadableRetries: 0 });
91594
+ summary.outcomes.set(key, "too-large");
91595
+ shipped?.set(key, {
91596
+ mtimeMs: session.mtimeMs,
91597
+ unreadableRetries: 0,
91598
+ content: shippedContent(session)
91599
+ });
90120
91600
  continue;
90121
91601
  }
90122
91602
  const query = `harness=${session.item.harness}&harnessSessionId=${encodeURIComponent(session.item.harnessSessionId)}`;
@@ -90148,6 +91628,7 @@ async function uploadRawSessions(apiUrl, machineToken, transcriptUploadSources,
90148
91628
  }
90149
91629
  if (response.status === 404) {
90150
91630
  summary.missingMetadata += 1;
91631
+ summary.outcomes.set(key, "missing-metadata");
90151
91632
  continue;
90152
91633
  }
90153
91634
  if (response.status === 429) {
@@ -90158,12 +91639,20 @@ async function uploadRawSessions(apiUrl, machineToken, transcriptUploadSources,
90158
91639
  }
90159
91640
  summary.uploaded += 1;
90160
91641
  summary.skippedSubagentFiles += skippedSubagentFiles;
91642
+ summary.outcomes.set(key, "uploaded");
90161
91643
  progress?.onUploaded?.();
90162
- shipped?.set(transcriptUploadSourcePath, {
91644
+ shipped?.set(key, {
90163
91645
  mtimeMs: session.mtimeMs,
90164
- unreadableRetries: unreadableSubagentFiles === 0 ? 0 : unchanged ? acknowledged.unreadableRetries - 1 : UNREADABLE_SUBAGENT_RETRIES
91646
+ unreadableRetries: unreadableSubagentFiles === 0 ? 0 : unchanged ? acknowledged.unreadableRetries - 1 : UNREADABLE_SUBAGENT_RETRIES,
91647
+ content: shippedContent(session)
90165
91648
  });
90166
91649
  } catch (error63) {
91650
+ if (error63 instanceof TranscriptChangedError) {
91651
+ summary.changedSinceRead += 1;
91652
+ summary.outcomes.set(key, "changed-since-read");
91653
+ continue;
91654
+ }
91655
+ summary.outcomes.set(key, "failed");
90167
91656
  if (error63 instanceof RateLimitedError) rateLimited ??= error63;
90168
91657
  summary.failed.push(
90169
91658
  `${session.item.harnessSessionId}: ${error63 instanceof Error ? error63.message : String(error63)}`
@@ -90228,7 +91717,7 @@ function gotrueAuthClient(url2, key) {
90228
91717
  }
90229
91718
  async function runEnrollment(config2, options) {
90230
91719
  const log3 = options.log ?? console.log;
90231
- const name = options.name?.trim() || os13.hostname();
91720
+ const name = options.name?.trim() || os14.hostname();
90232
91721
  let scopePaths = (options.scope ?? []).map(normalizeScopePath);
90233
91722
  let captureMode = scopePaths.length > 0 ? "scope" : "org-repos";
90234
91723
  if (captureMode === "scope") {
@@ -90245,7 +91734,7 @@ async function runEnrollment(config2, options) {
90245
91734
  const anyWorkspaceClaims = (orgPolicy?.workspaces ?? []).some(workspaceClaimsAnyRepo);
90246
91735
  if (orgPolicy !== void 0 && orgPolicy.owners.length === 0 && orgPolicy.repos === null && !anyWorkspaceClaims) {
90247
91736
  if (config2.appEnv === "development") {
90248
- const home = normalizeScopePath(os13.homedir());
91737
+ const home = normalizeScopePath(os14.homedir());
90249
91738
  log3(
90250
91739
  `No GitHub integration on org "${result.org.name}", syncing sessions under ${home} (your home folder, local DB only). Re-enroll with --scope <path> to narrow.`
90251
91740
  );
@@ -90319,7 +91808,7 @@ async function enroll(config2, options, createAuthClient = gotrueAuthClient, lis
90319
91808
  { ...listener, log: options.log, redeem: (code) => redeemApproval(config2.apiUrl, code, verifier) }
90320
91809
  );
90321
91810
  }
90322
- var challengeFor = (verifier) => createHash4("sha256").update(verifier).digest("hex");
91811
+ var challengeFor = (verifier) => createHash6("sha256").update(verifier).digest("hex");
90323
91812
  function enrollApprovalUrl(webUrl, {
90324
91813
  name,
90325
91814
  state,
@@ -90557,398 +92046,6 @@ function removeFolder(config2, rawPath, enrollCommand, log3, deps = defaultDeps)
90557
92046
  log3(`Removed ${folder}, sessions there stay local from the next sync pass.`);
90558
92047
  }
90559
92048
 
90560
- // ../../apps/syncservice/src/claudeCode.ts
90561
- import { readdirSync as readdirSync6 } from "node:fs";
90562
- import os14 from "node:os";
90563
- import path15 from "node:path";
90564
- import { setImmediate as yieldToEventLoop2 } from "node:timers/promises";
90565
- function parseClaudeCodeTranscript(text) {
90566
- const usageByMessage = /* @__PURE__ */ new Map();
90567
- const modelCounts = /* @__PURE__ */ new Map();
90568
- const timestamps = [];
90569
- let title;
90570
- let cwd;
90571
- let gitBranch;
90572
- let entrypoint;
90573
- let anonCounter = 0;
90574
- let speedMode;
90575
- for (const record2 of parseJsonl(text)) {
90576
- if (record2.type === "custom-title" && typeof record2.customTitle === "string") {
90577
- title = record2.customTitle;
90578
- continue;
90579
- }
90580
- if (record2.type === "ai-title" && typeof record2.aiTitle === "string") {
90581
- title = record2.aiTitle;
90582
- continue;
90583
- }
90584
- if (cwd === void 0 && typeof record2.cwd === "string" && record2.cwd !== "") cwd = record2.cwd;
90585
- if (typeof record2.entrypoint === "string" && record2.entrypoint !== "") {
90586
- entrypoint ??= record2.entrypoint;
90587
- }
90588
- if (typeof record2.gitBranch === "string" && record2.gitBranch !== "") {
90589
- gitBranch ??= record2.gitBranch;
90590
- }
90591
- if (typeof record2.timestamp === "string") timestamps.push(record2.timestamp);
90592
- if (record2.type !== "assistant") continue;
90593
- const message2 = record2.message;
90594
- if (!message2 || message2.model === "<synthetic>") continue;
90595
- const id = message2.id ?? (typeof record2.requestId === "string" ? record2.requestId : void 0) ?? (typeof record2.uuid === "string" ? record2.uuid : void 0) ?? `anon-${anonCounter++}`;
90596
- const previousMessage = usageByMessage.get(id);
90597
- const isNewMessage = previousMessage === void 0;
90598
- const speedValue = message2.usage?.speed;
90599
- const observedSpeed = speedModeOf(speedValue);
90600
- const messageSpeed = observedSpeed ?? previousMessage?.speedMode;
90601
- if (speedValue !== void 0) speedMode = observedSpeed ?? "unknown";
90602
- if (message2.usage) {
90603
- usageByMessage.set(id, {
90604
- usage: toUsage(message2.usage),
90605
- ...messageSpeed && { speedMode: messageSpeed }
90606
- });
90607
- } else if (isNewMessage) {
90608
- usageByMessage.set(id, {
90609
- usage: { input: 0, cacheRead: 0, cacheCreation: 0, output: 0 }
90610
- });
90611
- }
90612
- if (isNewMessage && typeof message2.model === "string") {
90613
- modelCounts.set(message2.model, (modelCounts.get(message2.model) ?? 0) + 1);
90614
- }
90615
- }
90616
- if (usageByMessage.size === 0 && timestamps.length === 0) return null;
90617
- const summed = [...usageByMessage.values()].map((entry) => entry.usage).reduce(addUsage, {
90618
- input: 0,
90619
- cacheRead: 0,
90620
- cacheCreation: 0,
90621
- output: 0
90622
- });
90623
- const speedFullyMeasured = usageByMessage.size > 0 && [...usageByMessage.values()].every((entry) => entry.speedMode !== void 0);
90624
- const fastUsage = speedFullyMeasured ? [...usageByMessage.values()].filter((entry) => entry.speedMode === "fast").map((entry) => entry.usage).reduce(addUsage, { input: 0, cacheRead: 0, cacheCreation: 0, output: 0 }) : null;
90625
- return {
90626
- startedAt: timestamps[0],
90627
- endedAt: timestamps[timestamps.length - 1],
90628
- model: dominantKey(modelCounts),
90629
- usage: usageByMessage.size > 0 ? summed : null,
90630
- fastUsage,
90631
- ...speedMode && { speedMode },
90632
- title,
90633
- turnCount: usageByMessage.size,
90634
- cwd,
90635
- gitBranch,
90636
- entrypoint
90637
- };
90638
- }
90639
- var AGENT_SDK_ENTRYPOINTS = /* @__PURE__ */ new Set(["sdk-py", "sdk-ts"]);
90640
- function isAutomationEntrypoint(entrypoint) {
90641
- return entrypoint !== void 0 && AGENT_SDK_ENTRYPOINTS.has(entrypoint);
90642
- }
90643
- async function collectClaudeCodeSessions(options = {}) {
90644
- const claudeDir = options.claudeDir ?? path15.join(os14.homedir(), ".claude");
90645
- const now = options.now ?? Date.now();
90646
- const capture2 = options.capture ?? captureEverythingUnresolved;
90647
- const maxBytes = options.maxTranscriptBytes ?? MAX_TRANSCRIPT_FILE_BYTES;
90648
- const projectsDir = path15.join(claudeDir, "projects");
90649
- const items = [];
90650
- const skips = { skippedOutOfScope: 0, skippedNoRepo: 0 };
90651
- const candidates = [];
90652
- const unreadable = [];
90653
- let skippedTooLarge = 0;
90654
- const home = path15.dirname(claudeDir);
90655
- const lifecycle = options.lifecycle ?? readLifecycleJournal(home, now, ["claude-local"]);
90656
- for (const projectDir of listDirs(projectsDir)) {
90657
- for (const entry of listFiles(projectDir, ".jsonl")) {
90658
- await yieldToEventLoop2();
90659
- const transcriptPath = path15.join(projectDir, entry);
90660
- const stat = statIfExists(transcriptPath);
90661
- if (!stat) continue;
90662
- const sessionId = path15.basename(entry, ".jsonl");
90663
- const subagents = options.locationsOnly ? NO_SUBAGENTS : listSubagents(path15.join(projectDir, sessionId, "subagents"));
90664
- const mtimeMs = Math.max(stat.mtimeMs, subagents.workflows.mtimeMs, ...subagents.files.map((file2) => file2.mtimeMs ?? 0));
90665
- if (options.sinceMs !== void 0 && mtimeMs < options.sinceMs) continue;
90666
- if (stat.size > maxBytes) {
90667
- skippedTooLarge += 1;
90668
- observeFileDiagnostic(options.onFileDiagnostic, {
90669
- path: transcriptPath,
90670
- reason: "too_large",
90671
- sizeBytes: stat.size
90672
- });
90673
- continue;
90674
- }
90675
- const endObservedAt = observedSessionEnd(lifecycle.latestBoundary("claude-local", sessionId), mtimeMs);
90676
- const status = endObservedAt === null && now - mtimeMs < ACTIVE_GRACE_MS ? "active" : "inactive";
90677
- try {
90678
- const cwd = transcriptCwd(transcriptPath);
90679
- const decision = captureDecision(cwd, capture2);
90680
- if (!decision.capture) {
90681
- countCaptureSkip(skips, decision.reason);
90682
- continue;
90683
- }
90684
- if (options.locationsOnly) {
90685
- items.push({
90686
- mtimeMs: stat.mtimeMs,
90687
- transcriptPath,
90688
- ...cwd && { cwd },
90689
- item: { harness: "claude-local", harnessSessionId: sessionId, status, capturedVia: decision.via }
90690
- });
90691
- continue;
90692
- }
90693
- candidates.push({
90694
- transcriptPath,
90695
- sessionId,
90696
- mtimeMs,
90697
- subagents,
90698
- status,
90699
- endObservedAt,
90700
- ...cwd && { cwd },
90701
- decision
90702
- });
90703
- } catch (error63) {
90704
- unreadable.push(`${sessionId}: ${describeReadError(error63)}`);
90705
- observeFileDiagnostic(options.onFileDiagnostic, {
90706
- path: transcriptPath,
90707
- reason: "unreadable",
90708
- sizeBytes: null,
90709
- errorCode: diagnosticErrorCode(error63)
90710
- });
90711
- }
90712
- }
90713
- }
90714
- if (options.locationsOnly) {
90715
- return {
90716
- sessions: items.sort((a, b2) => b2.mtimeMs - a.mtimeMs),
90717
- ...skips,
90718
- skippedTooLarge,
90719
- unreadable,
90720
- heldBack: 0
90721
- };
90722
- }
90723
- candidates.sort((a, b2) => b2.mtimeMs - a.mtimeMs);
90724
- const chosen = options.maxSessions === void 0 ? candidates : candidates.slice(0, options.maxSessions);
90725
- for (const candidate of chosen) {
90726
- await yieldToEventLoop2();
90727
- const { transcriptPath, sessionId, mtimeMs, subagents, status, endObservedAt, cwd, decision } = candidate;
90728
- try {
90729
- const text = readIfExists(transcriptPath);
90730
- if (text === null) continue;
90731
- const summary = parseClaudeCodeTranscript(text);
90732
- if (!summary) continue;
90733
- const usage = addSubagentUsage(
90734
- summary.usage,
90735
- summary.fastUsage,
90736
- subagents.files,
90737
- { maxBytes, sessionId, onFileDiagnostic: options.onFileDiagnostic }
90738
- );
90739
- skippedTooLarge += usage.skippedTooLarge;
90740
- unreadable.push(...usage.unreadable);
90741
- const endedAt = endObservedAt ?? laterTimestamp(summary.endedAt, usage.endedAt);
90742
- items.push({
90743
- mtimeMs,
90744
- transcriptPath,
90745
- subagents: {
90746
- files: usage.capture.files,
90747
- skipped: usage.capture.skipped + subagents.workflows.count,
90748
- skippedUnreadable: usage.capture.skippedUnreadable + subagents.workflows.unreadable
90749
- },
90750
- ...cwd && { cwd },
90751
- item: {
90752
- harness: "claude-local",
90753
- harnessSessionId: sessionId,
90754
- status,
90755
- ...summary.startedAt && { startedAt: summary.startedAt },
90756
- ...endedAt && { endedAt },
90757
- ...summary.model && { model: summary.model },
90758
- ...summary.speedMode && { speedMode: summary.speedMode },
90759
- // Total input-class tokens the source reported (uncached + cache
90760
- // reads + cache writes) — a real reported total, not an estimate.
90761
- // The cache breakdown rides along: pricing bills reads at 0.1×
90762
- // and writes at 1.25×, so collapsing it would inflate estimates.
90763
- ...usage.total && {
90764
- inputTokens: usage.total.input + usage.total.cacheRead + usage.total.cacheCreation
90765
- },
90766
- ...usage.total && { outputTokens: usage.total.output },
90767
- ...usage.total && { cacheReadTokens: usage.total.cacheRead },
90768
- ...usage.total && { cacheWriteTokens: usage.total.cacheCreation },
90769
- ...usage.fast && {
90770
- fastInputTokens: usage.fast.input + usage.fast.cacheRead + usage.fast.cacheCreation,
90771
- fastOutputTokens: usage.fast.output,
90772
- fastCacheReadTokens: usage.fast.cacheRead,
90773
- fastCacheWriteTokens: usage.fast.cacheCreation
90774
- },
90775
- ...summary.title && { title: redactText(summary.title).slice(0, 500) },
90776
- ...isAutomationEntrypoint(summary.entrypoint) && { automation: true },
90777
- // The PR-join coordinates: the checkout's directory name (bare
90778
- // repo, not the local path) and the branch the work started on.
90779
- ...cwd && { repoDir: path15.basename(cwd) },
90780
- ...summary.gitBranch && { gitBranch: summary.gitBranch },
90781
- ...decision.repoFullName && { repoFullName: decision.repoFullName },
90782
- capturedVia: decision.via
90783
- }
90784
- });
90785
- } catch (error63) {
90786
- unreadable.push(`${sessionId}: ${describeReadError(error63)}`);
90787
- observeFileDiagnostic(options.onFileDiagnostic, {
90788
- path: transcriptPath,
90789
- reason: "unreadable",
90790
- sizeBytes: null,
90791
- errorCode: diagnosticErrorCode(error63)
90792
- });
90793
- }
90794
- }
90795
- return {
90796
- sessions: items.sort((a, b2) => b2.mtimeMs - a.mtimeMs),
90797
- ...skips,
90798
- skippedTooLarge,
90799
- unreadable,
90800
- heldBack: candidates.length - chosen.length
90801
- };
90802
- }
90803
- function transcriptCwd(transcriptPath) {
90804
- const record2 = readFirstRecord(
90805
- transcriptPath,
90806
- (candidate) => typeof candidate.cwd === "string" && candidate.cwd !== ""
90807
- );
90808
- return typeof record2?.cwd === "string" ? record2.cwd : void 0;
90809
- }
90810
- function toUsage(u) {
90811
- const n = (value) => typeof value === "number" ? value : 0;
90812
- return {
90813
- input: n(u.input_tokens),
90814
- cacheRead: n(u.cache_read_input_tokens),
90815
- cacheCreation: n(u.cache_creation_input_tokens),
90816
- output: n(u.output_tokens)
90817
- };
90818
- }
90819
- function speedModeOf(value) {
90820
- return value === "fast" || value === "standard" ? value : void 0;
90821
- }
90822
- function addUsage(a, b2) {
90823
- return {
90824
- input: a.input + b2.input,
90825
- cacheRead: a.cacheRead + b2.cacheRead,
90826
- cacheCreation: a.cacheCreation + b2.cacheCreation,
90827
- output: a.output + b2.output
90828
- };
90829
- }
90830
- var NO_SUBAGENTS = { files: [], workflows: { count: 0, unreadable: 0, mtimeMs: 0 } };
90831
- function listSubagents(subagentsDir) {
90832
- const entries = directoryEntries(subagentsDir);
90833
- if (!Array.isArray(entries)) {
90834
- return { ...NO_SUBAGENTS, files: [{ entry: "subagents/", path: subagentsDir, mtimeMs: null, size: null, statError: entries.error }] };
90835
- }
90836
- const names = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => entry.name).sort();
90837
- const files = names.flatMap((entry) => {
90838
- const filePath = path15.join(subagentsDir, entry);
90839
- try {
90840
- const stat = statIfExists(filePath);
90841
- return stat === null ? [] : [{ entry, path: filePath, mtimeMs: stat.mtimeMs, size: stat.size, statError: null }];
90842
- } catch (statError) {
90843
- return [{ entry, path: filePath, mtimeMs: null, size: null, statError }];
90844
- }
90845
- });
90846
- const hasWorkflows = entries.some((entry) => entry.isDirectory() && entry.name === "workflows");
90847
- return { files, workflows: hasWorkflows ? listWorkflowTranscripts(path15.join(subagentsDir, "workflows")) : NO_SUBAGENTS.workflows };
90848
- }
90849
- function listWorkflowTranscripts(workflowsDir) {
90850
- const found = { count: 0, unreadable: 0, mtimeMs: 0 };
90851
- const failedToRead = () => {
90852
- found.count += 1;
90853
- found.unreadable += 1;
90854
- };
90855
- const list = (dir) => {
90856
- const entries = directoryEntries(dir);
90857
- if (Array.isArray(entries)) return entries;
90858
- failedToRead();
90859
- return [];
90860
- };
90861
- for (const run of list(workflowsDir)) {
90862
- if (!run.isDirectory()) continue;
90863
- const runDir = path15.join(workflowsDir, run.name);
90864
- for (const file2 of list(runDir)) {
90865
- if (!file2.isFile() || !file2.name.endsWith(".jsonl")) continue;
90866
- try {
90867
- const stat = statIfExists(path15.join(runDir, file2.name));
90868
- if (stat === null) continue;
90869
- found.count += 1;
90870
- found.mtimeMs = Math.max(found.mtimeMs, stat.mtimeMs);
90871
- } catch {
90872
- failedToRead();
90873
- }
90874
- }
90875
- }
90876
- return found;
90877
- }
90878
- function directoryEntries(dir) {
90879
- try {
90880
- return readdirSync6(dir, { withFileTypes: true });
90881
- } catch (error63) {
90882
- const code = error63.code;
90883
- return code === "ENOENT" || code === "ENOTDIR" ? [] : { error: error63 };
90884
- }
90885
- }
90886
- function subagentPart(entry) {
90887
- const match = /^agent-(.+)\.jsonl$/.exec(entry);
90888
- return match !== null && RAW_TRANSCRIPT_PART_PATTERN.test(match[1]) ? match[1] : null;
90889
- }
90890
- function laterTimestamp(a, b2) {
90891
- if (a === void 0 || b2 === void 0) return a ?? b2;
90892
- return Date.parse(b2) > Date.parse(a) ? b2 : a;
90893
- }
90894
- function addSubagentUsage(main2, mainFast, subagentFiles, bounds) {
90895
- let total = main2;
90896
- let fast = mainFast;
90897
- let fastCoverageComplete = main2 === null || mainFast !== null;
90898
- let skippedTooLarge = 0;
90899
- const unreadable = [];
90900
- const capture2 = { files: [], skipped: 0, skippedUnreadable: 0 };
90901
- let endedAt;
90902
- for (const file2 of subagentFiles) {
90903
- const { entry, path: sidecarPath } = file2;
90904
- try {
90905
- if (file2.size === null) throw file2.statError;
90906
- const { size } = file2;
90907
- if (size > bounds.maxBytes) {
90908
- skippedTooLarge += 1;
90909
- capture2.skipped += 1;
90910
- observeFileDiagnostic(bounds.onFileDiagnostic, { path: sidecarPath, reason: "too_large", sizeBytes: size });
90911
- continue;
90912
- }
90913
- const text = readIfExists(sidecarPath);
90914
- if (text === null) {
90915
- continue;
90916
- }
90917
- const part = subagentPart(entry);
90918
- if (part === null) capture2.skipped += 1;
90919
- else capture2.files.push({ part, path: sidecarPath });
90920
- const summary = parseClaudeCodeTranscript(text);
90921
- endedAt = laterTimestamp(endedAt, summary?.endedAt);
90922
- if (summary?.usage) {
90923
- total = addUsage(total ?? { input: 0, cacheRead: 0, cacheCreation: 0, output: 0 }, summary.usage);
90924
- if (summary.fastUsage === null) fastCoverageComplete = false;
90925
- }
90926
- if (summary?.fastUsage) {
90927
- fast = addUsage(fast ?? { input: 0, cacheRead: 0, cacheCreation: 0, output: 0 }, summary.fastUsage);
90928
- }
90929
- } catch (error63) {
90930
- capture2.skipped += 1;
90931
- capture2.skippedUnreadable += 1;
90932
- unreadable.push(`${bounds.sessionId} (subagent ${entry}): ${describeReadError(error63)}`);
90933
- observeFileDiagnostic(bounds.onFileDiagnostic, {
90934
- path: sidecarPath,
90935
- reason: "unreadable",
90936
- sizeBytes: null,
90937
- errorCode: diagnosticErrorCode(error63)
90938
- });
90939
- }
90940
- }
90941
- return { capture: capture2, total, fast: fastCoverageComplete ? fast : null, skippedTooLarge, unreadable, endedAt };
90942
- }
90943
- function claudeCodeSessionCwd(sessionId, claudeDir = path15.join(os14.homedir(), ".claude")) {
90944
- if (!SESSION_UUID.test(sessionId)) return void 0;
90945
- for (const projectDir of listDirs(path15.join(claudeDir, "projects"))) {
90946
- const cwd = transcriptCwd(path15.join(projectDir, `${sessionId}.jsonl`));
90947
- if (cwd !== void 0) return cwd;
90948
- }
90949
- return void 0;
90950
- }
90951
-
90952
92049
  // ../../apps/syncservice/src/protectedRepoDiscovery.ts
90953
92050
  import os15 from "node:os";
90954
92051
  import path16 from "node:path";
@@ -91379,7 +92476,9 @@ var claudeLocal = {
91379
92476
  const dirs = new Set(installs.map((install) => install.configDir ?? homeDir));
91380
92477
  return [...dirs].map((dir) => path18.join(dir, ".claude.json"));
91381
92478
  },
91382
- registration: jsonServerEntry
92479
+ registration: jsonServerEntry,
92480
+ // <project folder>/<session id>.jsonl
92481
+ transcriptSession: (transcriptPath) => ({ externalId: path18.basename(transcriptPath, ".jsonl") })
91383
92482
  };
91384
92483
  var codexLocal = {
91385
92484
  harness: "codex-local",
@@ -91419,7 +92518,12 @@ var codexLocal = {
91419
92518
  registration: (content) => content,
91420
92519
  // The stored ID is the base rollout's file name, and hooks name the
91421
92520
  // thread, whose ID the file name ends with.
91422
- hookSessionId: (externalId) => rolloutIdentity(`${externalId}.jsonl`).threadId
92521
+ hookSessionId: (externalId) => rolloutIdentity(`${externalId}.jsonl`).threadId,
92522
+ // A page file continues a thread whose base rollout names the session.
92523
+ transcriptSession(transcriptPath) {
92524
+ const identity = rolloutIdentity(transcriptPath);
92525
+ return identity.page ? { hookId: identity.threadId } : { externalId: path18.basename(transcriptPath, ".jsonl") };
92526
+ }
91423
92527
  };
91424
92528
  var cursorLocal = {
91425
92529
  harness: "cursor-local",
@@ -91714,7 +92818,7 @@ async function checkCursor(mcpJsonPath, context2) {
91714
92818
  }
91715
92819
 
91716
92820
  // ../../apps/syncservice/src/harnessConnections.ts
91717
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync10, renameSync as renameSync5, watch as fsWatch, writeFileSync as writeFileSync9 } from "node:fs";
92821
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync11, renameSync as renameSync6, watch as fsWatch, writeFileSync as writeFileSync8 } from "node:fs";
91718
92822
  import os18 from "node:os";
91719
92823
  import path19 from "node:path";
91720
92824
  var CONNECTION_STATES = [
@@ -91873,7 +92977,7 @@ function readSavedFile(readTextFile, homeDir) {
91873
92977
  }
91874
92978
  function bestEffortRead(filePath) {
91875
92979
  try {
91876
- return readFileSync10(filePath, "utf8");
92980
+ return readFileSync11(filePath, "utf8");
91877
92981
  } catch {
91878
92982
  return null;
91879
92983
  }
@@ -91882,8 +92986,8 @@ function writeSavedFile(record2, homeDir) {
91882
92986
  const file2 = detectedClientsPath(homeDir);
91883
92987
  mkdirSync10(path19.dirname(file2), { recursive: true });
91884
92988
  const temporary = `${file2}.${process.pid}.tmp`;
91885
- writeFileSync9(temporary, JSON.stringify(record2, null, 2));
91886
- renameSync5(temporary, file2);
92989
+ writeFileSync8(temporary, JSON.stringify(record2, null, 2));
92990
+ renameSync6(temporary, file2);
91887
92991
  }
91888
92992
  function loadDetectedClients(readTextFile = bestEffortRead, homeDir) {
91889
92993
  const parsed = detectedClientsSchema.safeParse(readSavedFile(readTextFile, homeDir));
@@ -92233,7 +93337,7 @@ function recentCatchupFloor(sinceMs, sessions) {
92233
93337
  function sessionsForCatchup(sessions, selection) {
92234
93338
  if (!selection) return sessions;
92235
93339
  return sessions.filter(
92236
- (session) => session.mtimeMs >= selection.sinceMs || selection.paths.has(session.transcriptPath)
93340
+ (session) => session.mtimeMs >= selection.sinceMs || selection.sessions.has(sessionKey(session.item))
92237
93341
  );
92238
93342
  }
92239
93343
  function recentSyncOptions(shared, narrowSinceMs, selection) {
@@ -92247,7 +93351,7 @@ function recentSyncOptions(shared, narrowSinceMs, selection) {
92247
93351
  }
92248
93352
 
92249
93353
  // ../../apps/syncservice/src/branchFacts.ts
92250
- import { readFileSync as readFileSync11 } from "node:fs";
93354
+ import { readFileSync as readFileSync12 } from "node:fs";
92251
93355
  import path20 from "node:path";
92252
93356
  var SHA_SHAPE = /^[0-9a-f]{40}$/;
92253
93357
  function observeBranch(cwd, environment = {}) {
@@ -92318,7 +93422,7 @@ function resolveRef2(commonDir, ref) {
92318
93422
  }
92319
93423
  function readIfExists2(p) {
92320
93424
  try {
92321
- return readFileSync11(p, "utf8");
93425
+ return readFileSync12(p, "utf8");
92322
93426
  } catch {
92323
93427
  return null;
92324
93428
  }
@@ -92336,7 +93440,8 @@ async function registerAnnouncedPreviews(apiUrl, machineToken, collected, memo2,
92336
93440
  };
92337
93441
  for (const session of collected) {
92338
93442
  if (session.item.status !== "active") continue;
92339
- const state = memo2.get(session.transcriptPath);
93443
+ const key = sessionKey(session.item);
93444
+ const state = memo2.get(key);
92340
93445
  if (state?.foreignOwner || state?.previewsOff) continue;
92341
93446
  if (state?.examinedMtimeMs === session.mtimeMs) continue;
92342
93447
  try {
@@ -92354,7 +93459,7 @@ async function registerAnnouncedPreviews(apiUrl, machineToken, collected, memo2,
92354
93459
  }));
92355
93460
  const registeredJson = JSON.stringify(previews);
92356
93461
  if (previews.length === 0 || state?.registeredJson === registeredJson) {
92357
- memo2.set(session.transcriptPath, { ...state, examinedMtimeMs: session.mtimeMs });
93462
+ memo2.set(key, { ...state, examinedMtimeMs: session.mtimeMs });
92358
93463
  continue;
92359
93464
  }
92360
93465
  const outcome = await upload(apiUrl, machineToken, {
@@ -92363,16 +93468,16 @@ async function registerAnnouncedPreviews(apiUrl, machineToken, collected, memo2,
92363
93468
  previews
92364
93469
  });
92365
93470
  if (outcome === "foreign-owner") {
92366
- memo2.set(session.transcriptPath, { examinedMtimeMs: session.mtimeMs, foreignOwner: true });
93471
+ memo2.set(key, { examinedMtimeMs: session.mtimeMs, foreignOwner: true });
92367
93472
  summary.foreignOwned.push(session.item.harnessSessionId);
92368
93473
  continue;
92369
93474
  }
92370
93475
  if (outcome === "previews-off") {
92371
- memo2.set(session.transcriptPath, { examinedMtimeMs: session.mtimeMs, previewsOff: true });
93476
+ memo2.set(key, { examinedMtimeMs: session.mtimeMs, previewsOff: true });
92372
93477
  summary.previewsOff.push(session.item.harnessSessionId);
92373
93478
  continue;
92374
93479
  }
92375
- memo2.set(session.transcriptPath, { examinedMtimeMs: session.mtimeMs, registeredJson });
93480
+ memo2.set(key, { examinedMtimeMs: session.mtimeMs, registeredJson });
92376
93481
  summary.registered += 1;
92377
93482
  } catch (error63) {
92378
93483
  summary.failed.push(
@@ -92531,6 +93636,8 @@ async function runSyncPassInternal(config2, options, diagnostic) {
92531
93636
  sinceMs,
92532
93637
  capture: capture2,
92533
93638
  maxSessions,
93639
+ ...options.codexThreadLinks && { threadLinks: options.codexThreadLinks },
93640
+ ...options.codexThreadReads && { threadReads: options.codexThreadReads },
92534
93641
  onFileDiagnostic: files.observe("codex")
92535
93642
  });
92536
93643
  diagnostic.codex = {
@@ -92619,12 +93726,8 @@ async function runSyncPassInternal(config2, options, diagnostic) {
92619
93726
  credentials.token,
92620
93727
  items
92621
93728
  );
92622
- const foreignKeys = new Set(
92623
- foreignOwned.map(({ harness, harnessSessionId }) => JSON.stringify([harness, harnessSessionId]))
92624
- );
92625
- const writable = collected.filter(
92626
- ({ item }) => !foreignKeys.has(JSON.stringify([item.harness, item.harnessSessionId]))
92627
- );
93729
+ const foreignKeys = new Set(foreignOwned.map(sessionKey));
93730
+ const writable = collected.filter(({ item }) => !foreignKeys.has(sessionKey(item)));
92628
93731
  for (const { harness, harnessSessionId } of foreignOwned) {
92629
93732
  log3(`Skipped ${harness} session ${harnessSessionId}: already synced from another machine.`);
92630
93733
  }
@@ -92679,17 +93782,31 @@ async function runSyncPassInternal(config2, options, diagnostic) {
92679
93782
  const rawWindowSkipped = writable.length - rawEligible.length;
92680
93783
  diagnostic.rawWindowSkipped = rawWindowSkipped;
92681
93784
  diagnostic.stage = "raw";
93785
+ const besideBackgroundSync = options.rawShipped === void 0 && (options.backgroundSyncIsRunning ?? (() => backgroundSyncIsRunning(config2.appEnv)))();
92682
93786
  const raw = await uploadRawSessions(
92683
93787
  credentials.apiUrl,
92684
93788
  credentials.token,
92685
93789
  rawEligible,
92686
- void 0,
93790
+ besideBackgroundSync ? readUnchangedTranscript : readCollectedTranscript,
92687
93791
  options.rawShipped,
92688
93792
  {
92689
93793
  beforeUpload: options.beforeRawUpload ? () => options.beforeRawUpload(collected) : void 0,
92690
93794
  onUploaded: options.onRawUploaded
92691
93795
  }
92692
93796
  );
93797
+ const givenUp = options.codexThreadLinks?.settleRecoveries((session) => {
93798
+ const key = sessionKey(session.item);
93799
+ if (raw.unconfigured || foreignKeys.has(key) || rawFloor !== null && session.mtimeMs < rawFloor) return "refused";
93800
+ if (options.rawShipped !== void 0 && isRawShipped(options.rawShipped, session)) return "shipped";
93801
+ const outcome = raw.outcomes.get(key);
93802
+ if (outcome === void 0) return "waiting";
93803
+ return outcome === "missing-metadata" ? "refused" : "failed";
93804
+ }) ?? [];
93805
+ for (const threadId2 of givenUp) {
93806
+ log3(
93807
+ `\u26A0 Codex thread ${threadId2} is still owed a corrected capture after ${CORRECTION_UPLOAD_ATTEMPTS} failed uploads; it is sent again when it changes.`
93808
+ );
93809
+ }
92693
93810
  diagnostic.rawUnconfigured = raw.unconfigured;
92694
93811
  if (!raw.unconfigured) {
92695
93812
  diagnostic.rawUploaded = raw.uploaded;
@@ -92706,6 +93823,11 @@ async function runSyncPassInternal(config2, options, diagnostic) {
92706
93823
  if (rawWindowSkipped > 0) parts.push(`${rawWindowSkipped} outside the dev raw window`);
92707
93824
  if (raw.skippedTooLarge > 0) parts.push(`${raw.skippedTooLarge} skipped (too large)`);
92708
93825
  if (raw.skippedSubagentFiles > 0) parts.push(`${raw.skippedSubagentFiles} subagent file(s) left out`);
93826
+ if (raw.changedSinceRead > 0) {
93827
+ parts.push(
93828
+ besideBackgroundSync ? `${raw.changedSinceRead} changed while syncing, left for the background sync` : `${raw.changedSinceRead} changed while syncing, next pass`
93829
+ );
93830
+ }
92709
93831
  if (raw.failed.length > 0) parts.push(`${raw.failed.length} failed`);
92710
93832
  log3(`Raw content: ${parts.join(", ")}.`);
92711
93833
  for (const failure2 of raw.failed) log3(` raw upload failed, ${failure2}`);
@@ -92714,7 +93836,7 @@ async function runSyncPassInternal(config2, options, diagnostic) {
92714
93836
  `${raw.missingMetadata} raw transcript(s) waiting on their session metadata, they re-send once it lands.`
92715
93837
  );
92716
93838
  }
92717
- const nothingElseAccountedFor = raw.skippedUnchanged === 0 && raw.skippedTooLarge === 0 && rawWindowSkipped === 0 && raw.missingMetadata === 0;
93839
+ const nothingElseAccountedFor = raw.skippedUnchanged === 0 && raw.skippedTooLarge === 0 && rawWindowSkipped === 0 && raw.missingMetadata === 0 && raw.changedSinceRead === 0;
92718
93840
  if (raw.uploaded === 0 && (raw.failed.length > 1 || raw.failed.length === 1 && nothingElseAccountedFor)) {
92719
93841
  throw new Error(`all ${raw.failed.length} raw upload(s) failed this pass, ${raw.failed[0]}`);
92720
93842
  }
@@ -92850,6 +93972,9 @@ function defaultStat(file2) {
92850
93972
  }
92851
93973
  }
92852
93974
 
93975
+ // ../../apps/syncservice/src/sessionStatusFeed.ts
93976
+ import { statSync as statSync5 } from "node:fs";
93977
+
92853
93978
  // ../../apps/syncservice/src/sessionStatusModel.ts
92854
93979
  function createSessionStatusModel(now = Date.now) {
92855
93980
  const sessions = /* @__PURE__ */ new Map();
@@ -92897,6 +94022,7 @@ function createSessionStatusModel(now = Date.now) {
92897
94022
  }
92898
94023
 
92899
94024
  // ../../apps/syncservice/src/sessionStatusFeed.ts
94025
+ var TRANSCRIPT_WRITE_DELAY_MS = 500;
92900
94026
  var HELD_HOOK_ENTRIES_MAX = 500;
92901
94027
  var HOOK_ID_PAIRS_MAX = 1e4;
92902
94028
  var MAX_TIMER_MS = 2 ** 31 - 1;
@@ -92904,7 +94030,8 @@ function createSessionStatusFeed(deps) {
92904
94030
  const now = deps.now ?? Date.now;
92905
94031
  const adapters = deps.adapters ?? HARNESS_ADAPTERS;
92906
94032
  const model = createSessionStatusModel(now);
92907
- const hookIdOf = (harness) => adapters.find((adapter) => adapter.harness === harness)?.hookSessionId;
94033
+ const adapterOf = (harness) => adapters.find((adapter) => adapter.harness === harness);
94034
+ const hookIdOf = (harness) => adapterOf(harness)?.hookSessionId;
92908
94035
  const pairKey = (harness, hookId) => `${harness}:${hookId.toLowerCase()}`;
92909
94036
  const storedIds = /* @__PURE__ */ new Map();
92910
94037
  let held = [];
@@ -92912,6 +94039,8 @@ function createSessionStatusFeed(deps) {
92912
94039
  let deadlineTimer = null;
92913
94040
  let readQueued = false;
92914
94041
  let readFailing = false;
94042
+ let statFailing = false;
94043
+ const writeTimers = /* @__PURE__ */ new Map();
92915
94044
  let stopped = false;
92916
94045
  function stored(entry) {
92917
94046
  if (hookIdOf(entry.harness) === void 0) return entry;
@@ -92927,6 +94056,61 @@ function createSessionStatusFeed(deps) {
92927
94056
  }
92928
94057
  if (model.applyLifecycleEntry(mapped) !== null) changedInBatch = true;
92929
94058
  }
94059
+ function learnPair(harness, externalId) {
94060
+ const hookId = hookIdOf(harness)?.(externalId);
94061
+ if (hookId === void 0) return false;
94062
+ const key = pairKey(harness, hookId);
94063
+ const known = storedIds.get(key);
94064
+ storedIds.delete(key);
94065
+ storedIds.set(key, externalId);
94066
+ if (storedIds.size > HOOK_ID_PAIRS_MAX) storedIds.delete(storedIds.keys().next().value);
94067
+ return known !== externalId;
94068
+ }
94069
+ function releaseHeld() {
94070
+ const waiting = held;
94071
+ held = [];
94072
+ for (const entry of waiting) apply(entry);
94073
+ }
94074
+ function applyWrite(harness, sessionId, atMs) {
94075
+ if (model.applyTranscriptWrite({ harness, sessionId, atMs }) !== null) changedInBatch = true;
94076
+ }
94077
+ function applyWatchedWrite(harness, transcriptPath) {
94078
+ const session = adapterOf(harness)?.transcriptSession?.(transcriptPath);
94079
+ if (session === void 0) return;
94080
+ const externalId = "externalId" in session ? session.externalId : storedIds.get(pairKey(harness, session.hookId));
94081
+ if (externalId === void 0) return;
94082
+ const mtimeMs = modifiedMs(transcriptPath);
94083
+ if (mtimeMs === null) return;
94084
+ const learned = "externalId" in session && learnPair(harness, externalId);
94085
+ applyWrite(harness, externalId, mtimeMs);
94086
+ if (learned) releaseHeld();
94087
+ }
94088
+ function modifiedMs(transcriptPath) {
94089
+ try {
94090
+ const mtimeMs = statSync5(transcriptPath).mtimeMs;
94091
+ if (statFailing) deps.log("transcript files readable again");
94092
+ statFailing = false;
94093
+ return mtimeMs;
94094
+ } catch (error63) {
94095
+ if (errorCode(error63) === "ENOENT") return null;
94096
+ if (!statFailing) {
94097
+ deps.log(`\u26A0 transcript file not read (${errorCode(error63)}), its status waits for the next sync pass`);
94098
+ }
94099
+ statFailing = true;
94100
+ return null;
94101
+ }
94102
+ }
94103
+ function noteWrite(harness, transcriptPath) {
94104
+ if (writeTimers.has(transcriptPath)) return;
94105
+ const timer = setTimeout(() => {
94106
+ writeTimers.delete(transcriptPath);
94107
+ if (stopped) return;
94108
+ applyWatchedWrite(harness, transcriptPath);
94109
+ settle2();
94110
+ }, TRANSCRIPT_WRITE_DELAY_MS);
94111
+ timer.unref?.();
94112
+ writeTimers.set(transcriptPath, timer);
94113
+ }
92930
94114
  const journal = createLifecycleJournalReader(deps.home, void 0, apply);
92931
94115
  function settle2() {
92932
94116
  armDeadline();
@@ -92963,6 +94147,7 @@ function createSessionStatusFeed(deps) {
92963
94147
  return {
92964
94148
  journal,
92965
94149
  forwardWatchEvents: (onEvent) => (change) => {
94150
+ if (typeof change === "object" && !stopped) noteWrite(change.harness, change.transcript);
92966
94151
  if (change === "lifecycle" && !stopped && !readQueued) {
92967
94152
  readQueued = true;
92968
94153
  queueMicrotask(() => {
@@ -92974,19 +94159,10 @@ function createSessionStatusFeed(deps) {
92974
94159
  },
92975
94160
  observeSessions(sessions) {
92976
94161
  for (const { item, mtimeMs } of sessions) {
92977
- const hookId = hookIdOf(item.harness)?.(item.harnessSessionId);
92978
- if (hookId !== void 0) {
92979
- const key = pairKey(item.harness, hookId);
92980
- storedIds.delete(key);
92981
- storedIds.set(key, item.harnessSessionId);
92982
- if (storedIds.size > HOOK_ID_PAIRS_MAX) storedIds.delete(storedIds.keys().next().value);
92983
- }
92984
- const change = model.applyTranscriptWrite({ harness: item.harness, sessionId: item.harnessSessionId, atMs: mtimeMs });
92985
- if (change !== null) changedInBatch = true;
92986
- }
92987
- const waiting = held;
92988
- held = [];
92989
- for (const entry of waiting) apply(entry);
94162
+ learnPair(item.harness, item.harnessSessionId);
94163
+ applyWrite(item.harness, item.harnessSessionId, mtimeMs);
94164
+ }
94165
+ releaseHeld();
92990
94166
  settle2();
92991
94167
  },
92992
94168
  deleteExpired() {
@@ -92999,6 +94175,8 @@ function createSessionStatusFeed(deps) {
92999
94175
  stop() {
93000
94176
  stopped = true;
93001
94177
  if (deadlineTimer) clearTimeout(deadlineTimer);
94178
+ for (const timer of writeTimers.values()) clearTimeout(timer);
94179
+ writeTimers.clear();
93002
94180
  }
93003
94181
  };
93004
94182
  }
@@ -93043,14 +94221,20 @@ function createWatchLoop(deps) {
93043
94221
  const pendingCompletions = /* @__PURE__ */ new Map();
93044
94222
  let lifecycleChanged = false;
93045
94223
  const knownTranscripts = /* @__PURE__ */ new Set();
93046
- const rememberTranscript = (transcript) => {
93047
- knownTranscripts.delete(transcript);
93048
- knownTranscripts.add(transcript);
93049
- if (knownTranscripts.size > KNOWN_TRANSCRIPTS_MAX) {
93050
- const [oldest] = knownTranscripts;
93051
- if (oldest !== void 0) knownTranscripts.delete(oldest);
94224
+ const knownSessions = /* @__PURE__ */ new Set();
94225
+ const remember = (known, value) => {
94226
+ known.delete(value);
94227
+ known.add(value);
94228
+ if (known.size > KNOWN_TRANSCRIPTS_MAX) {
94229
+ const [oldest] = known;
94230
+ if (oldest !== void 0) known.delete(oldest);
93052
94231
  }
93053
94232
  };
94233
+ const rememberTranscript = (transcript) => remember(knownTranscripts, transcript);
94234
+ const rememberSession = (session) => {
94235
+ rememberTranscript(session.transcriptPath);
94236
+ remember(knownSessions, sessionKey(session.item));
94237
+ };
93054
94238
  const newTranscripts = /* @__PURE__ */ new Map();
93055
94239
  let newSessionTimer = null;
93056
94240
  let newSessionPassDue = false;
@@ -93093,13 +94277,13 @@ function createWatchLoop(deps) {
93093
94277
  running = true;
93094
94278
  deps.log(`${transcripts.length} new transcript file(s), uploading new session records now`);
93095
94279
  const startedAt = now();
93096
- const known = new Set(knownTranscripts);
94280
+ const known = new Set(knownSessions);
93097
94281
  try {
93098
94282
  const summary = await runNewSessionPass({
93099
94283
  sinceMs: Math.min(...transcripts.map(([, firstWriteAt]) => firstWriteAt)) - NARROW_OVERLAP_MS,
93100
- isNew: (transcriptPath) => !known.has(transcriptPath)
94284
+ isNew: (session) => !known.has(sessionKey(session.item))
93101
94285
  });
93102
- for (const session of summary.sessions) rememberTranscript(session.transcriptPath);
94286
+ for (const session of summary.sessions) rememberSession(session);
93103
94287
  if (summary.upserted > 0) deps.onNewSessionsUploaded?.(now());
93104
94288
  } catch (error63) {
93105
94289
  const holdMs = Math.max(FAILED_PASS_RETRY_MS, retryAfterMs(error63) ?? 0);
@@ -93179,15 +94363,15 @@ function createWatchLoop(deps) {
93179
94363
  if (narrowSinceMs === void 0) startupOwed = false;
93180
94364
  for (const [key] of due) pendingCompletions.delete(key);
93181
94365
  for (const session of summary.sessions) {
93182
- rememberTranscript(session.transcriptPath);
94366
+ rememberSession(session);
93183
94367
  newTranscripts.delete(session.transcriptPath);
93184
94368
  if (session.item.status === "active") {
93185
- pendingCompletions.set(session.transcriptPath, {
94369
+ pendingCompletions.set(sessionKey(session.item), {
93186
94370
  mtimeMs: session.mtimeMs,
93187
94371
  dueAt: session.mtimeMs + ACTIVE_GRACE_MS + COMPLETION_SLACK_MS
93188
94372
  });
93189
94373
  } else {
93190
- pendingCompletions.delete(session.transcriptPath);
94374
+ pendingCompletions.delete(sessionKey(session.item));
93191
94375
  }
93192
94376
  }
93193
94377
  consecutiveFailures = 0;
@@ -93280,8 +94464,16 @@ function createWatchLoop(deps) {
93280
94464
  function sessionStoreWatchRoots(home) {
93281
94465
  return [
93282
94466
  // <project folder>/<session id>.jsonl. Subagent files are one level deeper.
93283
- { dir: path21.join(home, ".claude", "projects"), recursive: true, transcripts: /^[^/\\]+[/\\][^/\\]+\.jsonl$/ },
93284
- { dir: path21.join(home, ".codex", "sessions"), recursive: true, transcripts: /(?:^|[/\\])rollout-[^/\\]+\.jsonl$/ },
94467
+ {
94468
+ dir: path21.join(home, ".claude", "projects"),
94469
+ recursive: true,
94470
+ transcripts: { harness: "claude-local", path: /^[^/\\]+[/\\][^/\\]+\.jsonl$/ }
94471
+ },
94472
+ {
94473
+ dir: path21.join(home, ".codex", "sessions"),
94474
+ recursive: true,
94475
+ transcripts: { harness: "codex-local", path: /(?:^|[/\\])rollout-[^/\\]+\.jsonl$/ }
94476
+ },
93285
94477
  { dir: path21.join(home, ".codex", "archived_sessions"), recursive: true },
93286
94478
  // Cursor keeps every local session in one SQLite store here. Its WAL
93287
94479
  // file changes on session writes, so a non-recursive watch on the
@@ -93294,7 +94486,10 @@ function sessionStoreWatchRoots(home) {
93294
94486
  dir: path21.join(home, ".cursor", "projects"),
93295
94487
  recursive: true,
93296
94488
  cursorTranscripts: true,
93297
- transcripts: /(?:^|[/\\])agent-transcripts[/\\](?:([^/\\]+)[/\\]\1|[^/\\]+)\.jsonl$/
94489
+ transcripts: {
94490
+ harness: "cursor-local",
94491
+ path: /(?:^|[/\\])agent-transcripts[/\\](?:([^/\\]+)[/\\]\1|[^/\\]+)\.jsonl$/
94492
+ }
93298
94493
  }
93299
94494
  ];
93300
94495
  }
@@ -93314,7 +94509,9 @@ function lifecycleWatchRoots(home) {
93314
94509
  function watchEventChange(root, filename) {
93315
94510
  const name = filename?.toString();
93316
94511
  if (root.cursorTranscripts && name !== void 0 && !/(?:^|[/\\])agent-transcripts[/\\].*\.jsonl$/.test(name)) return "ignore";
93317
- if (name !== void 0 && root.transcripts?.test(name)) return { transcript: path21.join(root.dir, name) };
94512
+ if (name !== void 0 && root.transcripts?.path.test(name)) {
94513
+ return { transcript: path21.join(root.dir, name), harness: root.transcripts.harness };
94514
+ }
93318
94515
  if (!root.lifecycle) return void 0;
93319
94516
  return name === void 0 || isLifecycleJournalFile(name) || name === SESSION_CHANGE_FILENAME ? "lifecycle" : void 0;
93320
94517
  }
@@ -93334,11 +94531,13 @@ function watchSessionStores(roots, onEvent, onLost) {
93334
94531
  function newSessionSyncOptions(shared, sinceMs, isNew) {
93335
94532
  return {
93336
94533
  lifecycleJournal: shared.lifecycleJournal,
94534
+ codexThreadLinks: shared.codexThreadLinks,
94535
+ codexThreadReads: shared.codexThreadReads,
93337
94536
  log: shared.log,
93338
94537
  enrollCommand: shared.enrollCommand,
93339
94538
  explicitSince: String(STEADY_SYNC_WINDOW_DAYS),
93340
94539
  narrowSinceMs: sinceMs,
93341
- isNewSession: (session) => isNew(session.transcriptPath)
94540
+ isNewSession: isNew
93342
94541
  };
93343
94542
  }
93344
94543
  function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
@@ -93533,15 +94732,19 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
93533
94732
  });
93534
94733
  const rawShipped = /* @__PURE__ */ new Map();
93535
94734
  const previewRegistrations = /* @__PURE__ */ new Map();
94735
+ const codexThreadLinks = new CodexThreadLinks();
94736
+ const codexThreadReads = new CodexThreadReads();
93536
94737
  const loop = createWatchLoop({
93537
94738
  runPass: async (options) => {
93538
94739
  let uploaded = 0;
93539
94740
  let activeSinceMs = Infinity;
93540
- const activePaths = /* @__PURE__ */ new Set();
94741
+ const activeSessions = /* @__PURE__ */ new Set();
93541
94742
  let lastCatchupReportAt = -Infinity;
93542
94743
  const shared = {
93543
94744
  rawShipped,
93544
94745
  previewRegistrations,
94746
+ codexThreadLinks,
94747
+ codexThreadReads,
93545
94748
  lifecycleJournal: journal,
93546
94749
  log: log3,
93547
94750
  enrollCommand: runtime.enrollCommand,
@@ -93558,11 +94761,11 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
93558
94761
  const recent = await runSyncPass(config2, recentSyncOptions(
93559
94762
  shared,
93560
94763
  Math.min(narrowSinceMs, activeSinceMs),
93561
- { sinceMs: narrowSinceMs, paths: activePaths }
94764
+ { sinceMs: narrowSinceMs, sessions: activeSessions }
93562
94765
  ));
93563
94766
  activeSinceMs = recentCatchupFloor(activeSinceMs, recent.sessions);
93564
94767
  for (const session of recent.sessions) {
93565
- if (session.item.status === "active") activePaths.add(session.transcriptPath);
94768
+ if (session.item.status === "active") activeSessions.add(sessionKey(session.item));
93566
94769
  }
93567
94770
  return recent;
93568
94771
  });
@@ -93572,7 +94775,7 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
93572
94775
  beforeRawUpload: async (sessions) => {
93573
94776
  activeSinceMs = recentCatchupFloor(activeSinceMs, sessions);
93574
94777
  for (const session of sessions) {
93575
- if (session.item.status === "active") activePaths.add(session.transcriptPath);
94778
+ if (session.item.status === "active") activeSessions.add(sessionKey(session.item));
93576
94779
  }
93577
94780
  const beforeCatchup = uploaded;
93578
94781
  try {
@@ -93595,7 +94798,7 @@ function startWatch(config2, homeDir, runtime = { version: "checkout" }) {
93595
94798
  }
93596
94799
  return summary;
93597
94800
  },
93598
- runNewSessionPass: ({ sinceMs, isNew }) => runSyncPass(config2, newSessionSyncOptions({ lifecycleJournal: journal, log: log3, enrollCommand: runtime.enrollCommand }, sinceMs, isNew)),
94801
+ runNewSessionPass: ({ sinceMs, isNew }) => runSyncPass(config2, newSessionSyncOptions({ lifecycleJournal: journal, codexThreadLinks, codexThreadReads, log: log3, enrollCommand: runtime.enrollCommand }, sinceMs, isNew)),
93599
94802
  // An upload changes what the menu shows, so this write wakes the menu,
93600
94803
  // and the menu reads the new session from the server.
93601
94804
  onNewSessionsUploaded: (at) => writeStatus({ lastUploadAt: new Date(at).toISOString() }),
@@ -93865,7 +95068,7 @@ function hookPreferencesPath(appEnv, home) {
93865
95068
  function readHookPreferences(appEnv, home) {
93866
95069
  const file2 = hookPreferencesPath(appEnv, home);
93867
95070
  try {
93868
- return preferencesSchema.parse(JSON.parse(readFileSync12(file2, "utf8")));
95071
+ return preferencesSchema.parse(JSON.parse(readFileSync13(file2, "utf8")));
93869
95072
  } catch (error63) {
93870
95073
  if (error63 instanceof Error && "code" in error63 && error63.code === "ENOENT") {
93871
95074
  return { version: 1, codexNotFiringDismissed: false };
@@ -93887,12 +95090,12 @@ function dismissCodexHookWarning(appEnv, home) {
93887
95090
  const file2 = hookPreferencesPath(appEnv, home);
93888
95091
  mkdirSync12(path24.dirname(file2), { recursive: true });
93889
95092
  const temporary = `${file2}.${process.pid}.tmp`;
93890
- writeFileSync10(temporary, JSON.stringify({ ...preferences, codexNotFiringDismissed: true }, null, 2), { mode: 384 });
93891
- renameSync6(temporary, file2);
95093
+ writeFileSync9(temporary, JSON.stringify({ ...preferences, codexNotFiringDismissed: true }, null, 2), { mode: 384 });
95094
+ renameSync7(temporary, file2);
93892
95095
  }
93893
95096
 
93894
95097
  // src/mcpPreferences.ts
93895
- import { mkdirSync as mkdirSync13, readFileSync as readFileSync13, renameSync as renameSync7, writeFileSync as writeFileSync11 } from "node:fs";
95098
+ import { mkdirSync as mkdirSync13, readFileSync as readFileSync14, renameSync as renameSync8, writeFileSync as writeFileSync10 } from "node:fs";
93896
95099
  import path25 from "node:path";
93897
95100
  var preferenceSchema = external_exports.object({
93898
95101
  optedIn: external_exports.boolean().optional(),
@@ -93913,7 +95116,7 @@ function mcpPreferencesPath(appEnv, home) {
93913
95116
  function readMcpPreferences(appEnv, home) {
93914
95117
  const file2 = mcpPreferencesPath(appEnv, home);
93915
95118
  try {
93916
- return preferencesSchema2.parse(JSON.parse(readFileSync13(file2, "utf8")));
95119
+ return preferencesSchema2.parse(JSON.parse(readFileSync14(file2, "utf8")));
93917
95120
  } catch (error63) {
93918
95121
  if (error63 instanceof Error && "code" in error63 && error63.code === "ENOENT") return emptyMcpPreferences();
93919
95122
  throw new ExpectedCliError(`Cannot read MCP preferences at ${file2}. Restore or remove that file, then retry.`, { cause: error63 });
@@ -93923,8 +95126,8 @@ function writeMcpPreferences(appEnv, preferences, home) {
93923
95126
  const file2 = mcpPreferencesPath(appEnv, home);
93924
95127
  mkdirSync13(path25.dirname(file2), { recursive: true });
93925
95128
  const temporary = `${file2}.${process.pid}.tmp`;
93926
- writeFileSync11(temporary, JSON.stringify(preferencesSchema2.parse(preferences), null, 2), { mode: 384 });
93927
- renameSync7(temporary, file2);
95129
+ writeFileSync10(temporary, JSON.stringify(preferencesSchema2.parse(preferences), null, 2), { mode: 384 });
95130
+ renameSync8(temporary, file2);
93928
95131
  }
93929
95132
  function recordMcpChoices(appEnv, offered, selected, home) {
93930
95133
  const preferences = readMcpPreferences(appEnv, home);
@@ -94087,7 +95290,7 @@ function sessionLinks(session, webUrl, options) {
94087
95290
  }
94088
95291
  };
94089
95292
  case "codex": {
94090
- const threadId2 = codexThreadId(session.externalId);
95293
+ const threadId2 = codexThreadId2(session.externalId);
94091
95294
  if (threadId2 === null || !options.localMachine) return { powerplantUrl };
94092
95295
  return {
94093
95296
  powerplantUrl,
@@ -94104,7 +95307,7 @@ function sessionLinks(session, webUrl, options) {
94104
95307
  }
94105
95308
  }
94106
95309
  var TRAILING_UUID = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
94107
- function codexThreadId(externalId) {
95310
+ function codexThreadId2(externalId) {
94108
95311
  return TRAILING_UUID.exec(externalId)?.[1] ?? null;
94109
95312
  }
94110
95313
  var CODEX_APP_BUNDLES = ["ChatGPT.app", "Codex.app"];
@@ -94254,7 +95457,7 @@ async function probeClaudeDesktopSessions(wanted, installed, reportFault, root =
94254
95457
 
94255
95458
  // src/menuModel.ts
94256
95459
  import { randomUUID as randomUUID5 } from "node:crypto";
94257
- import { mkdirSync as mkdirSync17, rmSync as rmSync9, watch as fsWatch3 } from "node:fs";
95460
+ import { mkdirSync as mkdirSync17, rmSync as rmSync10, watch as fsWatch3 } from "node:fs";
94258
95461
  import path34 from "node:path";
94259
95462
 
94260
95463
  // src/menu.ts
@@ -94276,15 +95479,15 @@ function ago(ms) {
94276
95479
  }
94277
95480
 
94278
95481
  // src/update.ts
94279
- import { mkdirSync as mkdirSync14, readFileSync as readFileSync15, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "node:fs";
95482
+ import { mkdirSync as mkdirSync14, readFileSync as readFileSync16, rmSync as rmSync8, writeFileSync as writeFileSync11 } from "node:fs";
94280
95483
  import path29 from "node:path";
94281
95484
 
94282
95485
  // src/installOwner.ts
94283
- import { accessSync, constants as constants2, readFileSync as readFileSync14 } from "node:fs";
95486
+ import { accessSync, constants as constants2, readFileSync as readFileSync15 } from "node:fs";
94284
95487
  import path28 from "node:path";
94285
95488
 
94286
95489
  // src/version.ts
94287
- var CLI_VERSION = true ? "0.4.18" : "0.0.0-checkout";
95490
+ var CLI_VERSION = true ? "0.4.19" : "0.0.0-checkout";
94288
95491
  var CLI_RELEASE_BUILD = true ? true : false;
94289
95492
  var CLI_PACKAGE_NAME = "@usepowerplant/cli";
94290
95493
 
@@ -94385,7 +95588,7 @@ function realInspectEnvironment() {
94385
95588
  pnpmHome: process.env.PNPM_HOME,
94386
95589
  readTextFile: (file2) => {
94387
95590
  try {
94388
- return readFileSync14(file2, "utf8");
95591
+ return readFileSync15(file2, "utf8");
94389
95592
  } catch (error63) {
94390
95593
  if (error63 instanceof Error && "code" in error63 && error63.code === "ENOENT") return null;
94391
95594
  throw error63;
@@ -94451,18 +95654,18 @@ function updateProgressFile(report, homeDir) {
94451
95654
  return {
94452
95655
  read: () => readUpdateProgress((filePath) => {
94453
95656
  try {
94454
- return readFileSync15(filePath, "utf8");
95657
+ return readFileSync16(filePath, "utf8");
94455
95658
  } catch {
94456
95659
  return null;
94457
95660
  }
94458
95661
  }, file2),
94459
95662
  write: (record2) => {
94460
95663
  mkdirSync14(path29.dirname(file2), { recursive: true });
94461
- writeFileSync12(file2, JSON.stringify(record2, null, 2));
95664
+ writeFileSync11(file2, JSON.stringify(record2, null, 2));
94462
95665
  wakeMenus2();
94463
95666
  },
94464
95667
  clear: () => {
94465
- rmSync7(file2, { force: true });
95668
+ rmSync8(file2, { force: true });
94466
95669
  wakeMenus2();
94467
95670
  }
94468
95671
  };
@@ -95231,7 +96434,7 @@ function writeVersionCache(options) {
95231
96434
  }
95232
96435
 
95233
96436
  // src/menuSnapshot.ts
95234
- import { chmodSync as chmodSync5, mkdirSync as mkdirSync15, readFileSync as readFileSync16, renameSync as renameSync8, rmSync as rmSync8, statSync as statSync5, writeFileSync as writeFileSync13 } from "node:fs";
96437
+ import { chmodSync as chmodSync4, mkdirSync as mkdirSync15, readFileSync as readFileSync17, renameSync as renameSync9, rmSync as rmSync9, statSync as statSync6, writeFileSync as writeFileSync12 } from "node:fs";
95235
96438
  import path31 from "node:path";
95236
96439
  var MENU_SNAPSHOT_VERSION = 1;
95237
96440
  var MENU_SNAPSHOT_STALE_AFTER_MS = 3 * 6e4;
@@ -95276,19 +96479,19 @@ var snapshotFileSchema = external_exports.object({
95276
96479
  function writeMenuSnapshot(file2, snapshot) {
95277
96480
  const dir = path31.dirname(file2);
95278
96481
  mkdirSync15(dir, { recursive: true, mode: 448 });
95279
- chmodSync5(dir, 448);
96482
+ chmodSync4(dir, 448);
95280
96483
  const temporary = `${file2}.${process.pid}.tmp`;
95281
96484
  try {
95282
- writeFileSync13(temporary, encodeMenuPayload(snapshot), { mode: 384 });
95283
- renameSync8(temporary, file2);
96485
+ writeFileSync12(temporary, encodeMenuPayload(snapshot), { mode: 384 });
96486
+ renameSync9(temporary, file2);
95284
96487
  } finally {
95285
- rmSync8(temporary, { force: true });
96488
+ rmSync9(temporary, { force: true });
95286
96489
  }
95287
96490
  }
95288
96491
  function readMenuSnapshot(options) {
95289
96492
  let raw;
95290
96493
  try {
95291
- raw = readFileSync16(options.file, "utf8");
96494
+ raw = readFileSync17(options.file, "utf8");
95292
96495
  } catch (error63) {
95293
96496
  return { outcome: error63 instanceof Error && "code" in error63 && error63.code === "ENOENT" ? "missing" : "unreadable" };
95294
96497
  }
@@ -95328,7 +96531,7 @@ function isPidAlive2(pid) {
95328
96531
  }
95329
96532
  function requestedAfter(file2, writtenAtMs) {
95330
96533
  try {
95331
- return statSync5(file2).mtimeMs > writtenAtMs;
96534
+ return statSync6(file2).mtimeMs > writtenAtMs;
95332
96535
  } catch {
95333
96536
  return false;
95334
96537
  }
@@ -95336,7 +96539,7 @@ function requestedAfter(file2, writtenAtMs) {
95336
96539
 
95337
96540
  // src/connect.ts
95338
96541
  import { spawnSync as spawnSync2 } from "node:child_process";
95339
- import { mkdirSync as mkdirSync16, readFileSync as readFileSync17, statSync as statSync6, writeFileSync as writeFileSync14 } from "node:fs";
96542
+ import { mkdirSync as mkdirSync16, readFileSync as readFileSync18, statSync as statSync7, writeFileSync as writeFileSync13 } from "node:fs";
95340
96543
  import os23 from "node:os";
95341
96544
  import path32 from "node:path";
95342
96545
  import { emitKeypressEvents } from "node:readline";
@@ -95445,7 +96648,7 @@ function unsigned(candidate, verifySignature) {
95445
96648
  }
95446
96649
  function isExecutableFile2(filePath) {
95447
96650
  try {
95448
- const stats = statSync6(filePath);
96651
+ const stats = statSync7(filePath);
95449
96652
  return stats.isFile() && (stats.mode & 73) !== 0;
95450
96653
  } catch {
95451
96654
  return false;
@@ -96132,14 +97335,14 @@ async function fetchOfficialClients(apiUrl, log3) {
96132
97335
  }
96133
97336
  function realReadTextFile(filePath) {
96134
97337
  try {
96135
- return readFileSync17(filePath, "utf8");
97338
+ return readFileSync18(filePath, "utf8");
96136
97339
  } catch {
96137
97340
  return null;
96138
97341
  }
96139
97342
  }
96140
97343
  function realReadTextFileStrict(filePath) {
96141
97344
  try {
96142
- return readFileSync17(filePath, "utf8");
97345
+ return readFileSync18(filePath, "utf8");
96143
97346
  } catch (error63) {
96144
97347
  if (error63 instanceof Error && error63.code === "ENOENT") return null;
96145
97348
  throw error63;
@@ -96147,7 +97350,7 @@ function realReadTextFileStrict(filePath) {
96147
97350
  }
96148
97351
  function realWriteTextFile(filePath, content) {
96149
97352
  mkdirSync16(path32.dirname(filePath), { recursive: true });
96150
- writeFileSync14(filePath, content);
97353
+ writeFileSync13(filePath, content);
96151
97354
  }
96152
97355
  function realOpenUrl(url2) {
96153
97356
  const bin = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : null;
@@ -96685,7 +97888,7 @@ function startMenuModel(sources) {
96685
97888
  })
96686
97889
  ),
96687
97890
  writeSnapshot: (snapshot) => writeMenuSnapshot(snapshotFile, snapshot),
96688
- removeSnapshot: () => rmSync9(snapshotFile, { force: true }),
97891
+ removeSnapshot: () => rmSync10(snapshotFile, { force: true }),
96689
97892
  wake: () => signalMenu(appEnv, host.home),
96690
97893
  watchRefreshRequests: (onRequest, onLost) => {
96691
97894
  const dir = path34.dirname(menuRefreshRequestPath(appEnv, host.home));
@@ -96708,7 +97911,7 @@ import path37 from "node:path";
96708
97911
  import { parseArgs } from "node:util";
96709
97912
 
96710
97913
  // src/doctor.ts
96711
- import { closeSync as closeSync8, fstatSync as fstatSync2, openSync as openSync8, readSync as readSync6 } from "node:fs";
97914
+ import { closeSync as closeSync8, fstatSync as fstatSync3, openSync as openSync8, readSync as readSync6 } from "node:fs";
96712
97915
  var DOCTOR_LOG_TAIL_LINES = 50;
96713
97916
  var LOG_TAIL_READ_BYTES = 256 * 1024;
96714
97917
  function installLine(install) {
@@ -96730,7 +97933,7 @@ function readDoctorLogTail(path38) {
96730
97933
  return { path: path38, lines: null, readError: error63 instanceof Error ? error63.message : String(error63) };
96731
97934
  }
96732
97935
  try {
96733
- const size = fstatSync2(fd).size;
97936
+ const size = fstatSync3(fd).size;
96734
97937
  const offset = Math.max(0, size - LOG_TAIL_READ_BYTES);
96735
97938
  const buffer = Buffer.alloc(Math.min(size, LOG_TAIL_READ_BYTES));
96736
97939
  readSync6(fd, buffer, 0, buffer.length, offset);
@@ -97813,7 +99016,7 @@ async function tryCredentialUpgradeLock(file2) {
97813
99016
  }
97814
99017
 
97815
99018
  // src/menubarApp.ts
97816
- import { existsSync as existsSync14, mkdirSync as mkdirSync19, renameSync as renameSync9, rmSync as rmSync10 } from "node:fs";
99019
+ import { existsSync as existsSync14, mkdirSync as mkdirSync19, renameSync as renameSync10, rmSync as rmSync11 } from "node:fs";
97817
99020
  import os25 from "node:os";
97818
99021
  import path36 from "node:path";
97819
99022
  import { spawnSync as spawnSync4 } from "node:child_process";
@@ -97883,8 +99086,8 @@ function realMenubarAppOps(input2) {
97883
99086
  mkdirSync19(parent, { recursive: true });
97884
99087
  const staging = path36.join(parent, `.${MENUBAR_APP_NAME}.swap-${process.pid}`);
97885
99088
  const retired = path36.join(parent, `.${MENUBAR_APP_NAME}.old-${process.pid}`);
97886
- rmSync10(staging, { recursive: true, force: true });
97887
- rmSync10(retired, { recursive: true, force: true });
99089
+ rmSync11(staging, { recursive: true, force: true });
99090
+ rmSync11(retired, { recursive: true, force: true });
97888
99091
  try {
97889
99092
  const extract = spawnSync4("ditto", ["-xk", zip, staging]);
97890
99093
  if (extract.status !== 0) {
@@ -97897,16 +99100,16 @@ function realMenubarAppOps(input2) {
97897
99100
  throw new Error(`${zip} does not contain ${MENUBAR_APP_NAME}`);
97898
99101
  }
97899
99102
  const hadOld = existsSync14(appPath);
97900
- if (hadOld) renameSync9(appPath, retired);
99103
+ if (hadOld) renameSync10(appPath, retired);
97901
99104
  try {
97902
- renameSync9(extracted, appPath);
99105
+ renameSync10(extracted, appPath);
97903
99106
  } catch (error63) {
97904
- if (hadOld) renameSync9(retired, appPath);
99107
+ if (hadOld) renameSync10(retired, appPath);
97905
99108
  throw error63;
97906
99109
  }
97907
99110
  } finally {
97908
- rmSync10(staging, { recursive: true, force: true });
97909
- rmSync10(retired, { recursive: true, force: true });
99111
+ rmSync11(staging, { recursive: true, force: true });
99112
+ rmSync11(retired, { recursive: true, force: true });
97910
99113
  }
97911
99114
  },
97912
99115
  launch: (appPath) => {
@@ -98890,4 +100093,4 @@ undici/lib/web/websocket/frame.js:
98890
100093
  (*! ws. MIT License. Einar Otto Stangvik <einaros@gmail.com> *)
98891
100094
  */
98892
100095
 
98893
- //# debugId=7730a7ff-ca8b-5911-9406-4ee9329a7ea8
100096
+ //# debugId=5ecc7184-ee4b-53d3-858b-4d0790456d70