@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.
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +24 -13
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.js +0 -71
- package/dist/bin/sync-runner-company.test.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +0 -10
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +13 -170
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.d.ts +1 -16
- package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-routes.js +2 -40
- package/dist/bin/sync-runner-watch-routes.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.test.js +1 -53
- package/dist/bin/sync-runner-watch-routes.test.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +3 -15
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +13 -66
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +13 -433
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/conflict.d.ts +2 -25
- package/dist/cli/conflict.d.ts.map +1 -1
- package/dist/cli/conflict.js +4 -29
- package/dist/cli/conflict.js.map +1 -1
- package/dist/cli/share.d.ts +4 -40
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +86 -478
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +150 -845
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync-scope.test.js +18 -147
- package/dist/cli/sync-scope.test.js.map +1 -1
- package/dist/cli/sync.d.ts +0 -20
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +29 -261
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +90 -519
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/journal.d.ts +0 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +25 -155
- package/dist/journal.js.map +1 -1
- package/dist/journal.test.js +11 -29
- package/dist/journal.test.js.map +1 -1
- package/dist/object-io.d.ts +13 -34
- package/dist/object-io.d.ts.map +1 -1
- package/dist/object-io.js +29 -154
- package/dist/object-io.js.map +1 -1
- package/dist/outcome-telemetry.d.ts +1 -7
- package/dist/outcome-telemetry.d.ts.map +1 -1
- package/dist/outcome-telemetry.js +9 -15
- package/dist/outcome-telemetry.js.map +1 -1
- package/dist/outcome-telemetry.test.js +0 -15
- package/dist/outcome-telemetry.test.js.map +1 -1
- package/dist/prefix-coalesce.d.ts +0 -14
- package/dist/prefix-coalesce.d.ts.map +1 -1
- package/dist/prefix-coalesce.js +0 -29
- package/dist/prefix-coalesce.js.map +1 -1
- package/dist/prefix-coalesce.test.js +1 -22
- package/dist/prefix-coalesce.test.js.map +1 -1
- package/dist/skill-telemetry.d.ts +1 -6
- package/dist/skill-telemetry.d.ts.map +1 -1
- package/dist/skill-telemetry.js +8 -14
- package/dist/skill-telemetry.js.map +1 -1
- package/dist/skill-telemetry.test.js +0 -43
- package/dist/skill-telemetry.test.js.map +1 -1
- package/dist/sts-credential-refresh.test.js +2 -87
- package/dist/sts-credential-refresh.test.js.map +1 -1
- package/dist/sync/state-store.d.ts.map +1 -1
- package/dist/sync/state-store.js +1 -20
- package/dist/sync/state-store.js.map +1 -1
- package/dist/sync/state-store.test.js +0 -64
- package/dist/sync/state-store.test.js.map +1 -1
- package/dist/sync-core.d.ts.map +1 -1
- package/dist/sync-core.js +2 -7
- package/dist/sync-core.js.map +1 -1
- package/dist/sync-core.test.js +0 -36
- package/dist/sync-core.test.js.map +1 -1
- package/dist/telemetry.d.ts +6 -30
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +135 -331
- package/dist/telemetry.js.map +1 -1
- package/dist/telemetry.test.js +0 -403
- package/dist/telemetry.test.js.map +1 -1
- package/dist/types.d.ts +3 -26
- package/dist/types.d.ts.map +1 -1
- package/dist/watcher.d.ts +0 -76
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +3 -105
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +1 -165
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -1
- package/dist/bandwidth.d.ts +0 -128
- package/dist/bandwidth.d.ts.map +0 -1
- package/dist/bandwidth.js +0 -254
- package/dist/bandwidth.js.map +0 -1
- package/dist/bandwidth.test.d.ts +0 -2
- package/dist/bandwidth.test.d.ts.map +0 -1
- package/dist/bandwidth.test.js +0 -243
- package/dist/bandwidth.test.js.map +0 -1
- package/dist/journal-delta-writes.test.d.ts +0 -2
- package/dist/journal-delta-writes.test.d.ts.map +0 -1
- package/dist/journal-delta-writes.test.js +0 -140
- 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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
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
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
|
|
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
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
|
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
|
-
//
|
|
789
|
-
//
|
|
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
|
|
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
|
-
|
|
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 (
|
|
888
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
778
|
+
const discarding = codexContext?.discarding_partial_line === true;
|
|
957
779
|
if (discarding || lineBytes >= MAX_PARTIAL_LINE_BYTES) {
|
|
958
|
-
if (
|
|
959
|
-
|
|
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
|
-
...(
|
|
966
|
-
? { context: cloneCodexContext(
|
|
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
|
-
...(
|
|
978
|
-
? { context: cloneCodexContext(
|
|
799
|
+
...(codexContext
|
|
800
|
+
? { context: cloneCodexContext(codexContext) }
|
|
979
801
|
: {}),
|
|
980
802
|
});
|
|
981
|
-
if (
|
|
982
|
-
|
|
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
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
836
|
+
const sanitized = sanitizeRow(sourceRow, companyUid);
|
|
837
|
+
if (!sanitized) {
|
|
838
|
+
batchSources.push(source());
|
|
839
|
+
continue;
|
|
1009
840
|
}
|
|
1010
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
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
|
-
|
|
1065
|
-
|
|
869
|
+
if (batchesSent >= maxBatchesPerRun) {
|
|
870
|
+
batchLimitReached = true;
|
|
871
|
+
break filesLoop;
|
|
1066
872
|
}
|
|
1067
|
-
|
|
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
|
-
|
|
1074
|
-
|
|
875
|
+
else {
|
|
876
|
+
batchBytes += addCost;
|
|
1075
877
|
}
|
|
878
|
+
batchEvents.push(bounded);
|
|
879
|
+
batchSources.push(source());
|
|
1076
880
|
}
|
|
1077
881
|
}
|
|
1078
882
|
if (!uploadFailed && !batchLimitReached) {
|