@indigoai-us/hq-cloud 6.15.29 → 6.15.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  2. package/dist/bin/sync-runner-company.js +24 -13
  3. package/dist/bin/sync-runner-company.js.map +1 -1
  4. package/dist/bin/sync-runner-company.test.js +0 -71
  5. package/dist/bin/sync-runner-company.test.js.map +1 -1
  6. package/dist/bin/sync-runner-watch-loop.d.ts +0 -10
  7. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  8. package/dist/bin/sync-runner-watch-loop.js +13 -170
  9. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  10. package/dist/bin/sync-runner-watch-routes.d.ts +1 -16
  11. package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
  12. package/dist/bin/sync-runner-watch-routes.js +2 -40
  13. package/dist/bin/sync-runner-watch-routes.js.map +1 -1
  14. package/dist/bin/sync-runner-watch-routes.test.js +1 -53
  15. package/dist/bin/sync-runner-watch-routes.test.js.map +1 -1
  16. package/dist/bin/sync-runner.d.ts +3 -15
  17. package/dist/bin/sync-runner.d.ts.map +1 -1
  18. package/dist/bin/sync-runner.js +13 -66
  19. package/dist/bin/sync-runner.js.map +1 -1
  20. package/dist/bin/sync-runner.test.js +13 -433
  21. package/dist/bin/sync-runner.test.js.map +1 -1
  22. package/dist/cli/conflict.d.ts +2 -25
  23. package/dist/cli/conflict.d.ts.map +1 -1
  24. package/dist/cli/conflict.js +4 -29
  25. package/dist/cli/conflict.js.map +1 -1
  26. package/dist/cli/share.d.ts +4 -40
  27. package/dist/cli/share.d.ts.map +1 -1
  28. package/dist/cli/share.js +86 -478
  29. package/dist/cli/share.js.map +1 -1
  30. package/dist/cli/share.test.js +150 -845
  31. package/dist/cli/share.test.js.map +1 -1
  32. package/dist/cli/sync-scope.test.js +18 -147
  33. package/dist/cli/sync-scope.test.js.map +1 -1
  34. package/dist/cli/sync.d.ts +0 -20
  35. package/dist/cli/sync.d.ts.map +1 -1
  36. package/dist/cli/sync.js +29 -261
  37. package/dist/cli/sync.js.map +1 -1
  38. package/dist/cli/sync.test.js +90 -519
  39. package/dist/cli/sync.test.js.map +1 -1
  40. package/dist/journal.d.ts +0 -1
  41. package/dist/journal.d.ts.map +1 -1
  42. package/dist/journal.js +25 -155
  43. package/dist/journal.js.map +1 -1
  44. package/dist/journal.test.js +11 -29
  45. package/dist/journal.test.js.map +1 -1
  46. package/dist/object-io.d.ts +13 -34
  47. package/dist/object-io.d.ts.map +1 -1
  48. package/dist/object-io.js +29 -154
  49. package/dist/object-io.js.map +1 -1
  50. package/dist/outcome-telemetry.d.ts +1 -7
  51. package/dist/outcome-telemetry.d.ts.map +1 -1
  52. package/dist/outcome-telemetry.js +9 -15
  53. package/dist/outcome-telemetry.js.map +1 -1
  54. package/dist/outcome-telemetry.test.js +0 -15
  55. package/dist/outcome-telemetry.test.js.map +1 -1
  56. package/dist/prefix-coalesce.d.ts +0 -14
  57. package/dist/prefix-coalesce.d.ts.map +1 -1
  58. package/dist/prefix-coalesce.js +0 -29
  59. package/dist/prefix-coalesce.js.map +1 -1
  60. package/dist/prefix-coalesce.test.js +1 -22
  61. package/dist/prefix-coalesce.test.js.map +1 -1
  62. package/dist/skill-telemetry.d.ts +1 -6
  63. package/dist/skill-telemetry.d.ts.map +1 -1
  64. package/dist/skill-telemetry.js +8 -14
  65. package/dist/skill-telemetry.js.map +1 -1
  66. package/dist/skill-telemetry.test.js +0 -43
  67. package/dist/skill-telemetry.test.js.map +1 -1
  68. package/dist/sts-credential-refresh.test.js +2 -87
  69. package/dist/sts-credential-refresh.test.js.map +1 -1
  70. package/dist/sync/state-store.d.ts.map +1 -1
  71. package/dist/sync/state-store.js +1 -20
  72. package/dist/sync/state-store.js.map +1 -1
  73. package/dist/sync/state-store.test.js +0 -64
  74. package/dist/sync/state-store.test.js.map +1 -1
  75. package/dist/sync-core.d.ts.map +1 -1
  76. package/dist/sync-core.js +2 -7
  77. package/dist/sync-core.js.map +1 -1
  78. package/dist/sync-core.test.js +0 -36
  79. package/dist/sync-core.test.js.map +1 -1
  80. package/dist/telemetry.d.ts +6 -30
  81. package/dist/telemetry.d.ts.map +1 -1
  82. package/dist/telemetry.js +135 -331
  83. package/dist/telemetry.js.map +1 -1
  84. package/dist/telemetry.test.js +0 -403
  85. package/dist/telemetry.test.js.map +1 -1
  86. package/dist/types.d.ts +3 -26
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/watcher.d.ts +0 -76
  89. package/dist/watcher.d.ts.map +1 -1
  90. package/dist/watcher.js +3 -105
  91. package/dist/watcher.js.map +1 -1
  92. package/dist/watcher.test.js +1 -165
  93. package/dist/watcher.test.js.map +1 -1
  94. package/package.json +1 -1
  95. package/dist/bandwidth.d.ts +0 -128
  96. package/dist/bandwidth.d.ts.map +0 -1
  97. package/dist/bandwidth.js +0 -254
  98. package/dist/bandwidth.js.map +0 -1
  99. package/dist/bandwidth.test.d.ts +0 -2
  100. package/dist/bandwidth.test.d.ts.map +0 -1
  101. package/dist/bandwidth.test.js +0 -243
  102. package/dist/bandwidth.test.js.map +0 -1
  103. package/dist/journal-delta-writes.test.d.ts +0 -2
  104. package/dist/journal-delta-writes.test.d.ts.map +0 -1
  105. package/dist/journal-delta-writes.test.js +0 -140
  106. package/dist/journal-delta-writes.test.js.map +0 -1
