iteration-cli 0.14.1 → 0.14.3

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
@@ -939,7 +939,7 @@ import { promises as fs20 } from "node:fs";
939
939
  import { createInterface as createInterface2 } from "node:readline/promises";
940
940
  import { parseArgs } from "node:util";
941
941
 
942
- // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/core/util.js
942
+ // ../../node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/util.js
943
943
  var util_exports = {};
944
944
  __export(util_exports, {
945
945
  BIGINT_FORMAT_RANGES: () => BIGINT_FORMAT_RANGES,
@@ -1010,6 +1010,7 @@ __export(util_exports, {
1010
1010
  required: () => required,
1011
1011
  safeExtend: () => safeExtend,
1012
1012
  shallowClone: () => shallowClone,
1013
+ sizeCaps: () => sizeCaps,
1013
1014
  slugify: () => slugify,
1014
1015
  stringifyPrimitive: () => stringifyPrimitive,
1015
1016
  toZod: () => toZod,
@@ -1529,6 +1530,18 @@ function prefixIssues(path25, issues) {
1529
1530
  function unwrapMessage(message) {
1530
1531
  return typeof message === "string" ? message : message?.message;
1531
1532
  }
1533
+ function sizeCaps(defChecks) {
1534
+ const caps = [];
1535
+ for (const ch of defChecks ?? []) {
1536
+ const d = ch._zod.def;
1537
+ if (!/_(length|size)$|_equals$/.test(d.check))
1538
+ break;
1539
+ const max = d.maximum ?? d.length ?? d.size;
1540
+ if (max >= 0)
1541
+ caps.push([max, ch]);
1542
+ }
1543
+ return caps;
1544
+ }
1532
1545
  function attachSchema(issues, start2, inst) {
1533
1546
  var _a3;
1534
1547
  for (let i = start2; i < issues.length; i++) {
@@ -1782,7 +1795,7 @@ function constantCatch(value) {
1782
1795
  return fn;
1783
1796
  }
1784
1797
 
1785
- // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/core/core.js
1798
+ // ../../node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/core.js
1786
1799
  var _a;
1787
1800
  var _zodDesc = { value: void 0, enumerable: false };
1788
1801
  var _E = "captureStackTrace" in Error ? Error : null;
@@ -1900,7 +1913,7 @@ function config(newConfig) {
1900
1913
  return globalConfig;
1901
1914
  }
1902
1915
 
1903
- // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/core/errors.js
1916
+ // ../../node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/errors.js
1904
1917
  function _getMessage() {
1905
1918
  const internals = this._zod;
1906
1919
  internals.message ?? (internals.message = JSON.stringify(internals.def, jsonStringifyReplacer, 2));
@@ -2015,7 +2028,7 @@ function formatError(error2, mapper = (issue2) => issue2.message) {
2015
2028
  return fieldErrors;
2016
2029
  }
2017
2030
 
2018
- // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/core/parse.js
2031
+ // ../../node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/parse.js
2019
2032
  function finalizeParams(callee, params) {
2020
2033
  return { callee: params?.callee ?? callee, Err: params?.Err };
2021
2034
  }
@@ -2163,7 +2176,7 @@ var _safeDecodeAsync = (_Err) => async (schema, value, _ctx) => {
2163
2176
  return _safeParseAsync(_Err)(schema, value, _ctx);
2164
2177
  };
2165
2178
 
2166
- // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/core/regexes.js
2179
+ // ../../node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/regexes.js
2167
2180
  var cuid = /^[cC][0-9a-z]{6,}$/;
2168
2181
  var cuid2 = /^[0-9a-z]+$/;
2169
2182
  var ulid = /^[0-7][0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{25}$/;
@@ -2223,7 +2236,7 @@ var _null = /^null$/i;
2223
2236
  var lowercase = /^[^A-Z]*$/;
2224
2237
  var uppercase = /^[^a-z]*$/;
2225
2238
 
2226
- // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/core/checks.js
2239
+ // ../../node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/checks.js
2227
2240
  var $ZodCheck = /* @__PURE__ */ $constructor("$ZodCheck", (inst, def) => {
2228
2241
  var _a3;
2229
2242
  inst._zod ?? (inst._zod = {});
@@ -2543,7 +2556,7 @@ var $ZodCheckOverwrite = /* @__PURE__ */ $constructor("$ZodCheckOverwrite", (ins
2543
2556
  };
2544
2557
  });
2545
2558
 
2546
- // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/core/doc.js
2559
+ // ../../node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/doc.js
2547
2560
  var Doc = class {
2548
2561
  constructor(args = [], closed = {}) {
2549
2562
  this.content = [];
@@ -2584,14 +2597,14 @@ ${content.join("\n")}
2584
2597
  }
2585
2598
  };
2586
2599
 
2587
- // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/core/versions.js
2600
+ // ../../node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/versions.js
2588
2601
  var version = {
2589
2602
  major: 4,
2590
2603
  minor: 6,
2591
- patch: 4
2604
+ patch: 6
2592
2605
  };
2593
2606
 
2594
- // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/core/schemas.js
2607
+ // ../../node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/schemas.js
2595
2608
  var $ZodType = /* @__PURE__ */ $constructor("$ZodType", (inst, def) => {
2596
2609
  var _a3;
2597
2610
  inst ?? (inst = {});
@@ -3206,6 +3219,29 @@ var $ZodDate = /* @__PURE__ */ $constructor("$ZodDate", (inst, def) => {
3206
3219
  return payload;
3207
3220
  };
3208
3221
  });
3222
+ function capSize(inst, size) {
3223
+ const caps = sizeCaps(inst._zod.def.checks);
3224
+ if (!caps.length)
3225
+ return;
3226
+ const cap = Math.min(...caps.map((c) => c[0]));
3227
+ const parse2 = inst._zod.parse;
3228
+ inst._zod.parse = (payload, ctx) => {
3229
+ const n = size(payload.value);
3230
+ if (n > cap && !ctx?.skipChecks && ctx?.direction !== "backward") {
3231
+ for (const [max, ch] of sizeCaps(inst._zod.def.checks)) {
3232
+ if (n > max && ch._zod.def.when(payload)) {
3233
+ const start2 = payload.issues.length;
3234
+ ch._zod.check(payload);
3235
+ payload.issues[start2].continue = false;
3236
+ attachSchema(payload.issues, start2, inst);
3237
+ return payload;
3238
+ }
3239
+ }
3240
+ }
3241
+ return parse2(payload, ctx);
3242
+ };
3243
+ }
3244
+ var arraySize = (input) => Array.isArray(input) ? input.length : -1;
3209
3245
  function handleArrayResult(result, final, index) {
3210
3246
  if (result.issues.length) {
3211
3247
  final.issues.push(...prefixIssues(index, result.issues));
@@ -3249,6 +3285,7 @@ var $ZodArray = /* @__PURE__ */ $constructor("$ZodArray", (inst, def) => {
3249
3285
  }
3250
3286
  return payload;
3251
3287
  };
3288
+ capSize(inst, arraySize);
3252
3289
  });
3253
3290
  function handlePropertyResult(result, final, key, input, optin, optout) {
3254
3291
  const isPresent = key in input;
@@ -4292,7 +4329,7 @@ function handleRefineResult(result, payload, input, inst) {
4292
4329
  }
4293
4330
  }
4294
4331
 
4295
- // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/core/memoizer.js
4332
+ // ../../node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/memoizer.js
4296
4333
  var $ZodCyclicError = class extends Error {
4297
4334
  constructor() {
4298
4335
  super(`Cannot parse a reference cycle that closes through a transform`);
@@ -4566,7 +4603,7 @@ function isBackEdge(ctx, value) {
4566
4603
  return backEdges !== void 0 && isRef(value) && backEdges.has(value);
4567
4604
  }
4568
4605
 
4569
- // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/locales/en.js
4606
+ // ../../node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/locales/en.js
4570
4607
  var error = () => {
4571
4608
  const Sizable = {
4572
4609
  string: { unit: "characters", verb: "to have" },
@@ -4691,7 +4728,7 @@ function en_default() {
4691
4728
  };
4692
4729
  }
4693
4730
 
4694
- // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/core/registries.js
4731
+ // ../../node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/registries.js
4695
4732
  var _a2;
4696
4733
  var $ZodRegistry = class {
4697
4734
  constructor() {
@@ -4739,7 +4776,7 @@ function registry() {
4739
4776
  (_a2 = globalThis).__zod_globalRegistry ?? (_a2.__zod_globalRegistry = registry());
4740
4777
  var globalRegistry = globalThis.__zod_globalRegistry;
4741
4778
 
4742
- // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/core/api.js
4779
+ // ../../node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/api.js
4743
4780
  function snapshotChecks(def) {
4744
4781
  if (def.checks)
4745
4782
  def.checks = [...def.checks];
@@ -5286,7 +5323,7 @@ function _check(fn, params) {
5286
5323
  return ch;
5287
5324
  }
5288
5325
 
5289
- // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/core/to-json-schema.js
5326
+ // ../../node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/to-json-schema.js
5290
5327
  function assignProps(target, ...sources) {
5291
5328
  for (const source of sources) {
5292
5329
  for (const key of Reflect.ownKeys(source)) {
@@ -5816,7 +5853,7 @@ var createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params) =
5816
5853
  return finalize(ctx, schema);
5817
5854
  };
5818
5855
 
5819
- // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/core/json-schema-processors.js
5856
+ // ../../node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/json-schema-processors.js
5820
5857
  var narrowMin = (agg, key, value) => {
5821
5858
  if (agg[key] === void 0 || value > agg[key])
5822
5859
  agg[key] = value;
@@ -6366,7 +6403,7 @@ var lazyProcessor = (schema, ctx, _json, params) => {
6366
6403
  seen.ref = innerType;
6367
6404
  };
6368
6405
 
6369
- // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/classic/errors.js
6406
+ // ../../node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/classic/errors.js
6370
6407
  var _installedErrorProtos = /* @__PURE__ */ new WeakSet([Object.prototype, Error.prototype]);
6371
6408
  function _lazyMethod(proto, key, make) {
6372
6409
  Object.defineProperty(proto, key, {
@@ -6411,7 +6448,7 @@ var ZodRealError = /* @__PURE__ */ $constructor("ZodError", initializer2, void 0
6411
6448
  Parent: Error
6412
6449
  });
6413
6450
 
6414
- // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/classic/parse.js
6451
+ // ../../node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/classic/parse.js
6415
6452
  var parse = /* @__PURE__ */ _parse(ZodRealError);
6416
6453
  var parseAsync = /* @__PURE__ */ _parseAsync(ZodRealError);
6417
6454
  var safeParse = /* @__PURE__ */ _safeParse(ZodRealError);
@@ -6425,7 +6462,7 @@ var safeDecode = /* @__PURE__ */ _safeDecode(ZodRealError);
6425
6462
  var safeEncodeAsync = /* @__PURE__ */ _safeEncodeAsync(ZodRealError);
6426
6463
  var safeDecodeAsync = /* @__PURE__ */ _safeDecodeAsync(ZodRealError);
6427
6464
 
6428
- // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/classic/schemas.js
6465
+ // ../../node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/classic/schemas.js
6429
6466
  function _ensureDefaultLocale() {
6430
6467
  if (!globalConfig.localeError)
6431
6468
  config(en_default());
@@ -7447,7 +7484,7 @@ function json(params) {
7447
7484
  return jsonSchema;
7448
7485
  }
7449
7486
 
7450
- // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/classic/iso.js
7487
+ // ../../node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/classic/iso.js
7451
7488
  var iso_exports = {};
7452
7489
  __export(iso_exports, {
7453
7490
  ZodISODate: () => ZodISODate,
@@ -7472,7 +7509,7 @@ function duration2(params) {
7472
7509
  return _isoDuration(ZodISODuration, params);
7473
7510
  }
7474
7511
 
7475
- // ../../node_modules/.pnpm/zod@4.6.4/node_modules/zod/v4/classic/coerce.js
7512
+ // ../../node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/classic/coerce.js
7476
7513
  var coerce_exports = {};
7477
7514
  __export(coerce_exports, {
7478
7515
  bigint: () => bigint2,
@@ -7497,7 +7534,7 @@ function date3(params) {
7497
7534
  return _coercedDate(ZodDate, params);
7498
7535
  }
7499
7536
 
7500
- // ../trace-contracts/dist/index.js
7537
+ // ../trace-contracts/dist/contracts.js
7501
7538
  var TRACE_EVENT_TYPES = [
7502
7539
  "user_message",
7503
7540
  "assistant_message",
@@ -7585,10 +7622,10 @@ var codexBillingRequestSchema = object({
7585
7622
  mode: _enum(["chatgpt", "api_key", "conflicting"])
7586
7623
  }).strict()).max(5e3)
7587
7624
  }).strict();
7588
- var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
7625
+ var UUID_PATTERN = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/;
7589
7626
  var TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
7590
7627
  var unpairedSurrogate = /[\uD800-\uDFFF]/u;
7591
- var traceIdentitySchema = string2().min(1).max(200).refine((value) => !value.includes("\0") && !unpairedSurrogate.test(value), "Trace identifiers cannot contain NUL or unpaired Unicode surrogates.");
7628
+ var traceIdentitySchema = string2().min(1).max(200).refine((value) => !value.includes("\0") && !unpairedSurrogate.test(value), "Trace identifiers cannot contain NUL or unpaired Unicode surrogates.").describe("Must not contain NUL or unpaired Unicode surrogates.");
7592
7629
  var jsonValueSchema = lazy(() => union([
7593
7630
  string2(),
7594
7631
  number2().finite(),
@@ -7619,28 +7656,28 @@ var traceEventSchema = object({
7619
7656
  sequence: number2().int().min(0).max(Number.MAX_SAFE_INTEGER),
7620
7657
  callId: string2().max(255).nullable(),
7621
7658
  projectId: string2().regex(UUID_PATTERN).nullable(),
7622
- occurredAt: string2().regex(TIMESTAMP_PATTERN).refine((value) => !Number.isNaN(Date.parse(value))).nullable(),
7659
+ occurredAt: string2().regex(TIMESTAMP_PATTERN).refine((value) => !Number.isNaN(Date.parse(value))).describe("Must also be a timestamp accepted by Date.parse.").nullable(),
7623
7660
  eventType: _enum(TRACE_EVENT_TYPES),
7624
7661
  payload: jsonPayloadSchema,
7625
7662
  run: runMarkerSchema.optional()
7626
7663
  }).strict();
7627
7664
  var traceEventBatchSchema = object({
7628
7665
  events: array(traceEventSchema).min(1).max(MAX_TRACE_EVENTS_PER_BATCH)
7629
- }).strict();
7666
+ }).strict().describe("Event IDs must be unique within the batch.");
7630
7667
  var acknowledgementSchema = object({
7631
7668
  deviceId: string2().regex(UUID_PATTERN),
7632
7669
  acceptedEventIds: array(string2())
7633
- }).strict();
7670
+ }).strict().describe("Accepted event IDs must match the submitted batch exactly, without duplicates.");
7634
7671
  var episodeCountSchema = number2().int().min(0).max(Number.MAX_SAFE_INTEGER);
7635
- var collectorFrictionSummarySchema = object({ weekEpisodes: episodeCountSchema, dayEpisodes: episodeCountSchema }).strict().refine((summary) => summary.dayEpisodes <= summary.weekEpisodes, "The last day cannot have more friction episodes than the last week.");
7672
+ var collectorFrictionSummarySchema = object({ weekEpisodes: episodeCountSchema, dayEpisodes: episodeCountSchema }).strict().refine((summary) => summary.dayEpisodes <= summary.weekEpisodes, "The last day cannot have more friction episodes than the last week.").describe("dayEpisodes must not exceed weekEpisodes.");
7636
7673
  var sessionCountSchema = number2().int().min(0).max(1e6);
7637
7674
  var collectorHistoryProgressSchema = object({
7638
7675
  sessions: sessionCountSchema,
7639
7676
  readSessions: sessionCountSchema,
7640
7677
  complete: boolean2()
7641
- }).strict().refine((progress) => progress.readSessions <= progress.sessions, "Read sessions cannot exceed listed sessions.").refine((progress) => !progress.complete || progress.readSessions === progress.sessions, "A complete import has read every listed session.");
7678
+ }).strict().refine((progress) => progress.readSessions <= progress.sessions, "Read sessions cannot exceed listed sessions.").refine((progress) => !progress.complete || progress.readSessions === progress.sessions, "A complete import has read every listed session.").describe("readSessions must not exceed sessions; complete requires equal counts.");
7642
7679
  var collectorEnrollmentRequestSchema = object({
7643
- deviceName: string2().trim().min(1).max(160),
7680
+ deviceName: string2().trim().min(1).max(160).describe("Trimmed before length validation."),
7644
7681
  organizationId: string2().regex(UUID_PATTERN).optional(),
7645
7682
  source: _enum(COLLECTOR_SOURCES).optional()
7646
7683
  }).strict();
@@ -7771,6 +7808,129 @@ import { execFile as execFile2 } from "node:child_process";
7771
7808
  import { createInterface } from "node:readline/promises";
7772
7809
  import { promisify as promisify2 } from "node:util";
7773
7810
 
7811
+ // src/collector-fetch.ts
7812
+ import { randomUUID } from "node:crypto";
7813
+ import http from "node:http";
7814
+ import https from "node:https";
7815
+
7816
+ // src/logger.ts
7817
+ function writeLog(line, level) {
7818
+ if (level === "info") console.info(line);
7819
+ else console.error(line);
7820
+ }
7821
+ function createLogger(context, write = writeLog) {
7822
+ return (entry) => {
7823
+ write(
7824
+ JSON.stringify({
7825
+ ...entry,
7826
+ ...context,
7827
+ at: (/* @__PURE__ */ new Date()).toISOString(),
7828
+ pid: process.pid,
7829
+ runtimeVersion: process.env.ITERATION_RUNTIME_VERSION ?? null
7830
+ }),
7831
+ entry.level
7832
+ );
7833
+ };
7834
+ }
7835
+ var collectorLogger = createLogger({ component: "collector" });
7836
+
7837
+ // src/collector-fetch.ts
7838
+ var MAX_RESPONSE_BYTES = 1024 * 1024;
7839
+ var NULL_BODY_STATUSES = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
7840
+ var agents = {
7841
+ "http:": new http.Agent({ keepAlive: false }),
7842
+ "https:": new https.Agent({ keepAlive: false })
7843
+ };
7844
+ var sendCollectorRequest = (input, init = {}) => {
7845
+ const url2 = new URL(input);
7846
+ if (url2.protocol !== "https:" && url2.protocol !== "http:") {
7847
+ throw new TypeError(
7848
+ `Unsupported collector request protocol ${url2.protocol}`
7849
+ );
7850
+ }
7851
+ const body = init.body ?? null;
7852
+ const headers = { ...init.headers };
7853
+ if (body !== null) {
7854
+ headers["content-length"] = String(Buffer.byteLength(body, "utf8"));
7855
+ }
7856
+ const transport = url2.protocol === "https:" ? https : http;
7857
+ return new Promise((resolve, reject) => {
7858
+ const request2 = transport.request(
7859
+ url2,
7860
+ {
7861
+ method: init.method ?? "GET",
7862
+ headers,
7863
+ agent: agents[url2.protocol],
7864
+ signal: init.signal
7865
+ },
7866
+ (response) => {
7867
+ const chunks = [];
7868
+ let size = 0;
7869
+ response.on("data", (chunk) => {
7870
+ size += chunk.byteLength;
7871
+ if (size > MAX_RESPONSE_BYTES) {
7872
+ response.destroy(
7873
+ new Error("Iteration returned an oversized response.")
7874
+ );
7875
+ return;
7876
+ }
7877
+ chunks.push(chunk);
7878
+ });
7879
+ response.on("error", reject);
7880
+ response.on("end", () => {
7881
+ const status2 = response.statusCode ?? 0;
7882
+ const responseHeaders = new Headers();
7883
+ for (const [name, value] of Object.entries(response.headers)) {
7884
+ if (value === void 0) continue;
7885
+ for (const item of Array.isArray(value) ? value : [value]) {
7886
+ responseHeaders.append(name, item);
7887
+ }
7888
+ }
7889
+ resolve(
7890
+ new Response(
7891
+ NULL_BODY_STATUSES.has(status2) ? null : Buffer.concat(chunks),
7892
+ { status: status2, headers: responseHeaders }
7893
+ )
7894
+ );
7895
+ });
7896
+ }
7897
+ );
7898
+ request2.on("error", reject);
7899
+ request2.end(body ?? void 0);
7900
+ });
7901
+ };
7902
+ var collectorFetch = async (input, init = {}) => {
7903
+ const requestId = randomUUID();
7904
+ const started = performance.now();
7905
+ const headers = new Headers(init.headers);
7906
+ headers.set("X-Request-ID", requestId);
7907
+ try {
7908
+ const response = await sendCollectorRequest(input, {
7909
+ ...init,
7910
+ headers: Object.fromEntries(headers)
7911
+ });
7912
+ collectorLogger({
7913
+ level: response.ok ? "info" : "warning",
7914
+ operation: "http_request",
7915
+ requestId,
7916
+ method: init.method ?? "GET",
7917
+ status: response.status,
7918
+ durationMs: Math.round(performance.now() - started)
7919
+ });
7920
+ return response;
7921
+ } catch (error2) {
7922
+ collectorLogger({
7923
+ level: "error",
7924
+ operation: "http_request",
7925
+ requestId,
7926
+ method: init.method ?? "GET",
7927
+ errorName: error2 instanceof Error ? error2.name : "UnknownError",
7928
+ durationMs: Math.round(performance.now() - started)
7929
+ });
7930
+ throw error2;
7931
+ }
7932
+ };
7933
+
7774
7934
  // src/windows-powershell.ts
7775
7935
  import { execFile as execFileCallback } from "node:child_process";
7776
7936
  import path from "node:path";
@@ -7878,9 +8038,12 @@ async function responseJson(response) {
7878
8038
  }
7879
8039
  }
7880
8040
  async function loadOAuthConfiguration(endpoint) {
7881
- const response = await fetch(apiRoute(endpoint, "./collector-auth/config"), {
7882
- signal: AbortSignal.timeout(1e4)
7883
- });
8041
+ const response = await collectorFetch(
8042
+ apiRoute(endpoint, "./collector-auth/config"),
8043
+ {
8044
+ signal: AbortSignal.timeout(1e4)
8045
+ }
8046
+ );
7884
8047
  const body = await responseJson(response);
7885
8048
  if (!response.ok) {
7886
8049
  throw new Error(
@@ -8036,15 +8199,18 @@ async function enrollDevice(endpoint, accessToken, deviceName, organizationId, s
8036
8199
  const body = { deviceName };
8037
8200
  if (source !== "codex") body.source = source;
8038
8201
  if (organizationId !== null) body.organizationId = organizationId;
8039
- const response = await fetch(apiRoute(endpoint, "./collector-auth/enroll"), {
8040
- method: "POST",
8041
- headers: {
8042
- Authorization: "Bearer " + accessToken,
8043
- "Content-Type": "application/json"
8044
- },
8045
- body: JSON.stringify(body),
8046
- signal: AbortSignal.timeout(15e3)
8047
- });
8202
+ const response = await collectorFetch(
8203
+ apiRoute(endpoint, "./collector-auth/enroll"),
8204
+ {
8205
+ method: "POST",
8206
+ headers: {
8207
+ Authorization: "Bearer " + accessToken,
8208
+ "Content-Type": "application/json"
8209
+ },
8210
+ body: JSON.stringify(body),
8211
+ signal: AbortSignal.timeout(15e3)
8212
+ }
8213
+ );
8048
8214
  const payload = await responseJson(response);
8049
8215
  const parsed = parseCollectorEnrollmentResponse(payload);
8050
8216
  if (response.ok && parsed.success) return parsed.value;
@@ -8824,8 +8990,8 @@ import { promisify as promisify5 } from "node:util";
8824
8990
  import { execFile as execFileCallback3 } from "node:child_process";
8825
8991
  import { createHash as createHash2 } from "node:crypto";
8826
8992
  import { createWriteStream, promises as fs4 } from "node:fs";
8827
- import http from "node:http";
8828
- import https from "node:https";
8993
+ import http2 from "node:http";
8994
+ import https2 from "node:https";
8829
8995
  import path4 from "node:path";
8830
8996
  import { pipeline } from "node:stream/promises";
8831
8997
  import { Transform } from "node:stream";
@@ -8871,7 +9037,7 @@ function validateRegistryUrl(value) {
8871
9037
  return url2;
8872
9038
  }
8873
9039
  function request(url2, accept, signal) {
8874
- const transport = url2.protocol === "https:" ? https : http;
9040
+ const transport = url2.protocol === "https:" ? https2 : http2;
8875
9041
  return new Promise((resolve, reject) => {
8876
9042
  const outgoing = transport.request(
8877
9043
  url2,
@@ -9859,74 +10025,6 @@ async function backgroundServiceUsesRuntime(directory, source) {
9859
10025
  ) === true;
9860
10026
  }
9861
10027
 
9862
- // src/collector-fetch.ts
9863
- import http2 from "node:http";
9864
- import https2 from "node:https";
9865
- var MAX_RESPONSE_BYTES = 1024 * 1024;
9866
- var NULL_BODY_STATUSES = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
9867
- var agents = {
9868
- "http:": new http2.Agent({ keepAlive: false }),
9869
- "https:": new https2.Agent({ keepAlive: false })
9870
- };
9871
- var collectorFetch = (input, init = {}) => {
9872
- const url2 = new URL(input);
9873
- if (url2.protocol !== "https:" && url2.protocol !== "http:") {
9874
- throw new TypeError(
9875
- `Unsupported collector request protocol ${url2.protocol}`
9876
- );
9877
- }
9878
- const body = init.body ?? null;
9879
- const headers = { ...init.headers };
9880
- if (body !== null) {
9881
- headers["content-length"] = String(Buffer.byteLength(body, "utf8"));
9882
- }
9883
- const transport = url2.protocol === "https:" ? https2 : http2;
9884
- return new Promise((resolve, reject) => {
9885
- const request2 = transport.request(
9886
- url2,
9887
- {
9888
- method: init.method ?? "GET",
9889
- headers,
9890
- agent: agents[url2.protocol],
9891
- signal: init.signal
9892
- },
9893
- (response) => {
9894
- const chunks = [];
9895
- let size = 0;
9896
- response.on("data", (chunk) => {
9897
- size += chunk.byteLength;
9898
- if (size > MAX_RESPONSE_BYTES) {
9899
- response.destroy(
9900
- new Error("Iteration returned an oversized response.")
9901
- );
9902
- return;
9903
- }
9904
- chunks.push(chunk);
9905
- });
9906
- response.on("error", reject);
9907
- response.on("end", () => {
9908
- const status2 = response.statusCode ?? 0;
9909
- const responseHeaders = new Headers();
9910
- for (const [name, value] of Object.entries(response.headers)) {
9911
- if (value === void 0) continue;
9912
- for (const item of Array.isArray(value) ? value : [value]) {
9913
- responseHeaders.append(name, item);
9914
- }
9915
- }
9916
- resolve(
9917
- new Response(
9918
- NULL_BODY_STATUSES.has(status2) ? null : Buffer.concat(chunks),
9919
- { status: status2, headers: responseHeaders }
9920
- )
9921
- );
9922
- });
9923
- }
9924
- );
9925
- request2.on("error", reject);
9926
- request2.end(body ?? void 0);
9927
- });
9928
- };
9929
-
9930
10028
  // src/turn-summary.ts
9931
10029
  import { execFile as execFileCallback7 } from "node:child_process";
9932
10030
  import { createHash as createHash3 } from "node:crypto";
@@ -9936,7 +10034,7 @@ import { promisify as promisify8 } from "node:util";
9936
10034
  import { crc32 } from "node:zlib";
9937
10035
 
9938
10036
  // src/claude-git-hook.ts
9939
- import { randomUUID } from "node:crypto";
10037
+ import { randomUUID as randomUUID2 } from "node:crypto";
9940
10038
  import { promises as fs10 } from "node:fs";
9941
10039
  import path9 from "node:path";
9942
10040
 
@@ -10058,7 +10156,7 @@ async function captureClaudeGit(dataDirectory2) {
10058
10156
  (name2) => name2.endsWith(".jsonl")
10059
10157
  );
10060
10158
  if (files.length >= 1e4) throw new Error("Claude Git hook queue is full.");
10061
- const name = randomUUID();
10159
+ const name = randomUUID2();
10062
10160
  const temporary = path9.join(directory, `${name}.tmp`);
10063
10161
  const filename = path9.join(directory, `${name}.jsonl`);
10064
10162
  try {
@@ -10096,7 +10194,7 @@ async function claudeGitHookFiles(dataDirectory2) {
10096
10194
  async function recordClaudeGitHookFailure(dataDirectory2) {
10097
10195
  const filename = claudeGitHookPaths(dataDirectory2).failure;
10098
10196
  await ensurePrivateDirectory(path9.dirname(filename));
10099
- const temporary = `${filename}.${randomUUID()}.tmp`;
10197
+ const temporary = `${filename}.${randomUUID2()}.tmp`;
10100
10198
  await fs10.writeFile(temporary, `${JSON.stringify({ atMs: Date.now() })}
10101
10199
  `, {
10102
10200
  mode: 384
@@ -16651,14 +16749,12 @@ async function connectNewDetectedSources(anchor2, consent) {
16651
16749
  try {
16652
16750
  if (source !== "cursor") await installTurnSummary(config2);
16653
16751
  } catch (error2) {
16654
- console.error(
16655
- JSON.stringify({
16656
- level: "warning",
16657
- source,
16658
- operation: "install_turn_summary",
16659
- message: error2 instanceof Error ? error2.message : "The turn summary could not be installed."
16660
- })
16661
- );
16752
+ collectorLogger({
16753
+ level: "warning",
16754
+ source,
16755
+ operation: "install_turn_summary",
16756
+ message: error2 instanceof Error ? error2.message : "The turn summary could not be installed."
16757
+ });
16662
16758
  }
16663
16759
  }
16664
16760
  return connected;
@@ -16798,12 +16894,17 @@ function parseClaudeLine(line, filename, byteOffset, repositoryUrl = gitReposito
16798
16894
  const record3 = object2(decoded);
16799
16895
  if (record3 === null) return { events: [], gaps: ["record_is_not_an_object"] };
16800
16896
  const recordType = string4(record3.type);
16801
- const sourceSessionId = string4(record3.sessionId);
16802
- if (identity.agentId !== null && sourceSessionId === null || sourceSessionId !== null && `claude:${sourceSessionId}` !== (identity.parentSessionId ?? identity.sessionId)) {
16803
- return { events: [], gaps: ["session_id_does_not_match_path"] };
16804
- }
16805
- if (identity.agentId !== null && string4(record3.agentId) !== identity.agentId)
16806
- return { events: [], gaps: ["agent_id_does_not_match_path"] };
16897
+ const identityGap = () => {
16898
+ const sourceSessionId = string4(record3.sessionId);
16899
+ if (identity.agentId !== null && sourceSessionId === null || sourceSessionId !== null && `claude:${sourceSessionId}` !== (identity.parentSessionId ?? identity.sessionId)) {
16900
+ return "session_id_does_not_match_path";
16901
+ }
16902
+ if (identity.agentId !== null && string4(record3.agentId) !== identity.agentId)
16903
+ return "agent_id_does_not_match_path";
16904
+ return null;
16905
+ };
16906
+ const gap = identityGap();
16907
+ if (gap !== null) return { events: [], gaps: [gap] };
16807
16908
  const gaps = [];
16808
16909
  if (record3.timestamp !== void 0 && timestamp(record3) === null)
16809
16910
  gaps.push("invalid_source_timestamp");
@@ -16841,7 +16942,7 @@ function parseClaudeLine(line, filename, byteOffset, repositoryUrl = gitReposito
16841
16942
  }
16842
16943
  });
16843
16944
  }
16844
- if (recordType === "user") {
16945
+ const appendUserMessage = () => {
16845
16946
  const message = object2(record3.message);
16846
16947
  const content = message === null ? null : string4(message.content);
16847
16948
  if (message === null) gaps.push("user_without_message");
@@ -16889,7 +16990,8 @@ function parseClaudeLine(line, filename, byteOffset, repositoryUrl = gitReposito
16889
16990
  }
16890
16991
  }
16891
16992
  } else gaps.push("unsupported_user_content");
16892
- } else if (recordType === "assistant") {
16993
+ };
16994
+ const appendAssistantMessage = () => {
16893
16995
  const message = object2(record3.message);
16894
16996
  if (message === null || !Array.isArray(message.content))
16895
16997
  gaps.push("assistant_without_content_blocks");
@@ -16942,7 +17044,10 @@ function parseClaudeLine(line, filename, byteOffset, repositoryUrl = gitReposito
16942
17044
  );
16943
17045
  }
16944
17046
  }
16945
- } else if (recordType !== null && CONTEXT_RECORD_TYPES.has(recordType)) {
17047
+ };
17048
+ if (recordType === "user") appendUserMessage();
17049
+ else if (recordType === "assistant") appendAssistantMessage();
17050
+ else if (recordType !== null && CONTEXT_RECORD_TYPES.has(recordType)) {
16946
17051
  const attachment = object2(record3.attachment);
16947
17052
  events.push(
16948
17053
  event(identity, record3, byteOffset, 1, "source_record", {
@@ -17060,8 +17165,7 @@ function parseCursorTranscriptLine(line, filename, byteOffset) {
17060
17165
  }
17061
17166
  });
17062
17167
  }
17063
- const role = string5(source.role);
17064
- if (role === "user" || role === "assistant") {
17168
+ const appendMessage = (role2) => {
17065
17169
  const message = record2(source.message);
17066
17170
  if (message === null || !Array.isArray(message.content))
17067
17171
  gaps.push("cursor_message_without_content");
@@ -17073,7 +17177,7 @@ function parseCursorTranscriptLine(line, filename, byteOffset) {
17073
17177
  continue;
17074
17178
  }
17075
17179
  const type = string5(block.type);
17076
- const eventType = type === "tool_use" ? "tool_call" : type === "thinking" ? "reasoning" : type === "text" ? role === "user" ? "user_message" : "assistant_message" : "source_record";
17180
+ const eventType = type === "tool_use" ? "tool_call" : type === "thinking" ? "reasoning" : type === "text" ? role2 === "user" ? "user_message" : "assistant_message" : "source_record";
17077
17181
  events.push(
17078
17182
  event2(
17079
17183
  conversationId,
@@ -17095,7 +17199,10 @@ function parseCursorTranscriptLine(line, filename, byteOffset) {
17095
17199
  gaps.push(`unsupported_cursor_content_block:${type ?? "unknown"}`);
17096
17200
  }
17097
17201
  }
17098
- } else if (source.type === "turn_ended") {
17202
+ };
17203
+ const role = string5(source.role);
17204
+ if (role === "user" || role === "assistant") appendMessage(role);
17205
+ else if (source.type === "turn_ended") {
17099
17206
  events.push(
17100
17207
  event2(
17101
17208
  conversationId,
@@ -17130,45 +17237,64 @@ function parseCursorHookLine(line, filename) {
17130
17237
  const hook = wrapper === null ? null : string5(wrapper.hook);
17131
17238
  const capturedAt = wrapper === null ? null : string5(wrapper.capturedAt);
17132
17239
  if (wrapper?.kind === "subagent_transcript_chunk") {
17133
- const subagent = record2(wrapper.subagent);
17134
- const part = nonnegativeInteger(subagent?.part);
17135
- const totalParts = nonnegativeInteger(subagent?.totalParts);
17136
- if (conversationId === null || !UUID.test(conversationId) || capturedAt === null || Number.isNaN(Date.parse(capturedAt)) || subagent === null || part === null || totalParts === null || part < 1 || part > totalParts || string5(subagent.base64) === null)
17137
- return { events: [], gaps: ["invalid_cursor_subagent_transcript_chunk"] };
17138
- const transcriptBytes2 = nonnegativeInteger(wrapper.transcriptBytes) ?? 0;
17139
- const candidate = event2(
17240
+ return parseSubagentChunk(
17241
+ wrapper,
17242
+ input,
17140
17243
  conversationId,
17141
- filename,
17142
- transcriptBytes2,
17143
- part,
17144
- "source_record",
17145
- {
17146
- sourceType: "cursor_subagent_transcript",
17147
- text: string5(subagent.text),
17148
- base64: string5(subagent.base64),
17149
- path: string5(subagent.path),
17150
- part,
17151
- totalParts,
17152
- parent_conversation_id: string5(input?.parent_conversation_id),
17153
- subagent_id: string5(input?.subagent_id)
17154
- },
17155
- null,
17156
- capturedAt
17157
- );
17158
- const hash = candidate.eventId.split(":").at(-1);
17159
- return validated(
17160
- [
17161
- {
17162
- ...candidate,
17163
- sequence: transcriptBytes2 * 1e3 + 500,
17164
- eventId: `${String(Date.parse(capturedAt)).padStart(13, "0")}:${String(2e3 + part).padStart(4, "0")}:${hash}`
17165
- }
17166
- ],
17167
- []
17244
+ capturedAt,
17245
+ filename
17168
17246
  );
17169
17247
  }
17170
17248
  if (input === null || conversationId === null || !UUID.test(conversationId) || hook === null || capturedAt === null || Number.isNaN(Date.parse(capturedAt)))
17171
17249
  return { events: [], gaps: ["invalid_cursor_hook_record"] };
17250
+ return parseHookEvents(
17251
+ wrapper,
17252
+ input,
17253
+ conversationId,
17254
+ hook,
17255
+ capturedAt,
17256
+ filename
17257
+ );
17258
+ }
17259
+ function parseSubagentChunk(wrapper, input, conversationId, capturedAt, filename) {
17260
+ const subagent = record2(wrapper.subagent);
17261
+ const part = nonnegativeInteger(subagent?.part);
17262
+ const totalParts = nonnegativeInteger(subagent?.totalParts);
17263
+ if (conversationId === null || !UUID.test(conversationId) || capturedAt === null || Number.isNaN(Date.parse(capturedAt)) || subagent === null || part === null || totalParts === null || part < 1 || part > totalParts || string5(subagent.base64) === null)
17264
+ return { events: [], gaps: ["invalid_cursor_subagent_transcript_chunk"] };
17265
+ const transcriptBytes = nonnegativeInteger(wrapper.transcriptBytes) ?? 0;
17266
+ const candidate = event2(
17267
+ conversationId,
17268
+ filename,
17269
+ transcriptBytes,
17270
+ part,
17271
+ "source_record",
17272
+ {
17273
+ sourceType: "cursor_subagent_transcript",
17274
+ text: string5(subagent.text),
17275
+ base64: string5(subagent.base64),
17276
+ path: string5(subagent.path),
17277
+ part,
17278
+ totalParts,
17279
+ parent_conversation_id: string5(input?.parent_conversation_id),
17280
+ subagent_id: string5(input?.subagent_id)
17281
+ },
17282
+ null,
17283
+ capturedAt
17284
+ );
17285
+ const hash = candidate.eventId.split(":").at(-1);
17286
+ return validated(
17287
+ [
17288
+ {
17289
+ ...candidate,
17290
+ sequence: transcriptBytes * 1e3 + 500,
17291
+ eventId: `${String(Date.parse(capturedAt)).padStart(13, "0")}:${String(2e3 + part).padStart(4, "0")}:${hash}`
17292
+ }
17293
+ ],
17294
+ []
17295
+ );
17296
+ }
17297
+ function parseHookEvents(wrapper, input, conversationId, hook, capturedAt, filename) {
17172
17298
  const transcriptBytes = wrapper?.transcriptBytes;
17173
17299
  const sequence = nonnegativeInteger(transcriptBytes) ?? 0;
17174
17300
  const events = [];
@@ -17243,82 +17369,85 @@ function parseCursorHookLine(line, filename) {
17243
17369
  )
17244
17370
  );
17245
17371
  }
17246
- const next = events.length + 1;
17247
- if (hook === "preToolUse") {
17248
- events.push(
17249
- event2(
17250
- conversationId,
17251
- filename,
17252
- sequence,
17253
- next,
17254
- "tool_call",
17255
- {
17256
- name: string5(input.tool_name),
17257
- input: input.tool_input ?? null
17258
- },
17259
- string5(input.tool_use_id),
17260
- capturedAt
17261
- )
17262
- );
17263
- }
17264
- if (hook === "postToolUse" || hook === "postToolUseFailure") {
17265
- const output = string5(input.tool_output) ?? string5(input.error_message) ?? string5(input.error) ?? (input.tool_output === void 0 && input.error === void 0 ? null : JSON.stringify(input.tool_output ?? input.error));
17266
- const chunks = output === null ? [null] : splitText(output);
17267
- for (const [index, text] of chunks.entries())
17372
+ const appendHookPayload = () => {
17373
+ const next = events.length + 1;
17374
+ if (hook === "preToolUse") {
17268
17375
  events.push(
17269
17376
  event2(
17270
17377
  conversationId,
17271
17378
  filename,
17272
17379
  sequence,
17273
- next + index,
17274
- "tool_result",
17380
+ next,
17381
+ "tool_call",
17275
17382
  {
17276
- text,
17277
17383
  name: string5(input.tool_name),
17278
- part: index + 1,
17279
- totalParts: chunks.length
17384
+ input: input.tool_input ?? null
17280
17385
  },
17281
17386
  string5(input.tool_use_id),
17282
17387
  capturedAt
17283
17388
  )
17284
17389
  );
17285
- } else if (hook === "afterAgentThought") {
17286
- events.push(
17287
- event2(
17288
- conversationId,
17289
- filename,
17290
- sequence,
17291
- next,
17292
- "reasoning",
17293
- {
17294
- text: string5(input.text) ?? string5(input.thought)
17295
- },
17296
- null,
17297
- capturedAt
17298
- )
17299
- );
17300
- } else if (hook === "afterAgentResponse" && (input.usage !== void 0 || input.input_tokens !== void 0 || input.output_tokens !== void 0)) {
17301
- events.push(
17302
- event2(
17303
- conversationId,
17304
- filename,
17305
- sequence,
17306
- next,
17307
- "token_usage_record",
17308
- {
17309
- model: string5(input.model),
17310
- usage: input.usage ?? {
17311
- input_tokens: input.input_tokens ?? null,
17312
- output_tokens: input.output_tokens ?? null,
17313
- cache_read_tokens: input.cache_read_tokens ?? null,
17314
- cache_write_tokens: input.cache_write_tokens ?? null
17315
- }
17316
- },
17317
- null,
17318
- capturedAt
17319
- )
17320
- );
17321
- }
17390
+ }
17391
+ if (hook === "postToolUse" || hook === "postToolUseFailure") {
17392
+ const output = hookToolOutput(input);
17393
+ const chunks = output === null ? [null] : splitText(output);
17394
+ for (const [index, text] of chunks.entries())
17395
+ events.push(
17396
+ event2(
17397
+ conversationId,
17398
+ filename,
17399
+ sequence,
17400
+ next + index,
17401
+ "tool_result",
17402
+ {
17403
+ text,
17404
+ name: string5(input.tool_name),
17405
+ part: index + 1,
17406
+ totalParts: chunks.length
17407
+ },
17408
+ string5(input.tool_use_id),
17409
+ capturedAt
17410
+ )
17411
+ );
17412
+ } else if (hook === "afterAgentThought") {
17413
+ events.push(
17414
+ event2(
17415
+ conversationId,
17416
+ filename,
17417
+ sequence,
17418
+ next,
17419
+ "reasoning",
17420
+ {
17421
+ text: string5(input.text) ?? string5(input.thought)
17422
+ },
17423
+ null,
17424
+ capturedAt
17425
+ )
17426
+ );
17427
+ } else if (hook === "afterAgentResponse" && (input.usage !== void 0 || input.input_tokens !== void 0 || input.output_tokens !== void 0)) {
17428
+ events.push(
17429
+ event2(
17430
+ conversationId,
17431
+ filename,
17432
+ sequence,
17433
+ next,
17434
+ "token_usage_record",
17435
+ {
17436
+ model: string5(input.model),
17437
+ usage: input.usage ?? {
17438
+ input_tokens: input.input_tokens ?? null,
17439
+ output_tokens: input.output_tokens ?? null,
17440
+ cache_read_tokens: input.cache_read_tokens ?? null,
17441
+ cache_write_tokens: input.cache_write_tokens ?? null
17442
+ }
17443
+ },
17444
+ null,
17445
+ capturedAt
17446
+ )
17447
+ );
17448
+ }
17449
+ };
17450
+ appendHookPayload();
17322
17451
  const capturedAtMs = Date.parse(capturedAt);
17323
17452
  const ordered = events.map((candidate, index) => {
17324
17453
  const digest3 = candidate.eventId.split(":").at(-1);
@@ -17333,6 +17462,9 @@ function parseCursorHookLine(line, filename) {
17333
17462
  });
17334
17463
  return validated(ordered, []);
17335
17464
  }
17465
+ function hookToolOutput(input) {
17466
+ return string5(input.tool_output) ?? string5(input.error_message) ?? string5(input.error) ?? (input.tool_output === void 0 && input.error === void 0 ? null : JSON.stringify(input.tool_output ?? input.error));
17467
+ }
17336
17468
  function splitText(value) {
17337
17469
  if (value.length === 0) return [""];
17338
17470
  const chunks = [];
@@ -17569,83 +17701,86 @@ function parseRolloutLine(line, filename, byteOffset) {
17569
17701
  );
17570
17702
  }
17571
17703
  if (recordType === "event_msg" && payload !== null) {
17572
- const messageType = parseJsonString(payload.type);
17573
- if (messageType === "user_message") {
17574
- return capturedEvent(
17575
- record3,
17576
- filename,
17577
- byteOffset,
17578
- "user_message",
17579
- payload,
17580
- null
17581
- );
17582
- }
17583
- if (messageType === "agent_message") {
17584
- return capturedEvent(
17585
- record3,
17586
- filename,
17587
- byteOffset,
17588
- "assistant_message",
17589
- payload,
17590
- null
17591
- );
17592
- }
17593
- if (oneOf(messageType, [
17594
- "mcp_tool_call_end",
17595
- "web_search_end",
17596
- "image_generation_end",
17597
- "patch_apply_end"
17598
- ])) {
17599
- return capturedEvent(
17600
- record3,
17601
- filename,
17602
- byteOffset,
17603
- "tool_result",
17604
- payload,
17605
- callIdFrom(payload)
17606
- );
17607
- }
17608
- if (oneOf(messageType, IGNORED_EVENT_MESSAGES)) return { kind: "ignored" };
17609
- if (oneOf(messageType, EVENT_CONTEXT_TYPES)) {
17610
- const ordinal = sourceOrdinal(record3.ordinal);
17611
- const run = messageType === "thread_settings_applied" && parseJsonString(payload.thread_id) === sessionFromPath(filename).slice(-36) && ordinal !== null ? { kind: "start", sourceOrdinal: ordinal } : void 0;
17612
- return capturedEvent(
17613
- record3,
17614
- filename,
17615
- byteOffset,
17616
- messageType,
17617
- payload,
17618
- null,
17619
- run
17620
- );
17621
- }
17622
- if (messageType !== "item_completed") {
17623
- return {
17624
- kind: "gap",
17625
- reason: `unsupported_event_message:${messageType ?? "unknown"}`
17626
- };
17627
- }
17628
- const item = parseJsonRecord(payload.item);
17629
- if (item === null)
17630
- return { kind: "gap", reason: "item_completed_without_item" };
17631
- const mapped = mapCompletedItem(item);
17632
- if (mapped === null) {
17633
- const itemType = parseJsonString(item.type) ?? "unknown";
17634
- return { kind: "gap", reason: `unsupported_completed_item:${itemType}` };
17635
- }
17704
+ return parseEventMessage(record3, payload, filename, byteOffset);
17705
+ }
17706
+ return {
17707
+ kind: "gap",
17708
+ reason: `unsupported_rollout_record:${recordType ?? "unknown"}`
17709
+ };
17710
+ }
17711
+ function parseEventMessage(record3, payload, filename, byteOffset) {
17712
+ const messageType = parseJsonString(payload.type);
17713
+ if (messageType === "user_message") {
17636
17714
  return capturedEvent(
17637
17715
  record3,
17638
17716
  filename,
17639
17717
  byteOffset,
17640
- mapped.eventType,
17641
- item,
17642
- mapped.callId
17718
+ "user_message",
17719
+ payload,
17720
+ null
17643
17721
  );
17644
17722
  }
17645
- return {
17646
- kind: "gap",
17647
- reason: `unsupported_rollout_record:${recordType ?? "unknown"}`
17648
- };
17723
+ if (messageType === "agent_message") {
17724
+ return capturedEvent(
17725
+ record3,
17726
+ filename,
17727
+ byteOffset,
17728
+ "assistant_message",
17729
+ payload,
17730
+ null
17731
+ );
17732
+ }
17733
+ if (oneOf(messageType, [
17734
+ "mcp_tool_call_end",
17735
+ "web_search_end",
17736
+ "image_generation_end",
17737
+ "patch_apply_end"
17738
+ ])) {
17739
+ return capturedEvent(
17740
+ record3,
17741
+ filename,
17742
+ byteOffset,
17743
+ "tool_result",
17744
+ payload,
17745
+ callIdFrom(payload)
17746
+ );
17747
+ }
17748
+ if (oneOf(messageType, IGNORED_EVENT_MESSAGES)) return { kind: "ignored" };
17749
+ if (oneOf(messageType, EVENT_CONTEXT_TYPES)) {
17750
+ const ordinal = sourceOrdinal(record3.ordinal);
17751
+ const run = messageType === "thread_settings_applied" && parseJsonString(payload.thread_id) === sessionFromPath(filename).slice(-36) && ordinal !== null ? { kind: "start", sourceOrdinal: ordinal } : void 0;
17752
+ return capturedEvent(
17753
+ record3,
17754
+ filename,
17755
+ byteOffset,
17756
+ messageType,
17757
+ payload,
17758
+ null,
17759
+ run
17760
+ );
17761
+ }
17762
+ if (messageType !== "item_completed") {
17763
+ return {
17764
+ kind: "gap",
17765
+ reason: `unsupported_event_message:${messageType ?? "unknown"}`
17766
+ };
17767
+ }
17768
+ const item = parseJsonRecord(payload.item);
17769
+ if (item === null)
17770
+ return { kind: "gap", reason: "item_completed_without_item" };
17771
+ const mapped = mapCompletedItem(item);
17772
+ if (mapped === null) {
17773
+ const itemType = parseJsonString(item.type) ?? "unknown";
17774
+ return { kind: "gap", reason: `unsupported_completed_item:${itemType}` };
17775
+ }
17776
+ return capturedEvent(
17777
+ record3,
17778
+ filename,
17779
+ byteOffset,
17780
+ mapped.eventType,
17781
+ item,
17782
+ mapped.callId
17783
+ );
17649
17784
  }
17650
17785
 
17651
17786
  // src/collector.ts
@@ -17669,15 +17804,15 @@ var TraceUploadError = class extends Error {
17669
17804
  this.status = status2;
17670
17805
  }
17671
17806
  };
17672
- function logGap(filename, offset, reason) {
17673
- console.error(
17674
- JSON.stringify({
17675
- level: "capture_gap",
17676
- filename,
17677
- offset,
17678
- reason
17679
- })
17680
- );
17807
+ function logGap(source, filename, offset, reason) {
17808
+ collectorLogger({
17809
+ level: "warning",
17810
+ operation: "capture_gap",
17811
+ source,
17812
+ filename,
17813
+ offset,
17814
+ reason
17815
+ });
17681
17816
  }
17682
17817
  function parseSourceRecord(source, line, filename, byteOffset) {
17683
17818
  if (source === "claude")
@@ -17716,11 +17851,37 @@ async function scanSourceOnce(config2, paths, signal) {
17716
17851
  const queuedIds = await pendingEventIds(paths);
17717
17852
  const tickIndexes = /* @__PURE__ */ new Map();
17718
17853
  const events = [];
17854
+ const queueRecordEvents = async (parsedEvents, filename, byteOffset) => {
17855
+ for (const parsedEvent of parsedEvents) {
17856
+ const queuedAt = tickIndexes.get(parsedEvent.eventId);
17857
+ if (queuedAt !== void 0 && parsedEvent.eventType === "token_usage_record") {
17858
+ const event4 = await redactTraceEvent(parsedEvent);
17859
+ if (Buffer.byteLength(JSON.stringify(event4), "utf8") <= MAX_EVENT_BYTES)
17860
+ events[queuedAt] = event4;
17861
+ continue;
17862
+ }
17863
+ if (queuedIds.has(parsedEvent.eventId)) continue;
17864
+ const event3 = await redactTraceEvent(parsedEvent);
17865
+ const eventBytes = Buffer.byteLength(JSON.stringify(event3), "utf8");
17866
+ if (eventBytes > MAX_EVENT_BYTES) {
17867
+ logGap(
17868
+ source,
17869
+ filename,
17870
+ byteOffset,
17871
+ "normalized_event_exceeds_upload_limit"
17872
+ );
17873
+ } else {
17874
+ tickIndexes.set(event3.eventId, events.length);
17875
+ events.push(event3);
17876
+ queuedIds.add(event3.eventId);
17877
+ }
17878
+ }
17879
+ };
17719
17880
  let remainingBudget = MAX_SCAN_BYTES_PER_TICK;
17720
17881
  let caughtUp = true;
17721
17882
  for (const filename of files) {
17722
17883
  signal?.throwIfAborted();
17723
- const recordLimit = source === "cursor" && filename.includes(`${path18.sep}cursor-hooks${path18.sep}`) ? 12 * 1024 * 1024 : MAX_RECORD_BYTES;
17884
+ const recordLimit = isCursorHookFile(source, filename) ? 12 * 1024 * 1024 : MAX_RECORD_BYTES;
17724
17885
  if (remainingBudget === 0) {
17725
17886
  caughtUp = false;
17726
17887
  break;
@@ -17728,7 +17889,7 @@ async function scanSourceOnce(config2, paths, signal) {
17728
17889
  const size = listedSizes.get(filename) ?? (await fs15.stat(filename)).size;
17729
17890
  let offset = cursors[filename] ?? 0;
17730
17891
  if (offset > size) {
17731
- logGap(filename, offset, "rollout_file_shrank_resetting_cursor");
17892
+ logGap(source, filename, offset, "rollout_file_shrank_resetting_cursor");
17732
17893
  offset = 0;
17733
17894
  skipping.delete(filename);
17734
17895
  }
@@ -17764,59 +17925,51 @@ async function scanSourceOnce(config2, paths, signal) {
17764
17925
  buffer = buffer.subarray(newline + 1);
17765
17926
  skipping.delete(filename);
17766
17927
  }
17767
- let lineStart = 0;
17768
- for (let index = 0; index < buffer.length; index += 1) {
17769
- if (buffer[index] !== 10) continue;
17770
- let lineBytes = buffer.subarray(lineStart, index);
17771
- if (lineBytes.at(-1) === 13) lineBytes = lineBytes.subarray(0, -1);
17772
- const byteOffset = offset + lineStart;
17773
- if (lineBytes.length > recordLimit) {
17774
- logGap(filename, byteOffset, "rollout_record_exceeds_scan_limit");
17775
- lineStart = index + 1;
17776
- continue;
17777
- }
17778
- let line;
17779
- try {
17780
- line = UTF8_DECODER.decode(lineBytes);
17781
- } catch {
17782
- logGap(filename, byteOffset, "rollout_record_is_not_utf8");
17783
- lineStart = index + 1;
17784
- continue;
17785
- }
17786
- const parsed = parseSourceRecord(source, line, filename, byteOffset);
17787
- for (const reason of parsed.gaps) logGap(filename, byteOffset, reason);
17788
- for (const parsedEvent of parsed.events) {
17789
- const queuedAt = tickIndexes.get(parsedEvent.eventId);
17790
- if (queuedAt !== void 0 && parsedEvent.eventType === "token_usage_record") {
17791
- const event4 = await redactTraceEvent(parsedEvent);
17792
- if (Buffer.byteLength(JSON.stringify(event4), "utf8") <= MAX_EVENT_BYTES)
17793
- events[queuedAt] = event4;
17928
+ const scanRecords = async () => {
17929
+ let lineStart = 0;
17930
+ for (let index = 0; index < buffer.length; index += 1) {
17931
+ if (buffer[index] !== 10) continue;
17932
+ let lineBytes = buffer.subarray(lineStart, index);
17933
+ if (lineBytes.at(-1) === 13) lineBytes = lineBytes.subarray(0, -1);
17934
+ const byteOffset = offset + lineStart;
17935
+ if (lineBytes.length > recordLimit) {
17936
+ logGap(
17937
+ source,
17938
+ filename,
17939
+ byteOffset,
17940
+ "rollout_record_exceeds_scan_limit"
17941
+ );
17942
+ lineStart = index + 1;
17794
17943
  continue;
17795
17944
  }
17796
- if (queuedIds.has(parsedEvent.eventId)) continue;
17797
- const event3 = await redactTraceEvent(parsedEvent);
17798
- const eventBytes = Buffer.byteLength(JSON.stringify(event3), "utf8");
17799
- if (eventBytes > MAX_EVENT_BYTES) {
17800
- logGap(filename, byteOffset, "normalized_event_exceeds_upload_limit");
17801
- } else {
17802
- tickIndexes.set(event3.eventId, events.length);
17803
- events.push(event3);
17804
- queuedIds.add(event3.eventId);
17945
+ let line;
17946
+ try {
17947
+ line = UTF8_DECODER.decode(lineBytes);
17948
+ } catch {
17949
+ logGap(source, filename, byteOffset, "rollout_record_is_not_utf8");
17950
+ lineStart = index + 1;
17951
+ continue;
17805
17952
  }
17953
+ const parsed = parseSourceRecord(source, line, filename, byteOffset);
17954
+ for (const reason of parsed.gaps)
17955
+ logGap(source, filename, byteOffset, reason);
17956
+ await queueRecordEvents(parsed.events, filename, byteOffset);
17957
+ lineStart = index + 1;
17806
17958
  }
17807
- lineStart = index + 1;
17808
- }
17809
- if (buffer.length - lineStart > recordLimit) {
17810
- logGap(
17811
- filename,
17812
- offset + lineStart,
17813
- "incomplete_rollout_record_exceeds_scan_limit"
17814
- );
17815
- skipping.add(filename);
17816
- lineStart = buffer.length;
17817
- }
17818
- cursors[filename] = offset + lineStart;
17819
- if ((source === "cursor" && filename.includes(`${path18.sep}cursor-hooks${path18.sep}`) || source === "claude" && filename.includes(`${path18.sep}git-queue${path18.sep}`)) && cursors[filename] === size)
17959
+ if (buffer.length - lineStart > recordLimit) {
17960
+ logGap(
17961
+ source,
17962
+ filename,
17963
+ offset + lineStart,
17964
+ "incomplete_rollout_record_exceeds_scan_limit"
17965
+ );
17966
+ skipping.add(filename);
17967
+ lineStart = buffer.length;
17968
+ }
17969
+ cursors[filename] = offset + lineStart;
17970
+ };
17971
+ await scanRecords();
17972
+ if (isHookFile(source, filename) && cursors[filename] === size)
17820
17973
  completedHooks.push(filename);
17821
17974
  }
17822
17975
  await appendEvents(events, paths);
@@ -17845,12 +17998,24 @@ async function scanSourceOnce(config2, paths, signal) {
17845
17998
  }
17846
17999
  };
17847
18000
  }
18001
+ function isHookFile(source, filename) {
18002
+ return isCursorHookFile(source, filename) || source === "claude" && filename.includes(`${path18.sep}git-queue${path18.sep}`);
18003
+ }
18004
+ function isCursorHookFile(source, filename) {
18005
+ return source === "cursor" && filename.includes(`${path18.sep}cursor-hooks${path18.sep}`);
18006
+ }
18007
+ function retryAfterMs(response) {
18008
+ const header = response.headers.get("retry-after");
18009
+ if (header === null) return null;
18010
+ const milliseconds = /^\d+$/.test(header) ? Number(header) * 1e3 : Date.parse(header) - Date.now();
18011
+ return Number.isFinite(milliseconds) ? Math.max(1e3, Math.min(milliseconds, 3e5)) : 5e3;
18012
+ }
17848
18013
  async function uploadPendingOnce(config2, paths, fetcher = collectorFetch, signal, onAcknowledged) {
17849
18014
  let uploaded = 0;
17850
18015
  for (let attempt = 0; attempt < MAX_UPLOADS_PER_TICK; attempt += 1) {
17851
18016
  signal?.throwIfAborted();
17852
18017
  const batch = await nextBatch(paths, MAX_UPLOAD_BYTES);
17853
- if (batch.length === 0) return uploaded;
18018
+ if (batch.length === 0) return { uploaded, retryAfterMs: null };
17854
18019
  const body = JSON.stringify({ events: batch });
17855
18020
  if (Buffer.byteLength(body, "utf8") > MAX_REQUEST_BYTES) {
17856
18021
  throw new Error("Queued trace batch exceeds the upload request limit.");
@@ -17875,6 +18040,10 @@ async function uploadPendingOnce(config2, paths, fetcher = collectorFetch, signa
17875
18040
  { cause: error2 }
17876
18041
  );
17877
18042
  }
18043
+ if (response.status === 429) {
18044
+ await response.body?.cancel();
18045
+ return { uploaded, retryAfterMs: retryAfterMs(response) ?? 5e3 };
18046
+ }
17878
18047
  if (response.status !== 202) {
17879
18048
  throw new TraceUploadError(
17880
18049
  "http",
@@ -17901,12 +18070,14 @@ async function uploadPendingOnce(config2, paths, fetcher = collectorFetch, signa
17901
18070
  await acknowledgeEvents(new Set(parsed.value.acceptedEventIds), paths);
17902
18071
  onAcknowledged?.();
17903
18072
  uploaded += batch.length;
18073
+ const pause = retryAfterMs(response);
18074
+ if (pause !== null) return { uploaded, retryAfterMs: pause };
17904
18075
  }
17905
- return uploaded;
18076
+ return { uploaded, retryAfterMs: null };
17906
18077
  }
17907
18078
  async function collectorTick(config2, paths, fetcher = collectorFetch, signal, onAcknowledged) {
17908
18079
  await redactPendingOutbox(paths);
17909
- let uploaded = await uploadPendingOnce(
18080
+ let upload = await uploadPendingOnce(
17910
18081
  config2,
17911
18082
  paths,
17912
18083
  fetcher,
@@ -17914,19 +18085,23 @@ async function collectorTick(config2, paths, fetcher = collectorFetch, signal, o
17914
18085
  onAcknowledged
17915
18086
  );
17916
18087
  const scan = await outboxSize(paths) >= SCAN_PAUSE_OUTBOX_BYTES ? { discovered: 0, caughtUp: false, history: null } : await scanSourceOnce(config2, paths, signal);
17917
- uploaded += await uploadPendingOnce(
17918
- config2,
17919
- paths,
17920
- fetcher,
17921
- signal,
17922
- onAcknowledged
17923
- );
18088
+ if (upload.retryAfterMs === null) {
18089
+ const next = await uploadPendingOnce(
18090
+ config2,
18091
+ paths,
18092
+ fetcher,
18093
+ signal,
18094
+ onAcknowledged
18095
+ );
18096
+ upload = { ...next, uploaded: upload.uploaded + next.uploaded };
18097
+ }
17924
18098
  return {
17925
18099
  discovered: scan.discovered,
17926
- uploaded,
18100
+ uploaded: upload.uploaded,
17927
18101
  outboxBytes: await outboxSize(paths),
17928
18102
  historyCaughtUp: scan.caughtUp,
17929
- history: scan.history
18103
+ history: scan.history,
18104
+ retryAfterMs: upload.retryAfterMs ?? 0
17930
18105
  };
17931
18106
  }
17932
18107
 
@@ -18190,57 +18365,64 @@ async function syncCursorBilling(config2, fetcher = collectorFetch, signal) {
18190
18365
  nowMs
18191
18366
  );
18192
18367
  const byConversation = /* @__PURE__ */ new Map();
18193
- for (let page = 1; page <= MAX_PAGES; page += 1) {
18194
- signal?.throwIfAborted();
18195
- const response = await fetch(CURSOR_USAGE_URL, {
18196
- method: "POST",
18197
- headers: {
18198
- Authorization: `Bearer ${auth.token}`,
18199
- "Content-Type": "application/json",
18200
- "Connect-Protocol-Version": "1"
18201
- },
18202
- body: JSON.stringify({
18203
- page,
18204
- pageSize: PAGE_SIZE,
18205
- startDate: String(plan.startDateMs)
18206
- }),
18207
- signal: AbortSignal.any([
18208
- AbortSignal.timeout(2e4),
18209
- ...signal ? [signal] : []
18210
- ])
18211
- });
18212
- if (!response.ok)
18213
- throw new Error(
18214
- `Cursor billing lookup failed with HTTP ${response.status}.`
18215
- );
18216
- const rawPage = await response.json();
18217
- if (emptyUsagePageSchema.safeParse(rawPage).success) {
18218
- if (page !== 1)
18368
+ const readUsagePages = async () => {
18369
+ for (let page = 1; page <= MAX_PAGES; page += 1) {
18370
+ signal?.throwIfAborted();
18371
+ const response = await fetch(CURSOR_USAGE_URL, {
18372
+ method: "POST",
18373
+ headers: {
18374
+ Authorization: `Bearer ${auth.token}`,
18375
+ "Content-Type": "application/json",
18376
+ "Connect-Protocol-Version": "1"
18377
+ },
18378
+ body: JSON.stringify({
18379
+ page,
18380
+ pageSize: PAGE_SIZE,
18381
+ startDate: String(plan.startDateMs)
18382
+ }),
18383
+ signal: AbortSignal.any([
18384
+ AbortSignal.timeout(2e4),
18385
+ ...signal ? [signal] : []
18386
+ ])
18387
+ });
18388
+ if (!response.ok)
18389
+ throw new Error(
18390
+ `Cursor billing lookup failed with HTTP ${response.status}.`
18391
+ );
18392
+ const rawPage = await response.json();
18393
+ if (emptyUsagePageSchema.safeParse(rawPage).success) {
18394
+ if (page !== 1)
18395
+ throw new Error(
18396
+ "Cursor billing history ended before its reported count."
18397
+ );
18398
+ break;
18399
+ }
18400
+ const decoded = usagePageSchema.safeParse(rawPage);
18401
+ if (!decoded.success)
18402
+ throw new Error("Cursor returned an invalid billing page.");
18403
+ for (const value of decoded.data.usageEventsDisplay) {
18404
+ const normalized = normalizeCursorUsageEvent(value, localConversations);
18405
+ if (normalized === null) continue;
18406
+ const events = byConversation.get(normalized.conversationId) ?? [];
18407
+ events.push(normalized.event);
18408
+ if (events.length > 5e3)
18409
+ throw new Error(
18410
+ "Cursor conversation exceeds the billing sync limit."
18411
+ );
18412
+ byConversation.set(normalized.conversationId, events);
18413
+ }
18414
+ if (page * PAGE_SIZE >= decoded.data.totalUsageEventsCount) break;
18415
+ if (page === MAX_PAGES)
18416
+ throw new Error(
18417
+ "Cursor account exceeds the billing history page limit."
18418
+ );
18419
+ if (decoded.data.usageEventsDisplay.length === 0)
18219
18420
  throw new Error(
18220
18421
  "Cursor billing history ended before its reported count."
18221
18422
  );
18222
- break;
18223
18423
  }
18224
- const decoded = usagePageSchema.safeParse(rawPage);
18225
- if (!decoded.success)
18226
- throw new Error("Cursor returned an invalid billing page.");
18227
- for (const value of decoded.data.usageEventsDisplay) {
18228
- const normalized = normalizeCursorUsageEvent(value, localConversations);
18229
- if (normalized === null) continue;
18230
- const events = byConversation.get(normalized.conversationId) ?? [];
18231
- events.push(normalized.event);
18232
- if (events.length > 5e3)
18233
- throw new Error("Cursor conversation exceeds the billing sync limit.");
18234
- byConversation.set(normalized.conversationId, events);
18235
- }
18236
- if (page * PAGE_SIZE >= decoded.data.totalUsageEventsCount) break;
18237
- if (page === MAX_PAGES)
18238
- throw new Error("Cursor account exceeds the billing history page limit.");
18239
- if (decoded.data.usageEventsDisplay.length === 0)
18240
- throw new Error(
18241
- "Cursor billing history ended before its reported count."
18242
- );
18243
- }
18424
+ };
18425
+ await readUsagePages();
18244
18426
  const endpoint = new URL(config2.endpoint);
18245
18427
  endpoint.pathname = "/api/traces/cursor-billing";
18246
18428
  let matched = 0;
@@ -18615,11 +18797,14 @@ async function syncCodexBilling(config2, fetcher = collectorFetch, signal) {
18615
18797
  );
18616
18798
  const filenames = await sourceFiles("codex", config2.codexHome);
18617
18799
  const threads = [];
18618
- for (const filename of filenames) {
18619
- signal?.throwIfAborted();
18620
- const thread = await localThread(filename);
18621
- if (thread !== null) threads.push(thread);
18622
- }
18800
+ const readLocalThreads = async () => {
18801
+ for (const filename of filenames) {
18802
+ signal?.throwIfAborted();
18803
+ const thread = await localThread(filename);
18804
+ if (thread !== null) threads.push(thread);
18805
+ }
18806
+ };
18807
+ await readLocalThreads();
18623
18808
  const allTasks = groupCodexTasks(threads, auth.accountId);
18624
18809
  const retryIds = new Set(audit ? prior?.unavailableThreadIds ?? [] : []);
18625
18810
  const selected = allTasks.filter(
@@ -18657,36 +18842,40 @@ async function syncCodexBilling(config2, fetcher = collectorFetch, signal) {
18657
18842
  let apiTurns = 0;
18658
18843
  const endpoint = new URL(config2.endpoint);
18659
18844
  endpoint.pathname = "/api/traces/codex-billing";
18660
- const batches = [];
18661
- let current = [];
18662
- let currentIds = 0;
18663
- let currentTurns = 0;
18664
- for (const task of selected) {
18665
- const size = 1 + task.descendantThreadIds.length;
18666
- const turns = [task.threadId, ...task.descendantThreadIds].reduce(
18667
- (total, threadId) => total + (byThread.get(threadId)?.length ?? 0),
18668
- 0
18669
- );
18670
- if (size > 1e3)
18671
- throw new Error("Codex task exceeds the billing thread limit.");
18672
- if (current.length >= TASK_BATCH_SIZE || currentIds + size > 1e3 || currentTurns + turns > 5e3) {
18673
- if (current.length > 0) batches.push(current);
18674
- current = [];
18675
- currentIds = 0;
18676
- currentTurns = 0;
18677
- }
18678
- current.push(task);
18679
- currentIds += size;
18680
- currentTurns += turns;
18681
- if (turns > 5e3) {
18682
- batches.push(current);
18683
- current = [];
18684
- currentIds = 0;
18685
- currentTurns = 0;
18686
- }
18687
- }
18688
- if (current.length > 0) batches.push(current);
18689
- for (const batch of batches) {
18845
+ const createBatches = () => {
18846
+ const batches2 = [];
18847
+ let current = [];
18848
+ let currentIds = 0;
18849
+ let currentTurns = 0;
18850
+ for (const task of selected) {
18851
+ const size = 1 + task.descendantThreadIds.length;
18852
+ const turns = [task.threadId, ...task.descendantThreadIds].reduce(
18853
+ (total, threadId) => total + (byThread.get(threadId)?.length ?? 0),
18854
+ 0
18855
+ );
18856
+ if (size > 1e3)
18857
+ throw new Error("Codex task exceeds the billing thread limit.");
18858
+ if (current.length >= TASK_BATCH_SIZE || currentIds + size > 1e3 || currentTurns + turns > 5e3) {
18859
+ if (current.length > 0) batches2.push(current);
18860
+ current = [];
18861
+ currentIds = 0;
18862
+ currentTurns = 0;
18863
+ }
18864
+ current.push(task);
18865
+ currentIds += size;
18866
+ currentTurns += turns;
18867
+ if (turns > 5e3) {
18868
+ batches2.push(current);
18869
+ current = [];
18870
+ currentIds = 0;
18871
+ currentTurns = 0;
18872
+ }
18873
+ }
18874
+ if (current.length > 0) batches2.push(current);
18875
+ return batches2;
18876
+ };
18877
+ const batches = createBatches();
18878
+ const syncBatch = async (batch) => {
18690
18879
  signal?.throwIfAborted();
18691
18880
  const responseTasks = auth.accessToken === null || auth.accountId === null ? /* @__PURE__ */ new Map() : await queryTaskUsage(batch, auth.accessToken, auth.accountId, signal);
18692
18881
  const tasks = batch.map((task) => {
@@ -18734,7 +18923,7 @@ async function syncCodexBilling(config2, fetcher = collectorFetch, signal) {
18734
18923
  throw new Error("Iteration returned an invalid Codex billing result.");
18735
18924
  if (result.data.matched < tasks.length) serverIncomplete = true;
18736
18925
  }
18737
- for (const task of tasks) {
18926
+ const reconcileTask = (task) => {
18738
18927
  if (task.dataStatus === "available" && task.balanceUsageCredits !== null && (task.usageSource === "included_plan" || task.usageSource === "plan_and_credits"))
18739
18928
  unavailable.delete(task.threadId);
18740
18929
  else if (task.dataStatus !== "not_queried")
@@ -18746,9 +18935,11 @@ async function syncCodexBilling(config2, fetcher = collectorFetch, signal) {
18746
18935
  else if (codexCreditSign(task.balanceUsageCredits) > 0)
18747
18936
  creditTasks += 1;
18748
18937
  }
18749
- }
18938
+ };
18939
+ for (const task of tasks) reconcileTask(task);
18750
18940
  apiTurns += batchTurns.filter((turn) => turn.mode === "api_key").length;
18751
- }
18941
+ };
18942
+ for (const batch of batches) await syncBatch(batch);
18752
18943
  await saveState2(stateFile, {
18753
18944
  accountFingerprint: auth.accountFingerprint,
18754
18945
  lastSyncedAtMs: nowMs,
@@ -18795,13 +18986,11 @@ async function persistHealth(health, paths) {
18795
18986
  try {
18796
18987
  await saveHealth(health, paths);
18797
18988
  } catch {
18798
- console.error(
18799
- JSON.stringify({
18800
- level: "warning",
18801
- operation: "save_collector_health",
18802
- message: "Local collector status could not be saved; collection continues."
18803
- })
18804
- );
18989
+ collectorLogger({
18990
+ level: "warning",
18991
+ operation: "save_collector_health",
18992
+ message: "Local collector status could not be saved; collection continues."
18993
+ });
18805
18994
  }
18806
18995
  }
18807
18996
  async function acquireCollectorLock(paths) {
@@ -18873,15 +19062,12 @@ async function runtimeStep(version2, signal) {
18873
19062
  signal
18874
19063
  });
18875
19064
  if (outcome.kind !== "updated") return "continue";
18876
- console.info(
18877
- JSON.stringify({
18878
- level: "info",
18879
- at: (/* @__PURE__ */ new Date()).toISOString(),
18880
- operation: "runtime_update",
18881
- from: outcome.from,
18882
- to: outcome.to
18883
- })
18884
- );
19065
+ collectorLogger({
19066
+ level: "info",
19067
+ operation: "runtime_update",
19068
+ from: outcome.from,
19069
+ to: outcome.to
19070
+ });
18885
19071
  return "restart";
18886
19072
  }
18887
19073
  function versionFailure(error2) {
@@ -18911,13 +19097,11 @@ async function runCollector(directory, expectedSource) {
18911
19097
  const loadedHealth = await healthSnapshot(paths);
18912
19098
  let health = loadedHealth.health;
18913
19099
  if (loadedHealth.kind === "unavailable") {
18914
- console.error(
18915
- JSON.stringify({
18916
- level: "warning",
18917
- operation: "load_collector_health",
18918
- message: "Local collector status could not be read; collection continues."
18919
- })
18920
- );
19100
+ collectorLogger({
19101
+ level: "warning",
19102
+ operation: "load_collector_health",
19103
+ message: "Local collector status could not be read; collection continues."
19104
+ });
18921
19105
  }
18922
19106
  if ("cursorHome" in config2) {
18923
19107
  await ensureCursorHistoryBackfill(config2.cursorHome, paths);
@@ -18933,37 +19117,34 @@ async function runCollector(directory, expectedSource) {
18933
19117
  const runtimeVersion = process.env.ITERATION_RUNTIME_VERSION;
18934
19118
  let lastRuntimeCheckAt = 0;
18935
19119
  let trialSettled = runtimeVersion === void 0;
18936
- while (!controller.signal.aborted) {
19120
+ const checkRuntime = async () => {
18937
19121
  if (runtimeVersion !== void 0 && Date.now() - lastRuntimeCheckAt >= RUNTIME_CHECK_INTERVAL_MS) {
18938
19122
  lastRuntimeCheckAt = Date.now();
18939
19123
  let step = "continue";
18940
19124
  try {
18941
19125
  step = await runtimeStep(runtimeVersion, controller.signal);
18942
19126
  } catch (error2) {
18943
- if (controller.signal.aborted) break;
18944
- console.error(
18945
- JSON.stringify({
18946
- level: "error",
18947
- at: (/* @__PURE__ */ new Date()).toISOString(),
18948
- source,
18949
- operation: "runtime_update",
18950
- causes: errorCauses(error2),
18951
- message: error2 instanceof Error ? error2.message : "The update check failed."
18952
- })
18953
- );
19127
+ if (controller.signal.aborted) return false;
19128
+ collectorLogger({
19129
+ level: "error",
19130
+ source,
19131
+ operation: "runtime_update",
19132
+ causes: errorCauses(error2),
19133
+ message: error2 instanceof Error ? error2.message : "The update check failed."
19134
+ });
18954
19135
  }
18955
19136
  if (step === "restart") {
18956
- console.info(
18957
- JSON.stringify({
18958
- level: "info",
18959
- at: (/* @__PURE__ */ new Date()).toISOString(),
18960
- source,
18961
- operation: "runtime_restart"
18962
- })
18963
- );
18964
- break;
19137
+ collectorLogger({
19138
+ level: "info",
19139
+ source,
19140
+ operation: "runtime_restart"
19141
+ });
19142
+ return false;
18965
19143
  }
18966
19144
  }
19145
+ return true;
19146
+ };
19147
+ const connectDetectedSources = async () => {
18967
19148
  if (Date.now() - lastAutoCheckAt >= 6e4) {
18968
19149
  lastAutoCheckAt = Date.now();
18969
19150
  try {
@@ -18971,25 +19152,50 @@ async function runCollector(directory, expectedSource) {
18971
19152
  if (consent?.anchorSource === source) {
18972
19153
  const connected = await connectNewDetectedSources(config2, consent);
18973
19154
  if (connected.length > 0) {
18974
- console.info(
18975
- JSON.stringify({
18976
- level: "info",
18977
- operation: "auto_connect_sources",
18978
- connected
18979
- })
18980
- );
19155
+ collectorLogger({
19156
+ level: "info",
19157
+ operation: "auto_connect_sources",
19158
+ connected
19159
+ });
18981
19160
  }
18982
19161
  }
18983
19162
  } catch (error2) {
18984
- console.error(
18985
- JSON.stringify({
18986
- level: "error",
18987
- operation: "auto_connect_sources",
18988
- message: error2 instanceof Error ? error2.message : "Automatic source connection failed."
18989
- })
18990
- );
19163
+ collectorLogger({
19164
+ level: "error",
19165
+ operation: "auto_connect_sources",
19166
+ message: error2 instanceof Error ? error2.message : "Automatic source connection failed."
19167
+ });
18991
19168
  }
18992
19169
  }
19170
+ };
19171
+ const recordTickSuccess = async (result, acknowledgedAtMs) => {
19172
+ if (acknowledgedAtMs !== null || health.lastFailure !== null || result.historyCaughtUp && health.historyCaughtUpAtMs === null) {
19173
+ health = {
19174
+ lastUploadAtMs: acknowledgedAtMs ?? health.lastUploadAtMs,
19175
+ historyCaughtUpAtMs: health.historyCaughtUpAtMs ?? (result.historyCaughtUp ? Date.now() : null),
19176
+ lastFailure: null
19177
+ };
19178
+ await persistHealth(health, paths);
19179
+ }
19180
+ if (!trialSettled && runtimeVersion !== void 0) {
19181
+ await confirmRuntimeTrial(runtimeVersion, source);
19182
+ trialSettled = true;
19183
+ }
19184
+ if (result.discovered > 0 || result.uploaded > 0) {
19185
+ collectorLogger({
19186
+ level: "info",
19187
+ source,
19188
+ operation: "collector_tick",
19189
+ discovered: result.discovered,
19190
+ uploaded: result.uploaded,
19191
+ outboxBytes: result.outboxBytes
19192
+ });
19193
+ }
19194
+ retryDelayMs = Math.max(5e3, result.retryAfterMs);
19195
+ if (result.retryAfterMs > 0)
19196
+ retryDelayMs += Math.floor(Math.random() * 1e3);
19197
+ };
19198
+ const collect = async () => {
18993
19199
  let acknowledgedAtMs = null;
18994
19200
  let billingReady = false;
18995
19201
  let history = null;
@@ -19010,33 +19216,9 @@ async function runCollector(directory, expectedSource) {
19010
19216
  complete: billingReady && result.history.readSessions === result.history.sessions
19011
19217
  };
19012
19218
  }
19013
- if (acknowledgedAtMs !== null || health.lastFailure !== null || result.historyCaughtUp && health.historyCaughtUpAtMs === null) {
19014
- health = {
19015
- lastUploadAtMs: acknowledgedAtMs ?? health.lastUploadAtMs,
19016
- historyCaughtUpAtMs: health.historyCaughtUpAtMs ?? (result.historyCaughtUp ? Date.now() : null),
19017
- lastFailure: null
19018
- };
19019
- await persistHealth(health, paths);
19020
- }
19021
- if (!trialSettled && runtimeVersion !== void 0) {
19022
- await confirmRuntimeTrial(runtimeVersion, source);
19023
- trialSettled = true;
19024
- }
19025
- if (result.discovered > 0 || result.uploaded > 0) {
19026
- console.info(
19027
- JSON.stringify({
19028
- level: "info",
19029
- at: (/* @__PURE__ */ new Date()).toISOString(),
19030
- source,
19031
- discovered: result.discovered,
19032
- uploaded: result.uploaded,
19033
- outboxBytes: result.outboxBytes
19034
- })
19035
- );
19036
- }
19037
- retryDelayMs = 5e3;
19219
+ await recordTickSuccess(result, acknowledgedAtMs);
19038
19220
  } catch (error2) {
19039
- if (controller.signal.aborted) break;
19221
+ if (controller.signal.aborted) return null;
19040
19222
  if (!trialSettled && runtimeVersion !== void 0) {
19041
19223
  if (versionFailure(error2) && await onTrial(runtimeVersion))
19042
19224
  throw error2;
@@ -19048,20 +19230,20 @@ async function runCollector(directory, expectedSource) {
19048
19230
  historyCaughtUpAtMs: health.historyCaughtUpAtMs,
19049
19231
  lastFailure: collectorFailure(error2)
19050
19232
  };
19051
- console.error(
19052
- JSON.stringify({
19053
- level: "error",
19054
- at: (/* @__PURE__ */ new Date()).toISOString(),
19055
- source,
19056
- operation: "collector_tick",
19057
- errorName: error2 instanceof Error ? error2.name : "UnknownError",
19058
- causes: errorCauses(error2),
19059
- message: error2 instanceof Error ? error2.message : "Unknown collector failure."
19060
- })
19061
- );
19233
+ collectorLogger({
19234
+ level: "error",
19235
+ source,
19236
+ operation: "collector_tick",
19237
+ errorName: error2 instanceof Error ? error2.name : "UnknownError",
19238
+ causes: errorCauses(error2),
19239
+ message: error2 instanceof Error ? error2.message : "Unknown collector failure."
19240
+ });
19062
19241
  await persistHealth(health, paths);
19063
19242
  retryDelayMs = Math.min(retryDelayMs * 2, 6e4);
19064
19243
  }
19244
+ return { history, billingReady };
19245
+ };
19246
+ const reportHistory = async (history) => {
19065
19247
  if (history !== null) {
19066
19248
  try {
19067
19249
  await historyProgress.report(
@@ -19072,20 +19254,20 @@ async function runCollector(directory, expectedSource) {
19072
19254
  Date.now()
19073
19255
  );
19074
19256
  } catch (error2) {
19075
- if (controller.signal.aborted) break;
19076
- console.error(
19077
- JSON.stringify({
19078
- level: "warning",
19079
- at: (/* @__PURE__ */ new Date()).toISOString(),
19080
- source,
19081
- operation: "report_history_progress",
19082
- errorName: error2 instanceof Error ? error2.name : "UnknownError",
19083
- causes: errorCauses(error2),
19084
- message: error2 instanceof Error ? error2.message : "Unknown history progress failure."
19085
- })
19086
- );
19257
+ if (controller.signal.aborted) return false;
19258
+ collectorLogger({
19259
+ level: "warning",
19260
+ source,
19261
+ operation: "report_history_progress",
19262
+ errorName: error2 instanceof Error ? error2.name : "UnknownError",
19263
+ causes: errorCauses(error2),
19264
+ message: error2 instanceof Error ? error2.message : "Unknown history progress failure."
19265
+ });
19087
19266
  }
19088
19267
  }
19268
+ return true;
19269
+ };
19270
+ const refreshSummaries = async () => {
19089
19271
  try {
19090
19272
  if (source !== "cursor")
19091
19273
  await refreshTurnSummaries(
@@ -19096,19 +19278,19 @@ async function runCollector(directory, expectedSource) {
19096
19278
  controller.signal
19097
19279
  );
19098
19280
  } catch (error2) {
19099
- if (controller.signal.aborted) break;
19100
- console.error(
19101
- JSON.stringify({
19102
- level: "warning",
19103
- at: (/* @__PURE__ */ new Date()).toISOString(),
19104
- source,
19105
- operation: "refresh_turn_summaries",
19106
- errorName: error2 instanceof Error ? error2.name : "UnknownError",
19107
- causes: errorCauses(error2),
19108
- message: error2 instanceof Error ? error2.message : "Unknown turn summary failure."
19109
- })
19110
- );
19281
+ if (controller.signal.aborted) return false;
19282
+ collectorLogger({
19283
+ level: "warning",
19284
+ source,
19285
+ operation: "refresh_turn_summaries",
19286
+ errorName: error2 instanceof Error ? error2.name : "UnknownError",
19287
+ causes: errorCauses(error2),
19288
+ message: error2 instanceof Error ? error2.message : "Unknown turn summary failure."
19289
+ });
19111
19290
  }
19291
+ return true;
19292
+ };
19293
+ const syncBilling = async (billingReady) => {
19112
19294
  if ("cursorHome" in config2 && billingReady && Date.now() - lastBillingSyncAt >= 6 * 60 * 60 * 1e3) {
19113
19295
  try {
19114
19296
  const billing = await syncCursorBilling(
@@ -19117,23 +19299,19 @@ async function runCollector(directory, expectedSource) {
19117
19299
  controller.signal
19118
19300
  );
19119
19301
  lastBillingSyncAt = billing.skipped ? Date.now() - 6 * 60 * 60 * 1e3 + 5 * 60 * 1e3 : Date.now();
19120
- console.info(
19121
- JSON.stringify({
19122
- level: "info",
19123
- operation: "sync_cursor_billing",
19124
- ...billing
19125
- })
19126
- );
19302
+ collectorLogger({
19303
+ level: "info",
19304
+ operation: "sync_cursor_billing",
19305
+ ...billing
19306
+ });
19127
19307
  } catch (error2) {
19128
- if (controller.signal.aborted) break;
19308
+ if (controller.signal.aborted) return false;
19129
19309
  lastBillingSyncAt = Date.now() - 6 * 60 * 60 * 1e3 + 5 * 60 * 1e3;
19130
- console.error(
19131
- JSON.stringify({
19132
- level: "warning",
19133
- operation: "sync_cursor_billing",
19134
- message: error2 instanceof Error ? error2.message : "Cursor billing sync failed."
19135
- })
19136
- );
19310
+ collectorLogger({
19311
+ level: "warning",
19312
+ operation: "sync_cursor_billing",
19313
+ message: error2 instanceof Error ? error2.message : "Cursor billing sync failed."
19314
+ });
19137
19315
  }
19138
19316
  }
19139
19317
  if ("codexHome" in config2 && billingReady && Date.now() - lastBillingSyncAt >= 6 * 60 * 60 * 1e3) {
@@ -19144,25 +19322,31 @@ async function runCollector(directory, expectedSource) {
19144
19322
  controller.signal
19145
19323
  );
19146
19324
  lastBillingSyncAt = billing.skipped ? Date.now() - 6 * 60 * 60 * 1e3 + 5 * 60 * 1e3 : Date.now();
19147
- console.info(
19148
- JSON.stringify({
19149
- level: "info",
19150
- operation: "sync_codex_billing",
19151
- ...billing
19152
- })
19153
- );
19325
+ collectorLogger({
19326
+ level: "info",
19327
+ operation: "sync_codex_billing",
19328
+ ...billing
19329
+ });
19154
19330
  } catch (error2) {
19155
- if (controller.signal.aborted) break;
19331
+ if (controller.signal.aborted) return false;
19156
19332
  lastBillingSyncAt = Date.now() - 6 * 60 * 60 * 1e3 + 5 * 60 * 1e3;
19157
- console.error(
19158
- JSON.stringify({
19159
- level: "warning",
19160
- operation: "sync_codex_billing",
19161
- message: error2 instanceof Error ? error2.message : "Codex billing sync failed."
19162
- })
19163
- );
19333
+ collectorLogger({
19334
+ level: "warning",
19335
+ operation: "sync_codex_billing",
19336
+ message: error2 instanceof Error ? error2.message : "Codex billing sync failed."
19337
+ });
19164
19338
  }
19165
19339
  }
19340
+ return true;
19341
+ };
19342
+ while (!controller.signal.aborted) {
19343
+ if (!await checkRuntime()) break;
19344
+ await connectDetectedSources();
19345
+ const result = await collect();
19346
+ if (result === null) break;
19347
+ if (!await reportHistory(result.history)) break;
19348
+ if (!await refreshSummaries()) break;
19349
+ if (!await syncBilling(result.billingReady)) break;
19166
19350
  try {
19167
19351
  await delay4(retryDelayMs, void 0, { signal: controller.signal });
19168
19352
  } catch (error2) {
@@ -19372,76 +19556,90 @@ function renderHome(status2, nowMs = Date.now(), source = "codex") {
19372
19556
  workspaceName === null ? "Iteration" : `Iteration \xB7 ${workspaceName}`,
19373
19557
  ""
19374
19558
  ];
19375
- if (!status2.running) {
19376
- lines.push(`${name} is stopped`, "New sessions are not being collected.");
19377
- if (status2.pendingEvents > 0) lines.push(`${pendingDescription(status2)}.`);
19378
- lines.push(`Resume with iteration start ${source}.`);
19379
- } else if (status2.hookFailureAtMs !== null && status2.hookFailureAtMs !== void 0) {
19380
- lines.push(
19381
- `${name} hook capture needs attention`,
19382
- `A ${name} hook failed ${relativeTime(status2.hookFailureAtMs, nowMs)}. Some data may be missing. See iteration status ${source}.`
19383
- );
19384
- } else if (!status2.healthAvailable) {
19385
- lines.push(
19386
- `${name} is running; local status needs attention`,
19387
- "Upload history could not be read. Collection continues in the background."
19388
- );
19389
- if (status2.pendingEvents > 0) lines.push(`${pendingDescription(status2)}.`);
19390
- lines.push(`Details: iteration status ${source}`);
19391
- } else if (status2.health.lastFailure !== null) {
19392
- const failure2 = failureDescription(status2, source);
19393
- lines.push(failure2.heading, failure2.explanation);
19394
- if (status2.pendingEvents > 0) lines.push(`${pendingDescription(status2)}.`);
19395
- if (status2.health.lastUploadAtMs !== null) {
19559
+ const appendConnectionStatus = () => {
19560
+ if (!status2.running) {
19561
+ lines.push(`${name} is stopped`, "New sessions are not being collected.");
19562
+ if (status2.pendingEvents > 0)
19563
+ lines.push(`${pendingDescription(status2)}.`);
19564
+ lines.push(`Resume with iteration start ${source}.`);
19565
+ } else if (status2.hookFailureAtMs !== null && status2.hookFailureAtMs !== void 0) {
19566
+ lines.push(
19567
+ `${name} hook capture needs attention`,
19568
+ `A ${name} hook failed ${relativeTime(status2.hookFailureAtMs, nowMs)}. Some data may be missing. See iteration status ${source}.`
19569
+ );
19570
+ } else if (!status2.healthAvailable) {
19571
+ lines.push(
19572
+ `${name} is running; local status needs attention`,
19573
+ "Upload history could not be read. Collection continues in the background."
19574
+ );
19575
+ if (status2.pendingEvents > 0)
19576
+ lines.push(`${pendingDescription(status2)}.`);
19577
+ lines.push(`Details: iteration status ${source}`);
19578
+ } else if (status2.health.lastFailure !== null) {
19579
+ const failure2 = failureDescription(status2, source);
19580
+ lines.push(failure2.heading, failure2.explanation);
19581
+ if (status2.pendingEvents > 0)
19582
+ lines.push(`${pendingDescription(status2)}.`);
19583
+ if (status2.health.lastUploadAtMs !== null) {
19584
+ lines.push(
19585
+ `Last successful upload ${relativeTime(status2.health.lastUploadAtMs, nowMs)}.`
19586
+ );
19587
+ }
19588
+ lines.push(`Details: iteration status ${source}`);
19589
+ } else return false;
19590
+ return true;
19591
+ };
19592
+ const appendScanStatus = () => {
19593
+ if (status2.scan.kind === "waiting") {
19594
+ lines.push(
19595
+ `${name} is preparing your history`,
19596
+ progressDescription(status2.scan, source)
19597
+ );
19598
+ } else if (status2.scan.kind === "scanning" && status2.scan.totalBytes > 0 && status2.scan.scannedBytes < status2.scan.totalBytes && status2.health.historyCaughtUpAtMs === null) {
19599
+ lines.push(
19600
+ `${name} is importing existing sessions`,
19601
+ historyDescription(status2, source),
19602
+ "New activity is collected automatically."
19603
+ );
19604
+ } else if (status2.scan.kind === "scanning" && status2.scan.totalBytes - status2.scan.scannedBytes >= 1e6) {
19605
+ lines.push(
19606
+ `${name} is catching up with new activity`,
19607
+ historyDescription(status2, source)
19608
+ );
19609
+ if (status2.pendingEvents > 0)
19610
+ lines.push(`${pendingDescription(status2)}.`);
19611
+ } else if (status2.pendingEvents > 0) {
19612
+ lines.push(
19613
+ `${name} is uploading captured sessions`,
19614
+ `${pendingDescription(status2)}.`
19615
+ );
19616
+ } else if (status2.scan.totalBytes === 0) {
19617
+ lines.push(
19618
+ `${name} is connected`,
19619
+ `Waiting for ${name} sessions on this computer.`
19620
+ );
19621
+ } else if (status2.scan.kind === "scanning" && status2.scan.scannedBytes < status2.scan.totalBytes) {
19622
+ lines.push(
19623
+ `${name} is collecting new activity`,
19624
+ "Local history is imported; current activity is being scanned."
19625
+ );
19626
+ } else {
19627
+ lines.push(
19628
+ `${name} is collecting`,
19629
+ "Local scan is caught up and no uploads are waiting.",
19630
+ "New activity is collected automatically."
19631
+ );
19632
+ }
19633
+ };
19634
+ if (!appendConnectionStatus()) appendScanStatus();
19635
+ const appendLastUpload = () => {
19636
+ if (status2.running && status2.health.lastFailure === null && status2.health.lastUploadAtMs !== null) {
19396
19637
  lines.push(
19397
19638
  `Last successful upload ${relativeTime(status2.health.lastUploadAtMs, nowMs)}.`
19398
19639
  );
19399
19640
  }
19400
- lines.push(`Details: iteration status ${source}`);
19401
- } else if (status2.scan.kind === "waiting") {
19402
- lines.push(
19403
- `${name} is preparing your history`,
19404
- progressDescription(status2.scan, source)
19405
- );
19406
- } else if (status2.scan.kind === "scanning" && status2.scan.totalBytes > 0 && status2.scan.scannedBytes < status2.scan.totalBytes && status2.health.historyCaughtUpAtMs === null) {
19407
- lines.push(
19408
- `${name} is importing existing sessions`,
19409
- historyDescription(status2, source),
19410
- "New activity is collected automatically."
19411
- );
19412
- } else if (status2.scan.kind === "scanning" && status2.scan.totalBytes - status2.scan.scannedBytes >= 1e6) {
19413
- lines.push(
19414
- `${name} is catching up with new activity`,
19415
- historyDescription(status2, source)
19416
- );
19417
- if (status2.pendingEvents > 0) lines.push(`${pendingDescription(status2)}.`);
19418
- } else if (status2.pendingEvents > 0) {
19419
- lines.push(
19420
- `${name} is uploading captured sessions`,
19421
- `${pendingDescription(status2)}.`
19422
- );
19423
- } else if (status2.scan.totalBytes === 0) {
19424
- lines.push(
19425
- `${name} is connected`,
19426
- `Waiting for ${name} sessions on this computer.`
19427
- );
19428
- } else if (status2.scan.kind === "scanning" && status2.scan.scannedBytes < status2.scan.totalBytes) {
19429
- lines.push(
19430
- `${name} is collecting new activity`,
19431
- "Local history is imported; current activity is being scanned."
19432
- );
19433
- } else {
19434
- lines.push(
19435
- `${name} is collecting`,
19436
- "Local scan is caught up and no uploads are waiting.",
19437
- "New activity is collected automatically."
19438
- );
19439
- }
19440
- if (status2.running && status2.health.lastFailure === null && status2.health.lastUploadAtMs !== null) {
19441
- lines.push(
19442
- `Last successful upload ${relativeTime(status2.health.lastUploadAtMs, nowMs)}.`
19443
- );
19444
- }
19641
+ };
19642
+ appendLastUpload();
19445
19643
  if (status2.turnSummaryNeedsApproval) lines.push("", CODEX_APPROVAL_STEP);
19446
19644
  lines.push("", `Explore traces ${status2.tracesUrl}`);
19447
19645
  return lines.join("\n");
@@ -19884,7 +20082,7 @@ async function connectAll(values) {
19884
20082
  );
19885
20083
  }
19886
20084
  const configs = await Promise.all(COLLECTOR_SOURCES.map(connectedConfig));
19887
- const sourceHomes = {
20085
+ const resolveSourceHomes = () => ({
19888
20086
  codex: path24.resolve(
19889
20087
  optionalString(values, "codex-home") ?? (configs[0] && "codexHome" in configs[0] ? configs[0].codexHome : sourceHome("codex"))
19890
20088
  ),
@@ -19894,7 +20092,8 @@ async function connectAll(values) {
19894
20092
  cursor: path24.resolve(
19895
20093
  optionalString(values, "cursor-home") ?? (configs[2] && "cursorHome" in configs[2] ? configs[2].cursorHome : sourceHome("cursor"))
19896
20094
  )
19897
- };
20095
+ });
20096
+ const sourceHomes = resolveSourceHomes();
19898
20097
  const detected = await detectedSources(sourceHomes);
19899
20098
  const existing = COLLECTOR_SOURCES.flatMap(
19900
20099
  (source, index) => configs[index] ? [{ source, config: configs[index] }] : []
@@ -19916,63 +20115,76 @@ async function connectAll(values) {
19916
20115
  scope: await readCollectorScope(item.config)
19917
20116
  }))
19918
20117
  );
19919
- for (const item of existingScopes) {
19920
- if (item.config.endpoint !== endpoint) {
19921
- throw new Error(
19922
- "Existing sources use different Iteration servers. Connect one source at a time."
19923
- );
20118
+ const validateWorkspace = () => {
20119
+ for (const item of existingScopes) {
20120
+ if (item.config.endpoint !== endpoint) {
20121
+ throw new Error(
20122
+ "Existing sources use different Iteration servers. Connect one source at a time."
20123
+ );
20124
+ }
20125
+ if (existingScopes[0]?.scope.organizationId !== item.scope.organizationId) {
20126
+ throw new Error(
20127
+ "Existing sources use different workspaces. Connect one source at a time."
20128
+ );
20129
+ }
19924
20130
  }
19925
- if (existingScopes[0]?.scope.organizationId !== item.scope.organizationId) {
20131
+ const requestedOrganizationId2 = optionalString(values, "workspace");
20132
+ if (requestedOrganizationId2 && existingScopes[0] && requestedOrganizationId2 !== existingScopes[0].scope.organizationId) {
19926
20133
  throw new Error(
19927
- "Existing sources use different workspaces. Connect one source at a time."
20134
+ "The existing collector belongs to a different workspace."
19928
20135
  );
19929
20136
  }
19930
- }
20137
+ };
20138
+ validateWorkspace();
19931
20139
  const requestedOrganizationId = optionalString(values, "workspace");
19932
- if (requestedOrganizationId && existingScopes[0] && requestedOrganizationId !== existingScopes[0].scope.organizationId) {
19933
- throw new Error("The existing collector belongs to a different workspace.");
19934
- }
19935
20140
  const previousConsent = await loadMachineConsent();
19936
- if (previousConsent === null || anchorExisting === null || previousConsent.endpoint !== endpoint || previousConsent.anchorSource !== anchorSource || previousConsent.anchorDataDirectory !== anchorExisting.dataDirectory || previousConsent.organizationId !== existingScopes[0]?.scope.organizationId || previousConsent.sourceHomes.codex !== sourceHomes.codex || previousConsent.sourceHomes.claude !== sourceHomes.claude || previousConsent.sourceHomes.cursor !== sourceHomes.cursor) {
19937
- await approveAllSources(
19938
- await Promise.all(
19939
- detected.map(async (source) => ({
19940
- source,
19941
- sessions: await countSavedSessions(source, sourceHomes[source])
19942
- }))
19943
- ),
19944
- existingScopes[0]?.scope.organizationName ?? null,
19945
- values.yes === true
19946
- );
19947
- }
20141
+ const approveConnection = async () => {
20142
+ const needsApproval = () => previousConsent === null || anchorExisting === null || previousConsent.endpoint !== endpoint || previousConsent.anchorSource !== anchorSource || previousConsent.anchorDataDirectory !== anchorExisting.dataDirectory || previousConsent.organizationId !== existingScopes[0]?.scope.organizationId || previousConsent.sourceHomes.codex !== sourceHomes.codex || previousConsent.sourceHomes.claude !== sourceHomes.claude || previousConsent.sourceHomes.cursor !== sourceHomes.cursor;
20143
+ if (needsApproval()) {
20144
+ await approveAllSources(
20145
+ await Promise.all(
20146
+ detected.map(async (source) => ({
20147
+ source,
20148
+ sessions: await countSavedSessions(source, sourceHomes[source])
20149
+ }))
20150
+ ),
20151
+ existingScopes[0]?.scope.organizationName ?? null,
20152
+ values.yes === true
20153
+ );
20154
+ }
20155
+ };
20156
+ await approveConnection();
19948
20157
  await prepareBackgroundPersistence();
19949
20158
  let anchor2 = anchorExisting ?? await initialize(anchorSource, values);
19950
20159
  let organizationId = requestedOrganizationId ?? existingScopes[0]?.scope.organizationId ?? anchor2.organizationId ?? null;
19951
- for (const source of detected) {
19952
- if (source === anchorSource || configs[COLLECTOR_SOURCES.indexOf(source)] !== null)
19953
- continue;
19954
- const paths = collectorPaths(defaultCollectorDirectory(source));
19955
- await assertFreshCollectorDirectory(paths, source);
19956
- const state = await prepareSourceInitialState(
19957
- source,
19958
- sourceHomes[source],
19959
- true
19960
- );
19961
- const enrollment = await expandSource(anchor2, source, organizationId);
19962
- const config2 = await activateSource(
19963
- source,
19964
- sourceHomes[source],
19965
- endpoint,
19966
- enrollment,
19967
- state
19968
- );
19969
- organizationId = enrollment.organizationId;
19970
- const notice = await turnSummaryNotice(config2);
19971
- process.stdout.write(
19972
- `${collectorSourceLabel(source)} is connected to ${enrollment.organizationName}.${notice}
20160
+ const connectDetectedSources = async () => {
20161
+ for (const source of detected) {
20162
+ if (source === anchorSource || configs[COLLECTOR_SOURCES.indexOf(source)] !== null)
20163
+ continue;
20164
+ const paths = collectorPaths(defaultCollectorDirectory(source));
20165
+ await assertFreshCollectorDirectory(paths, source);
20166
+ const state = await prepareSourceInitialState(
20167
+ source,
20168
+ sourceHomes[source],
20169
+ true
20170
+ );
20171
+ const enrollment = await expandSource(anchor2, source, organizationId);
20172
+ const config2 = await activateSource(
20173
+ source,
20174
+ sourceHomes[source],
20175
+ endpoint,
20176
+ enrollment,
20177
+ state
20178
+ );
20179
+ organizationId = enrollment.organizationId;
20180
+ const notice = await turnSummaryNotice(config2);
20181
+ process.stdout.write(
20182
+ `${collectorSourceLabel(source)} is connected to ${enrollment.organizationName}.${notice}
19973
20183
  `
19974
- );
19975
- }
20184
+ );
20185
+ }
20186
+ };
20187
+ await connectDetectedSources();
19976
20188
  if (organizationId === null) {
19977
20189
  organizationId = (await readCollectorScope(anchor2)).organizationId;
19978
20190
  }
@@ -20242,15 +20454,15 @@ async function update(setting, values) {
20242
20454
  process.stdout.write(`${await waitForTrial(outcome.to)}
20243
20455
  `);
20244
20456
  }
20245
- async function main() {
20457
+ async function runInternalCommand() {
20246
20458
  if (process.argv[2] === "supervise") {
20247
- const source2 = process.argv[3];
20459
+ const source = process.argv[3];
20248
20460
  const directory = process.argv[5];
20249
- if (source2 !== "codex" && source2 !== "claude" && source2 !== "cursor" || process.argv[4] !== "--data-directory" || !directory || process.argv.length !== 6) {
20461
+ if (source !== "codex" && source !== "claude" && source !== "cursor" || process.argv[4] !== "--data-directory" || !directory || process.argv.length !== 6) {
20250
20462
  throw new Error("Invalid collector supervisor command.");
20251
20463
  }
20252
- await superviseCollector(source2, directory);
20253
- return;
20464
+ await superviseCollector(source, directory);
20465
+ return true;
20254
20466
  }
20255
20467
  if (process.argv[2] === "claude-git-hook") {
20256
20468
  const directory = process.argv[3];
@@ -20262,21 +20474,31 @@ async function main() {
20262
20474
  await recordClaudeGitHookFailure(directory);
20263
20475
  throw error2;
20264
20476
  }
20265
- return;
20477
+ return true;
20266
20478
  }
20267
20479
  if (process.argv[2] === "turn-summary-hook") {
20268
20480
  const directory = process.argv[3];
20269
20481
  if (!directory || process.argv.length !== 4)
20270
20482
  throw new Error("Invalid turn summary hook command.");
20271
20483
  await captureTurnSummary(directory);
20272
- return;
20484
+ return true;
20273
20485
  }
20486
+ return false;
20487
+ }
20488
+ async function main() {
20489
+ if (await runInternalCommand()) return;
20274
20490
  const delegated = await delegateToInstalledVersion();
20275
20491
  if (delegated !== null) {
20276
20492
  process.exitCode = delegated;
20277
20493
  return;
20278
20494
  }
20279
- const { command, source, values } = argumentsForCli();
20495
+ return dispatchCommand(argumentsForCli());
20496
+ }
20497
+ async function dispatchCommand({
20498
+ command,
20499
+ source,
20500
+ values
20501
+ }) {
20280
20502
  const selectedSource = source === "codex" || source === "claude" || source === "cursor" ? source : null;
20281
20503
  if (values.help === true || command === "help") {
20282
20504
  return usage(command === "help" ? source : command);
@@ -20290,18 +20512,7 @@ async function main() {
20290
20512
  return connectAll(values);
20291
20513
  }
20292
20514
  if (command === "") {
20293
- if (Object.keys(values).length > 0) {
20294
- throw new Error(
20295
- "Options require a source. Run iteration connect --help for details."
20296
- );
20297
- }
20298
- if (isCollectorPlatform()) await adoptRuntime();
20299
- await defaultCommand();
20300
- const summary = await runtimeSummaryText();
20301
- if (summary !== null) process.stdout.write(`
20302
- ${summary}
20303
- `);
20304
- return;
20515
+ return showHome(values);
20305
20516
  }
20306
20517
  if (command === "version") {
20307
20518
  const state = await readRuntimeState(runtimePaths());
@@ -20311,13 +20522,45 @@ ${summary}
20311
20522
  }
20312
20523
  if (command === "self-check") return selfCheck2();
20313
20524
  if (command === "update") return update(source, values);
20314
- if (selectedSource !== null) {
20315
- for (const name of ["codex", "claude", "cursor"])
20316
- if (selectedSource !== name && values[`${name}-home`] !== void 0)
20317
- throw new Error(
20318
- `--${name}-home requires the ${collectorSourceLabel(name)} source.`
20319
- );
20525
+ return dispatchSourceCommand(command, source, values, selectedSource);
20526
+ }
20527
+ async function showHome(values) {
20528
+ if (Object.keys(values).length > 0) {
20529
+ throw new Error(
20530
+ "Options require a source. Run iteration connect --help for details."
20531
+ );
20532
+ }
20533
+ if (isCollectorPlatform()) await adoptRuntime();
20534
+ await defaultCommand();
20535
+ const summary = await runtimeSummaryText();
20536
+ if (summary !== null) process.stdout.write(`
20537
+ ${summary}
20538
+ `);
20539
+ }
20540
+ async function disconnect(selectedSource, values) {
20541
+ await uninstallBackgroundService(selectedSource);
20542
+ const paths = collectorPaths(dataDirectory(selectedSource, values));
20543
+ if (await fileExists(paths.config)) {
20544
+ const config2 = await loadConfig(paths);
20545
+ if ("cursorHome" in config2)
20546
+ await uninstallCursorHooks(config2.cursorHome, config2.dataDirectory);
20547
+ else await uninstallTurnSummary(config2);
20320
20548
  }
20549
+ process.stdout.write(
20550
+ "Collector startup removed. Local pending data was retained.\n"
20551
+ );
20552
+ }
20553
+ async function dispatchSourceCommand(command, source, values, selectedSource) {
20554
+ const validateSourceOptions = () => {
20555
+ if (selectedSource !== null) {
20556
+ for (const name of ["codex", "claude", "cursor"])
20557
+ if (selectedSource !== name && values[`${name}-home`] !== void 0)
20558
+ throw new Error(
20559
+ `--${name}-home requires the ${collectorSourceLabel(name)} source.`
20560
+ );
20561
+ }
20562
+ };
20563
+ validateSourceOptions();
20321
20564
  if (command === "connect" && selectedSource !== null) {
20322
20565
  await initialize(selectedSource, values);
20323
20566
  return;
@@ -20331,39 +20574,30 @@ ${summary}
20331
20574
  );
20332
20575
  }
20333
20576
  if (command === "status" && (source === void 0 || selectedSource !== null)) {
20334
- if (source === void 0 && values["data-directory"] !== void 0)
20335
- throw new Error("--data-directory requires a source with status.");
20336
- requireCollectorPlatform();
20337
- await adoptRuntime();
20338
- await status(selectedSource ?? void 0, values);
20339
- const summary = await runtimeSummaryText();
20340
- if (summary !== null) process.stdout.write(`
20341
- ${summary}
20342
- `);
20343
- return;
20577
+ return showStatus(source, selectedSource, values);
20344
20578
  }
20345
20579
  if (command === "import-history" && selectedSource !== null)
20346
20580
  return importHistory(selectedSource, values);
20347
20581
  if (command === "start" && selectedSource !== null)
20348
20582
  return start(selectedSource, values);
20349
20583
  if (command === "disconnect" && selectedSource !== null) {
20350
- await uninstallBackgroundService(selectedSource);
20351
- const paths = collectorPaths(dataDirectory(selectedSource, values));
20352
- if (await fileExists(paths.config)) {
20353
- const config2 = await loadConfig(paths);
20354
- if ("cursorHome" in config2)
20355
- await uninstallCursorHooks(config2.cursorHome, config2.dataDirectory);
20356
- else await uninstallTurnSummary(config2);
20357
- }
20358
- process.stdout.write(
20359
- "Collector startup removed. Local pending data was retained.\n"
20360
- );
20361
- return;
20584
+ return disconnect(selectedSource, values);
20362
20585
  }
20363
20586
  throw new Error(
20364
20587
  `Unknown Iteration command: ${[command, source].filter(Boolean).join(" ")}. Run iteration help for usage.`
20365
20588
  );
20366
20589
  }
20590
+ async function showStatus(source, selectedSource, values) {
20591
+ if (source === void 0 && values["data-directory"] !== void 0)
20592
+ throw new Error("--data-directory requires a source with status.");
20593
+ requireCollectorPlatform();
20594
+ await adoptRuntime();
20595
+ await status(selectedSource ?? void 0, values);
20596
+ const summary = await runtimeSummaryText();
20597
+ if (summary !== null) process.stdout.write(`
20598
+ ${summary}
20599
+ `);
20600
+ }
20367
20601
  var background = process.argv[2] === "run";
20368
20602
  main().then(
20369
20603
  () => {