package/dist/telemetry.js CHANGED
@@ -7,12 +7,10 @@
7
7
  * (`hq-sync-runner`, `hq-cli`, mobile wrappers) emits telemetry uniformly.
8
8
  *
9
9
  * What it does: after each successful sync (`all-complete` arm of
10
- * `bin/sync-runner.ts`), walks Grok `updates.jsonl` sessions, live and archived
11
- * Codex rollouts, and Claude session logs, then diffs each file against the
12
- * persisted byte-offset cursor at `~/.hq/telemetry-cursor.json`, sanitizes new
13
- * rows through a tight allowlist that matches the server's KEEP_FIELDS set in
14
- * `apps/hq-pro/src/vault-service/handlers/usage.ts`, batches into server-sized
15
- * POST bodies, and ships them to `/v1/usage`.
10
+ * `bin/sync-runner.ts`), walks Claude session logs plus live and archived Codex
11
+ * rollouts, then diffs each file against the persisted byte-offset cursor at `~/.hq/telemetry-cursor.json`, sanitizes new rows through a tight allowlist that matches the server's
12
+ * KEEP_FIELDS set in `apps/hq-pro/src/vault-service/handlers/usage.ts`,
13
+ * batches into server-sized POST bodies, and ships them to `/v1/usage`.
16
14
  *
17
15
  * Trust model: the caller's `personUid` is resolved on the server from the
18
16
  * Cognito JWT — never from the body. `sanitizeRow` strips prompt bodies,
@@ -432,167 +430,6 @@ async function listCodexRolloutFiles(codexRoot) {
432
430
  ]);
433
431
  return sortFilesFreshestFirst([...live, ...archived].filter((file) => path.basename(file).startsWith("rollout-")));
434
432
  }
435
- // ── Grok session adapter ─────────────────────────────────────────────────────
436
- function grokTimestamp(value) {
437
- if (typeof value === "string") {
438
- return boundedString(value, MAX_TIMESTAMP_BYTES);
439
- }
440
- if (typeof value === "number" && Number.isFinite(value)) {
441
- const ms = value > 1e12 ? value : value * 1000;
442
- try {
443
- return new Date(ms).toISOString();
444
- }
445
- catch {
446
- return undefined;
447
- }
448
- }
449
- return undefined;
450
- }
451
- function stableGrokEventId(sessionId, promptId, model) {
452
- return ("grok-" +
453
- createHash("sha256")
454
- .update(sessionId)
455
- .update(Buffer.from([0]))
456
- .update(promptId)
457
- .update(Buffer.from([0]))
458
- .update(model)
459
- .digest("hex"));
460
- }
461
- function grokUsageMessage(usage, model) {
462
- const message = {
463
- usage: {
464
- input_tokens: tokenCount(usage.inputTokens),
465
- output_tokens: tokenCount(usage.outputTokens) + tokenCount(usage.reasoningTokens),
466
- cache_read_input_tokens: tokenCount(usage.cachedReadTokens),
467
- cache_creation_input_tokens: tokenCount(usage.cacheCreationTokens),
468
- },
469
- };
470
- if (model)
471
- message.model = model;
472
- return message;
473
- }
474
- /**
475
- * Adapt a Grok `updates.jsonl` record to Claude-shaped sanitizer input(s).
476
- * Emits only on `turn_completed` with `usage`. When `usage.modelUsage` is
477
- * present, one row per model key (chart stacks by model id).
478
- */
479
- export function grokUsageRows(row, sessionMeta) {
480
- if (!row || typeof row !== "object" || Array.isArray(row))
481
- return [];
482
- const obj = row;
483
- const method = obj.method;
484
- if (method !== "session/update" && method !== "_x.ai/session/update") {
485
- return [];
486
- }
487
- const rawParams = obj.params;
488
- if (!rawParams || typeof rawParams !== "object" || Array.isArray(rawParams)) {
489
- return [];
490
- }
491
- const params = rawParams;
492
- const rawUpdate = params.update;
493
- if (!rawUpdate || typeof rawUpdate !== "object" || Array.isArray(rawUpdate)) {
494
- return [];
495
- }
496
- const update = rawUpdate;
497
- if (update.sessionUpdate !== "turn_completed")
498
- return [];
499
- const rawUsage = update.usage;
500
- if (!rawUsage || typeof rawUsage !== "object" || Array.isArray(rawUsage)) {
501
- return [];
502
- }
503
- const usage = rawUsage;
504
- const sessionId = boundedString(params.sessionId, MAX_ID_BYTES);
505
- const promptId = typeof update.prompt_id === "string" ? update.prompt_id : "";
506
- const timestamp = grokTimestamp(obj.timestamp);
507
- const updateMeta = update._meta;
508
- const paramsMeta = params._meta;
509
- const eventId = (updateMeta &&
510
- typeof updateMeta === "object" &&
511
- !Array.isArray(updateMeta)
512
- ? boundedString(updateMeta.eventId, MAX_ID_BYTES)
513
- : undefined) ??
514
- (paramsMeta &&
515
- typeof paramsMeta === "object" &&
516
- !Array.isArray(paramsMeta)
517
- ? boundedString(paramsMeta.eventId, MAX_ID_BYTES)
518
- : undefined);
519
- const buildRow = (model, modelUsage, preferEventId) => {
520
- const normalized = {
521
- uuid: preferEventId && eventId
522
- ? eventId
523
- : stableGrokEventId(sessionId ?? "", promptId, model ?? ""),
524
- message: grokUsageMessage(modelUsage, model),
525
- };
526
- if (sessionId)
527
- normalized.sessionId = sessionId;
528
- if (timestamp)
529
- normalized.timestamp = timestamp;
530
- if (sessionMeta.cwd)
531
- normalized.cwd = sessionMeta.cwd;
532
- if (sessionMeta.gitBranch)
533
- normalized.gitBranch = sessionMeta.gitBranch;
534
- return normalized;
535
- };
536
- const modelUsage = usage.modelUsage;
537
- if (modelUsage &&
538
- typeof modelUsage === "object" &&
539
- !Array.isArray(modelUsage)) {
540
- const entries = Object.entries(modelUsage).filter(([, entry]) => !!entry && typeof entry === "object" && !Array.isArray(entry));
541
- const single = entries.length === 1;
542
- const rows = [];
543
- for (const [modelKey, entry] of entries) {
544
- const model = boundedString(modelKey, MAX_MODEL_BYTES);
545
- if (!model)
546
- continue;
547
- rows.push(buildRow(model, entry, single));
548
- }
549
- return rows;
550
- }
551
- return [
552
- buildRow(sessionMeta.fallback_model, usage, true),
553
- ];
554
- }
555
- function decodeGrokCwdFolder(encoded) {
556
- try {
557
- return boundedString(decodeURIComponent(encoded), MAX_PATH_BYTES);
558
- }
559
- catch {
560
- return boundedString(encoded, MAX_PATH_BYTES);
561
- }
562
- }
563
- /** Load cwd / branch / fallback model from sibling `summary.json` once per file. */
564
- export async function loadGrokSessionMeta(updatesPath) {
565
- const sessionDir = path.dirname(updatesPath);
566
- const encodedCwdDir = path.basename(path.dirname(sessionDir));
567
- const meta = {};
568
- try {
569
- const raw = await fs.readFile(path.join(sessionDir, "summary.json"), "utf-8");
570
- const parsed = JSON.parse(raw);
571
- const info = parsed.info && typeof parsed.info === "object" && !Array.isArray(parsed.info)
572
- ? parsed.info
573
- : undefined;
574
- meta.cwd =
575
- boundedString(info?.cwd, MAX_PATH_BYTES) ??
576
- boundedString(parsed.cwd, MAX_PATH_BYTES);
577
- meta.gitBranch =
578
- boundedString(parsed.head_branch, MAX_PATH_BYTES) ??
579
- boundedString(info?.head_branch, MAX_PATH_BYTES);
580
- meta.fallback_model =
581
- boundedString(parsed.current_model_id, MAX_MODEL_BYTES) ??
582
- boundedString(info?.current_model_id, MAX_MODEL_BYTES);
583
- }
584
- catch {
585
- // Missing / unreadable summary — fall through to folder-name decode.
586
- }
587
- if (!meta.cwd) {
588
- meta.cwd = decodeGrokCwdFolder(encodedCwdDir);
589
- }
590
- return meta;
591
- }
592
- async function listGrokUpdateFiles(grokRoot) {
593
- const all = await listJsonlFiles(path.join(grokRoot, "sessions"));
594
- return sortFilesFreshestFirst(all.filter((file) => path.basename(file) === "updates.jsonl"));
595
- }
596
433
  // ── Batching primitives ───────────────────────────────────────────────────────
597
434
  const MAX_BATCH_EVENTS = 100;
598
435
  const MAX_BATCH_BYTES = 240 * 1024;
@@ -657,7 +494,7 @@ function boundRowForPost(row, maxRowBytes) {
657
494
  }
658
495
  // ── Main entry point ──────────────────────────────────────────────────────────
659
496
  /**
660
- * Scan, sanitize, and POST new Grok, Codex, and Claude Code usage rows.
497
+ * Scan, sanitize, and POST new Claude Code and Codex usage rows.
661
498
  *
662
499
  * Fire-and-forget from the caller's perspective: errors are caught internally
663
500
  * and surfaced only via `log`. The returned summary lets observers (e.g.
@@ -668,7 +505,6 @@ export async function collectAndSendTelemetry(opts) {
668
505
  const home = os.homedir();
669
506
  const claudeProjectsRoot = opts.claudeProjectsRoot ?? path.join(home, ".claude", "projects");
670
507
  const codexRoot = opts.codexRoot ?? path.join(home, ".codex");
671
- const grokRoot = opts.grokRoot ?? path.join(home, ".grok");
672
508
  const cursorPath = opts.cursorPath ?? path.join(home, ".hq", "telemetry-cursor.json");
673
509
  const menubarPath = opts.menubarPath ?? path.join(home, ".hq", "menubar.json");
674
510
  const log = opts.log ?? (() => { });
@@ -686,80 +522,73 @@ export async function collectAndSendTelemetry(opts) {
686
522
  ? repoCompanyMap.bySlug.get(opts.fallbackCompany)
687
523
  : undefined;
688
524
  // 1. Opt-in check (server-authoritative, with local fallback + self-heal).
689
- // Fleet agents skip this: collection is always on (company + unattributed).
690
525
  let enabled;
691
526
  let optInSource;
692
- if (opts.forceCollect) {
693
- enabled = true;
694
- optInSource = "agent-required";
695
- }
696
- else {
697
- try {
698
- const resp = await opts.client.getTelemetryOptIn();
699
- enabled = resp.enabled === true;
700
- optInSource = "server";
701
- // Self-heal a consent the user gave but that never reached the server.
702
- //
703
- // The installer writes the answer to `~/.hq/menubar.json` (always succeeds)
704
- // AND posts it to `/v1/usage/opt-in` — but that post fires before the
705
- // person entity exists, so it 404s and the attribute is never written.
706
- // Absence then reads as `false`, the emitter goes silent, and the person
707
- // shows as "not opted in" forever. Measured: 22 of 33 active Indigo members
708
- // had no attribute at all, against only 2 genuine opt-outs.
709
- //
710
- // `unset` (never answered) is the ONLY state we heal. An explicit server
711
- // `false` is a real opt-out and is left strictly alone. We also require a
712
- // local answer to actually exist — we never invent consent, we only replay
713
- // the answer the user already gave.
714
- //
715
- // And it must be THIS account's answer. `menubar.json` is per-MACHINE, so
716
- // when two people sign in under the same OS user it holds whoever answered
717
- // last; replaying that for the second account would opt in someone who
718
- // never consented. The replay therefore requires the cached record to name
719
- // the same `prs_*` the server says we are. A legacy record with no binding
720
- // cannot be proven to belong to this caller and is skipped — for those
721
- // machines the installer's own post-sign-in upload is the recovery path,
722
- // and it is correctly account-scoped because it runs right after that
723
- // person authenticates.
724
- if (resp.unset === true && opts.client.setTelemetryOptIn) {
725
- const record = await readLocalConsentRecord(menubarPath);
726
- const local = record.enabled;
727
- const boundToCaller = record.personUid !== undefined &&
728
- resp.personUid !== undefined &&
729
- record.personUid === resp.personUid;
730
- if (local !== undefined && !boundToCaller) {
731
- log("[telemetry] skipping consent re-assert: the locally cached answer is not bound to the signed-in account");
732
- }
733
- if (local !== undefined && boundToCaller) {
734
- try {
735
- // `onlyIfUnset` keeps the replay atomic. Reading `unset` and writing
736
- // are two requests, so without it another device could record a real
737
- // opt-out in between and this stale local answer would overwrite it.
738
- // The server tests "still unset?" as part of the write instead.
739
- const ack = await opts.client.setTelemetryOptIn(local, { onlyIfUnset: true });
740
- if (ack && ack.applied === false) {
741
- // Lost the race — a real answer was recorded first and it stands.
742
- // Keep the server's answer for this run rather than the local one.
743
- log("[telemetry] consent was recorded elsewhere before the re-assert landed; deferring to the server");
744
- }
745
- else {
746
- enabled = local;
747
- optInSource = "menubar-reasserted";
748
- log(`[telemetry] server had no recorded consent; re-asserted the local install-time answer (enabled=${local})`);
749
- }
527
+ try {
528
+ const resp = await opts.client.getTelemetryOptIn();
529
+ enabled = resp.enabled === true;
530
+ optInSource = "server";
531
+ // Self-heal a consent the user gave but that never reached the server.
532
+ //
533
+ // The installer writes the answer to `~/.hq/menubar.json` (always succeeds)
534
+ // AND posts it to `/v1/usage/opt-in` — but that post fires before the
535
+ // person entity exists, so it 404s and the attribute is never written.
536
+ // Absence then reads as `false`, the emitter goes silent, and the person
537
+ // shows as "not opted in" forever. Measured: 22 of 33 active Indigo members
538
+ // had no attribute at all, against only 2 genuine opt-outs.
539
+ //
540
+ // `unset` (never answered) is the ONLY state we heal. An explicit server
541
+ // `false` is a real opt-out and is left strictly alone. We also require a
542
+ // local answer to actually exist — we never invent consent, we only replay
543
+ // the answer the user already gave.
544
+ //
545
+ // And it must be THIS account's answer. `menubar.json` is per-MACHINE, so
546
+ // when two people sign in under the same OS user it holds whoever answered
547
+ // last; replaying that for the second account would opt in someone who
548
+ // never consented. The replay therefore requires the cached record to name
549
+ // the same `prs_*` the server says we are. A legacy record with no binding
550
+ // cannot be proven to belong to this caller and is skipped — for those
551
+ // machines the installer's own post-sign-in upload is the recovery path,
552
+ // and it is correctly account-scoped because it runs right after that
553
+ // person authenticates.
554
+ if (resp.unset === true && opts.client.setTelemetryOptIn) {
555
+ const record = await readLocalConsentRecord(menubarPath);
556
+ const local = record.enabled;
557
+ const boundToCaller = record.personUid !== undefined &&
558
+ resp.personUid !== undefined &&
559
+ record.personUid === resp.personUid;
560
+ if (local !== undefined && !boundToCaller) {
561
+ log("[telemetry] skipping consent re-assert: the locally cached answer is not bound to the signed-in account");
562
+ }
563
+ if (local !== undefined && boundToCaller) {
564
+ try {
565
+ // `onlyIfUnset` keeps the replay atomic. Reading `unset` and writing
566
+ // are two requests, so without it another device could record a real
567
+ // opt-out in between and this stale local answer would overwrite it.
568
+ // The server tests "still unset?" as part of the write instead.
569
+ const ack = await opts.client.setTelemetryOptIn(local, { onlyIfUnset: true });
570
+ if (ack && ack.applied === false) {
571
+ // Lost the race — a real answer was recorded first and it stands.
572
+ // Keep the server's answer for this run rather than the local one.
573
+ log("[telemetry] consent was recorded elsewhere before the re-assert landed; deferring to the server");
750
574
  }
751
- catch (err) {
752
- // Non-fatal: fall through on the server's answer. Next run retries.
753
- log(`[telemetry] failed to re-assert local opt-in (${err.message ?? err})`);
575
+ else {
576
+ enabled = local;
577
+ optInSource = "menubar-reasserted";
578
+ log(`[telemetry] server had no recorded consent; re-asserted the local install-time answer (enabled=${local})`);
754
579
  }
755
580
  }
581
+ catch (err) {
582
+ // Non-fatal: fall through on the server's answer. Next run retries.
583
+ log(`[telemetry] failed to re-assert local opt-in (${err.message ?? err})`);
584
+ }
756
585
  }
757
586
  }
758
- catch (err) {
759
- log(`[telemetry] opt-in check failed (${err.message ?? err}) — falling back to local menubar.json`);
760
- enabled = await readLocalTelemetryEnabled(menubarPath);
761
- optInSource = "menubar-fallback";
762
- }
587
+ }
588
+ catch (err) {
589
+ log(`[telemetry] opt-in check failed (${err.message ?? err}) — falling back to local menubar.json`);
590
+ enabled = await readLocalTelemetryEnabled(menubarPath);
591
+ optInSource = "menubar-fallback";
763
592
  }
764
593
  // Consent split (company-scope mandatory capture): a FALSE/unset personal
765
594
  // opt-in silences PERSONAL/unattributed rows only. Rows produced inside a
@@ -779,16 +608,14 @@ export async function collectAndSendTelemetry(opts) {
779
608
  const loadedFiles = { ...cursor.files };
780
609
  const rotationResets = {};
781
610
  const newlyCommitted = {};
782
- const [claudeFilesUnsorted, codexFiles, grokFiles] = await Promise.all([
611
+ const [claudeFilesUnsorted, codexFiles] = await Promise.all([
783
612
  listJsonlFiles(claudeProjectsRoot),
784
613
  listCodexRolloutFiles(codexRoot),
785
- listGrokUpdateFiles(grokRoot),
786
614
  ]);
787
615
  const claudeFiles = await sortFilesFreshestFirst(claudeFilesUnsorted);
788
- // Newest runtimes first so a bounded first pass reports current Grok/Codex
789
- // use before replaying an older Claude backlog.
616
+ // Codex goes first so a fresh Linux install cannot spend its entire bounded
617
+ // pass replaying an older Claude backlog before reporting current Codex use.
790
618
  const files = [
791
- ...grokFiles.map((filePath) => ({ filePath, kind: "grok" })),
792
619
  ...codexFiles.map((filePath) => ({ filePath, kind: "codex" })),
793
620
  ...claudeFiles.map((filePath) => ({ filePath, kind: "claude" })),
794
621
  ];
@@ -808,7 +635,7 @@ export async function collectAndSendTelemetry(opts) {
808
635
  let batchesSent = 0;
809
636
  let uploadFailed = false;
810
637
  let batchLimitReached = false;
811
- const scannedBytes = { claude: 0, codex: 0, grok: 0 };
638
+ const scannedBytes = { claude: 0, codex: 0 };
812
639
  const commitSources = (sources) => {
813
640
  const maxPerFile = new Map();
814
641
  for (const source of sources) {
@@ -877,21 +704,16 @@ export async function collectAndSendTelemetry(opts) {
877
704
  const currentMtime = Math.floor(stat.mtimeMs / 1000);
878
705
  const stored = cursor.files[filePath] ?? { offset: 0, mtime: 0 };
879
706
  let offset = stored.offset;
880
- const needsScanContext = kind === "codex" || kind === "grok";
881
- let scanContext = needsScanContext
882
- ? cloneCodexContext(stored.context ?? {})
883
- : undefined;
707
+ let codexContext = kind === "codex" ? cloneCodexContext(stored.context ?? {}) : undefined;
884
708
  const rotated = currentSize < offset || (stored.mtime > 0 && currentMtime < stored.mtime);
885
709
  if (rotated) {
886
710
  offset = 0;
887
- if (needsScanContext)
888
- scanContext = {};
711
+ if (kind === "codex")
712
+ codexContext = {};
889
713
  rotationResets[filePath] = { offset: 0, mtime: currentMtime };
890
714
  }
891
715
  if (offset >= currentSize && !rotated)
892
716
  continue;
893
- // Grok session meta is loaded once per updates.jsonl (not persisted in cursor).
894
- const grokSessionMeta = kind === "grok" ? await loadGrokSessionMeta(filePath) : undefined;
895
717
  let content;
896
718
  let bytesRead = 0;
897
719
  let readBuffer = Buffer.alloc(0);
@@ -912,19 +734,19 @@ export async function collectAndSendTelemetry(opts) {
912
734
  }
913
735
  if (bytesRead === 0)
914
736
  continue;
915
- // Resume a previously discarded oversized Codex/Grok row using raw byte
737
+ // Resume a previously discarded oversized Codex row using raw byte
916
738
  // positions. Decoding from the middle of a multibyte UTF-8 codepoint would
917
739
  // otherwise skew byte offsets and could skip or replay later rows.
918
- if (scanContext?.discarding_partial_line) {
740
+ if (codexContext?.discarding_partial_line) {
919
741
  const newlineIndex = readBuffer.subarray(0, bytesRead).indexOf(0x0a);
920
742
  const discardedBytes = newlineIndex >= 0 ? newlineIndex + 1 : bytesRead;
921
- scanContext.discarding_partial_line = newlineIndex < 0;
743
+ codexContext.discarding_partial_line = newlineIndex < 0;
922
744
  scannedBytes[kind] += discardedBytes;
923
745
  batchSources.push({
924
746
  filePath,
925
747
  endOffset: offset + discardedBytes,
926
748
  mtime: currentMtime,
927
- context: cloneCodexContext(scanContext),
749
+ context: cloneCodexContext(codexContext),
928
750
  });
929
751
  if (newlineIndex < 0)
930
752
  continue;
@@ -953,17 +775,17 @@ export async function collectAndSendTelemetry(opts) {
953
775
  : lineEndOffsets[i];
954
776
  const lineBytes = endOffset - startOffset;
955
777
  if (!hasNewline && !reachedPhysicalEof) {
956
- const discarding = scanContext?.discarding_partial_line === true;
778
+ const discarding = codexContext?.discarding_partial_line === true;
957
779
  if (discarding || lineBytes >= MAX_PARTIAL_LINE_BYTES) {
958
- if (scanContext)
959
- scanContext.discarding_partial_line = true;
780
+ if (codexContext)
781
+ codexContext.discarding_partial_line = true;
960
782
  scannedBytes[kind] += lineBytes;
961
783
  batchSources.push({
962
784
  filePath,
963
785
  endOffset,
964
786
  mtime: currentMtime,
965
- ...(scanContext
966
- ? { context: cloneCodexContext(scanContext) }
787
+ ...(codexContext
788
+ ? { context: cloneCodexContext(codexContext) }
967
789
  : {}),
968
790
  });
969
791
  }
@@ -974,12 +796,12 @@ export async function collectAndSendTelemetry(opts) {
974
796
  filePath,
975
797
  endOffset,
976
798
  mtime: currentMtime,
977
- ...(scanContext
978
- ? { context: cloneCodexContext(scanContext) }
799
+ ...(codexContext
800
+ ? { context: cloneCodexContext(codexContext) }
979
801
  : {}),
980
802
  });
981
- if (scanContext?.discarding_partial_line) {
982
- scanContext.discarding_partial_line = false;
803
+ if (codexContext?.discarding_partial_line) {
804
+ codexContext.discarding_partial_line = false;
983
805
  batchSources.push(source());
984
806
  continue;
985
807
  }
@@ -996,83 +818,65 @@ export async function collectAndSendTelemetry(opts) {
996
818
  batchSources.push(source());
997
819
  continue;
998
820
  }
999
- let adaptedRows;
1000
- if (kind === "codex") {
1001
- const adapted = codexUsageRow(parsed, scanContext, filePath, startOffset, endOffset);
1002
- adaptedRows = adapted ? [adapted] : [];
1003
- }
1004
- else if (kind === "grok") {
1005
- adaptedRows = grokUsageRows(parsed, grokSessionMeta ?? {});
821
+ const sourceRow = kind === "codex"
822
+ ? codexUsageRow(parsed, codexContext, filePath, startOffset, endOffset)
823
+ : parsed;
824
+ const rowCwd = sourceRow && typeof sourceRow === "object" && !Array.isArray(sourceRow)
825
+ ? sourceRow.cwd
826
+ : undefined;
827
+ const companyUid = resolveCompanyForCwd(typeof rowCwd === "string" ? rowCwd : undefined, repoCompanyMap) ?? fallbackCompanyUid;
828
+ // Company-scope-only mode: a row that resolves to NO company is personal
829
+ // work by an opted-out person — skipped, but still cursor-advanced (via
830
+ // batchSources) exactly like a sanitize-rejected row, so it is not
831
+ // re-read every run.
832
+ if (companyScopeOnly && !companyUid) {
833
+ batchSources.push(source());
834
+ continue;
1006
835
  }
1007
- else {
1008
- adaptedRows = [parsed];
836
+ const sanitized = sanitizeRow(sourceRow, companyUid);
837
+ if (!sanitized) {
838
+ batchSources.push(source());
839
+ continue;
1009
840
  }
1010
- if (adaptedRows.length === 0) {
841
+ const maxRowBytes = MAX_BATCH_BYTES - ENVELOPE_BYTES;
842
+ const wasOversized = jsonBytes(sanitized) > maxRowBytes;
843
+ const bounded = boundRowForPost(sanitized, maxRowBytes);
844
+ if (!bounded) {
845
+ log("[telemetry] oversized row dropped before send (" +
846
+ filePath +
847
+ ":" +
848
+ (i + 1) +
849
+ ")");
1011
850
  batchSources.push(source());
1012
851
  continue;
1013
852
  }
1014
- let emittedFromLine = 0;
1015
- for (const sourceRow of adaptedRows) {
1016
- const rowCwd = sourceRow && typeof sourceRow === "object" && !Array.isArray(sourceRow)
1017
- ? sourceRow.cwd
1018
- : undefined;
1019
- const companyUid = resolveCompanyForCwd(typeof rowCwd === "string" ? rowCwd : undefined, repoCompanyMap) ?? fallbackCompanyUid;
1020
- // Company-scope-only mode: a row that resolves to NO company is personal
1021
- // work by an opted-out person — skipped, but still cursor-advanced (via
1022
- // batchSources) exactly like a sanitize-rejected row, so it is not
1023
- // re-read every run.
1024
- if (companyScopeOnly && !companyUid) {
1025
- continue;
1026
- }
1027
- const sanitized = sanitizeRow(sourceRow, companyUid);
1028
- if (!sanitized) {
1029
- continue;
1030
- }
1031
- const maxRowBytes = MAX_BATCH_BYTES - ENVELOPE_BYTES;
1032
- const wasOversized = jsonBytes(sanitized) > maxRowBytes;
1033
- const bounded = boundRowForPost(sanitized, maxRowBytes);
1034
- if (!bounded) {
1035
- log("[telemetry] oversized row dropped before send (" +
1036
- filePath +
1037
- ":" +
1038
- (i + 1) +
1039
- ")");
1040
- continue;
1041
- }
1042
- if (wasOversized) {
1043
- log("[telemetry] oversized row truncated before send (" +
1044
- filePath +
1045
- ":" +
1046
- (i + 1) +
1047
- ")");
1048
- }
1049
- const rowJsonBytes = jsonBytes(bounded);
1050
- const addCost = rowJsonBytes + (batchEvents.length > 0 ? 1 : 0);
1051
- if (batchEvents.length > 0 &&
1052
- (batchEvents.length >= MAX_BATCH_EVENTS ||
1053
- batchBytes + addCost > MAX_BATCH_BYTES)) {
1054
- if (!(await flush())) {
1055
- uploadFailed = true;
1056
- break filesLoop;
1057
- }
1058
- if (batchesSent >= maxBatchesPerRun) {
1059
- batchLimitReached = true;
1060
- break filesLoop;
1061
- }
1062
- batchBytes = ENVELOPE_BYTES + rowJsonBytes;
853
+ if (wasOversized) {
854
+ log("[telemetry] oversized row truncated before send (" +
855
+ filePath +
856
+ ":" +
857
+ (i + 1) +
858
+ ")");
859
+ }
860
+ const rowJsonBytes = jsonBytes(bounded);
861
+ const addCost = rowJsonBytes + (batchEvents.length > 0 ? 1 : 0);
862
+ if (batchEvents.length > 0 &&
863
+ (batchEvents.length >= MAX_BATCH_EVENTS ||
864
+ batchBytes + addCost > MAX_BATCH_BYTES)) {
865
+ if (!(await flush())) {
866
+ uploadFailed = true;
867
+ break filesLoop;
1063
868
  }
1064
- else {
1065
- batchBytes += addCost;
869
+ if (batchesSent >= maxBatchesPerRun) {
870
+ batchLimitReached = true;
871
+ break filesLoop;
1066
872
  }
1067
- batchEvents.push(bounded);
1068
- // Same endOffset for every model row from this JSONL line; commitSources
1069
- // keeps the max, so the cursor advances once per line.
1070
- batchSources.push(source());
1071
- emittedFromLine++;
873
+ batchBytes = ENVELOPE_BYTES + rowJsonBytes;
1072
874
  }
1073
- if (emittedFromLine === 0) {
1074
- batchSources.push(source());
875
+ else {
876
+ batchBytes += addCost;
1075
877
  }
878
+ batchEvents.push(bounded);
879
+ batchSources.push(source());
1076
880
  }
1077
881
  }
1078
882
  if (!uploadFailed && !batchLimitReached) {