@indigoai-us/hq-cloud 6.15.31 → 6.15.32
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/bandwidth.d.ts +128 -0
- package/dist/bandwidth.d.ts.map +1 -0
- package/dist/bandwidth.js +254 -0
- package/dist/bandwidth.js.map +1 -0
- package/dist/bandwidth.test.d.ts +2 -0
- package/dist/bandwidth.test.d.ts.map +1 -0
- package/dist/bandwidth.test.js +243 -0
- package/dist/bandwidth.test.js.map +1 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +13 -24
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.js +71 -0
- package/dist/bin/sync-runner-company.test.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +10 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +181 -16
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.d.ts +16 -1
- package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-routes.js +40 -2
- package/dist/bin/sync-runner-watch-routes.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.test.js +53 -1
- package/dist/bin/sync-runner-watch-routes.test.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +15 -3
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +66 -13
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +433 -13
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/conflict.d.ts +25 -2
- package/dist/cli/conflict.d.ts.map +1 -1
- package/dist/cli/conflict.js +29 -4
- package/dist/cli/conflict.js.map +1 -1
- package/dist/cli/share.d.ts +40 -4
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +478 -86
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +845 -150
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync-scope.test.js +147 -18
- package/dist/cli/sync-scope.test.js.map +1 -1
- package/dist/cli/sync.d.ts +20 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +261 -29
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +519 -90
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/journal-delta-writes.test.d.ts +2 -0
- package/dist/journal-delta-writes.test.d.ts.map +1 -0
- package/dist/journal-delta-writes.test.js +140 -0
- package/dist/journal-delta-writes.test.js.map +1 -0
- package/dist/journal.d.ts +1 -0
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +155 -25
- package/dist/journal.js.map +1 -1
- package/dist/journal.test.js +29 -11
- package/dist/journal.test.js.map +1 -1
- package/dist/object-io.d.ts +34 -13
- package/dist/object-io.d.ts.map +1 -1
- package/dist/object-io.js +154 -29
- package/dist/object-io.js.map +1 -1
- package/dist/operation-lock.d.ts +12 -0
- package/dist/operation-lock.d.ts.map +1 -1
- package/dist/operation-lock.js +103 -21
- package/dist/operation-lock.js.map +1 -1
- package/dist/operation-lock.test.js +90 -0
- package/dist/operation-lock.test.js.map +1 -1
- package/dist/outcome-telemetry.d.ts +7 -1
- package/dist/outcome-telemetry.d.ts.map +1 -1
- package/dist/outcome-telemetry.js +15 -9
- package/dist/outcome-telemetry.js.map +1 -1
- package/dist/outcome-telemetry.test.js +15 -0
- package/dist/outcome-telemetry.test.js.map +1 -1
- package/dist/outposts/client.d.ts +2 -2
- package/dist/outposts/client.d.ts.map +1 -1
- package/dist/outposts/client.js +4 -3
- package/dist/outposts/client.js.map +1 -1
- package/dist/outposts/command.d.ts.map +1 -1
- package/dist/outposts/command.js +9 -6
- package/dist/outposts/command.js.map +1 -1
- package/dist/outposts/command.test.js +59 -2
- package/dist/outposts/command.test.js.map +1 -1
- package/dist/outposts/remote-command.d.ts +8 -8
- package/dist/outposts/remote-command.d.ts.map +1 -1
- package/dist/outposts/remote-command.js +10 -8
- package/dist/outposts/remote-command.js.map +1 -1
- package/dist/prefix-coalesce.d.ts +14 -0
- package/dist/prefix-coalesce.d.ts.map +1 -1
- package/dist/prefix-coalesce.js +29 -0
- package/dist/prefix-coalesce.js.map +1 -1
- package/dist/prefix-coalesce.test.js +22 -1
- package/dist/prefix-coalesce.test.js.map +1 -1
- package/dist/skill-telemetry.d.ts +6 -1
- package/dist/skill-telemetry.d.ts.map +1 -1
- package/dist/skill-telemetry.js +14 -8
- package/dist/skill-telemetry.js.map +1 -1
- package/dist/skill-telemetry.test.js +43 -0
- package/dist/skill-telemetry.test.js.map +1 -1
- package/dist/sts-credential-refresh.test.js +87 -2
- 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 +20 -1
- package/dist/sync/state-store.js.map +1 -1
- package/dist/sync/state-store.test.js +64 -0
- package/dist/sync/state-store.test.js.map +1 -1
- package/dist/sync-core.d.ts.map +1 -1
- package/dist/sync-core.js +7 -2
- package/dist/sync-core.js.map +1 -1
- package/dist/sync-core.test.js +36 -0
- package/dist/sync-core.test.js.map +1 -1
- package/dist/telemetry.d.ts +30 -6
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +331 -135
- package/dist/telemetry.js.map +1 -1
- package/dist/telemetry.test.js +403 -0
- package/dist/telemetry.test.js.map +1 -1
- package/dist/types.d.ts +26 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/watcher.d.ts +76 -0
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +105 -3
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +165 -1
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -1
package/dist/telemetry.js
CHANGED
|
@@ -7,10 +7,12 @@
|
|
|
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
|
-
* rollouts, then diffs each file against the
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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`.
|
|
14
16
|
*
|
|
15
17
|
* Trust model: the caller's `personUid` is resolved on the server from the
|
|
16
18
|
* Cognito JWT — never from the body. `sanitizeRow` strips prompt bodies,
|
|
@@ -430,6 +432,167 @@ async function listCodexRolloutFiles(codexRoot) {
|
|
|
430
432
|
]);
|
|
431
433
|
return sortFilesFreshestFirst([...live, ...archived].filter((file) => path.basename(file).startsWith("rollout-")));
|
|
432
434
|
}
|
|
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
|
+
}
|
|
433
596
|
// ── Batching primitives ───────────────────────────────────────────────────────
|
|
434
597
|
const MAX_BATCH_EVENTS = 100;
|
|
435
598
|
const MAX_BATCH_BYTES = 240 * 1024;
|
|
@@ -494,7 +657,7 @@ function boundRowForPost(row, maxRowBytes) {
|
|
|
494
657
|
}
|
|
495
658
|
// ── Main entry point ──────────────────────────────────────────────────────────
|
|
496
659
|
/**
|
|
497
|
-
* Scan, sanitize, and POST new
|
|
660
|
+
* Scan, sanitize, and POST new Grok, Codex, and Claude Code usage rows.
|
|
498
661
|
*
|
|
499
662
|
* Fire-and-forget from the caller's perspective: errors are caught internally
|
|
500
663
|
* and surfaced only via `log`. The returned summary lets observers (e.g.
|
|
@@ -505,6 +668,7 @@ export async function collectAndSendTelemetry(opts) {
|
|
|
505
668
|
const home = os.homedir();
|
|
506
669
|
const claudeProjectsRoot = opts.claudeProjectsRoot ?? path.join(home, ".claude", "projects");
|
|
507
670
|
const codexRoot = opts.codexRoot ?? path.join(home, ".codex");
|
|
671
|
+
const grokRoot = opts.grokRoot ?? path.join(home, ".grok");
|
|
508
672
|
const cursorPath = opts.cursorPath ?? path.join(home, ".hq", "telemetry-cursor.json");
|
|
509
673
|
const menubarPath = opts.menubarPath ?? path.join(home, ".hq", "menubar.json");
|
|
510
674
|
const log = opts.log ?? (() => { });
|
|
@@ -522,73 +686,80 @@ export async function collectAndSendTelemetry(opts) {
|
|
|
522
686
|
? repoCompanyMap.bySlug.get(opts.fallbackCompany)
|
|
523
687
|
: undefined;
|
|
524
688
|
// 1. Opt-in check (server-authoritative, with local fallback + self-heal).
|
|
689
|
+
// Fleet agents skip this: collection is always on (company + unattributed).
|
|
525
690
|
let enabled;
|
|
526
691
|
let optInSource;
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
//
|
|
572
|
-
//
|
|
573
|
-
|
|
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
|
+
}
|
|
574
750
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
log(`[telemetry] server had no recorded consent; re-asserted the local install-time answer (enabled=${local})`);
|
|
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})`);
|
|
579
754
|
}
|
|
580
755
|
}
|
|
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
|
-
}
|
|
585
756
|
}
|
|
586
757
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
+
}
|
|
592
763
|
}
|
|
593
764
|
// Consent split (company-scope mandatory capture): a FALSE/unset personal
|
|
594
765
|
// opt-in silences PERSONAL/unattributed rows only. Rows produced inside a
|
|
@@ -608,14 +779,16 @@ export async function collectAndSendTelemetry(opts) {
|
|
|
608
779
|
const loadedFiles = { ...cursor.files };
|
|
609
780
|
const rotationResets = {};
|
|
610
781
|
const newlyCommitted = {};
|
|
611
|
-
const [claudeFilesUnsorted, codexFiles] = await Promise.all([
|
|
782
|
+
const [claudeFilesUnsorted, codexFiles, grokFiles] = await Promise.all([
|
|
612
783
|
listJsonlFiles(claudeProjectsRoot),
|
|
613
784
|
listCodexRolloutFiles(codexRoot),
|
|
785
|
+
listGrokUpdateFiles(grokRoot),
|
|
614
786
|
]);
|
|
615
787
|
const claudeFiles = await sortFilesFreshestFirst(claudeFilesUnsorted);
|
|
616
|
-
//
|
|
617
|
-
//
|
|
788
|
+
// Newest runtimes first so a bounded first pass reports current Grok/Codex
|
|
789
|
+
// use before replaying an older Claude backlog.
|
|
618
790
|
const files = [
|
|
791
|
+
...grokFiles.map((filePath) => ({ filePath, kind: "grok" })),
|
|
619
792
|
...codexFiles.map((filePath) => ({ filePath, kind: "codex" })),
|
|
620
793
|
...claudeFiles.map((filePath) => ({ filePath, kind: "claude" })),
|
|
621
794
|
];
|
|
@@ -635,7 +808,7 @@ export async function collectAndSendTelemetry(opts) {
|
|
|
635
808
|
let batchesSent = 0;
|
|
636
809
|
let uploadFailed = false;
|
|
637
810
|
let batchLimitReached = false;
|
|
638
|
-
const scannedBytes = { claude: 0, codex: 0 };
|
|
811
|
+
const scannedBytes = { claude: 0, codex: 0, grok: 0 };
|
|
639
812
|
const commitSources = (sources) => {
|
|
640
813
|
const maxPerFile = new Map();
|
|
641
814
|
for (const source of sources) {
|
|
@@ -704,16 +877,21 @@ export async function collectAndSendTelemetry(opts) {
|
|
|
704
877
|
const currentMtime = Math.floor(stat.mtimeMs / 1000);
|
|
705
878
|
const stored = cursor.files[filePath] ?? { offset: 0, mtime: 0 };
|
|
706
879
|
let offset = stored.offset;
|
|
707
|
-
|
|
880
|
+
const needsScanContext = kind === "codex" || kind === "grok";
|
|
881
|
+
let scanContext = needsScanContext
|
|
882
|
+
? cloneCodexContext(stored.context ?? {})
|
|
883
|
+
: undefined;
|
|
708
884
|
const rotated = currentSize < offset || (stored.mtime > 0 && currentMtime < stored.mtime);
|
|
709
885
|
if (rotated) {
|
|
710
886
|
offset = 0;
|
|
711
|
-
if (
|
|
712
|
-
|
|
887
|
+
if (needsScanContext)
|
|
888
|
+
scanContext = {};
|
|
713
889
|
rotationResets[filePath] = { offset: 0, mtime: currentMtime };
|
|
714
890
|
}
|
|
715
891
|
if (offset >= currentSize && !rotated)
|
|
716
892
|
continue;
|
|
893
|
+
// Grok session meta is loaded once per updates.jsonl (not persisted in cursor).
|
|
894
|
+
const grokSessionMeta = kind === "grok" ? await loadGrokSessionMeta(filePath) : undefined;
|
|
717
895
|
let content;
|
|
718
896
|
let bytesRead = 0;
|
|
719
897
|
let readBuffer = Buffer.alloc(0);
|
|
@@ -734,19 +912,19 @@ export async function collectAndSendTelemetry(opts) {
|
|
|
734
912
|
}
|
|
735
913
|
if (bytesRead === 0)
|
|
736
914
|
continue;
|
|
737
|
-
// Resume a previously discarded oversized Codex row using raw byte
|
|
915
|
+
// Resume a previously discarded oversized Codex/Grok row using raw byte
|
|
738
916
|
// positions. Decoding from the middle of a multibyte UTF-8 codepoint would
|
|
739
917
|
// otherwise skew byte offsets and could skip or replay later rows.
|
|
740
|
-
if (
|
|
918
|
+
if (scanContext?.discarding_partial_line) {
|
|
741
919
|
const newlineIndex = readBuffer.subarray(0, bytesRead).indexOf(0x0a);
|
|
742
920
|
const discardedBytes = newlineIndex >= 0 ? newlineIndex + 1 : bytesRead;
|
|
743
|
-
|
|
921
|
+
scanContext.discarding_partial_line = newlineIndex < 0;
|
|
744
922
|
scannedBytes[kind] += discardedBytes;
|
|
745
923
|
batchSources.push({
|
|
746
924
|
filePath,
|
|
747
925
|
endOffset: offset + discardedBytes,
|
|
748
926
|
mtime: currentMtime,
|
|
749
|
-
context: cloneCodexContext(
|
|
927
|
+
context: cloneCodexContext(scanContext),
|
|
750
928
|
});
|
|
751
929
|
if (newlineIndex < 0)
|
|
752
930
|
continue;
|
|
@@ -775,17 +953,17 @@ export async function collectAndSendTelemetry(opts) {
|
|
|
775
953
|
: lineEndOffsets[i];
|
|
776
954
|
const lineBytes = endOffset - startOffset;
|
|
777
955
|
if (!hasNewline && !reachedPhysicalEof) {
|
|
778
|
-
const discarding =
|
|
956
|
+
const discarding = scanContext?.discarding_partial_line === true;
|
|
779
957
|
if (discarding || lineBytes >= MAX_PARTIAL_LINE_BYTES) {
|
|
780
|
-
if (
|
|
781
|
-
|
|
958
|
+
if (scanContext)
|
|
959
|
+
scanContext.discarding_partial_line = true;
|
|
782
960
|
scannedBytes[kind] += lineBytes;
|
|
783
961
|
batchSources.push({
|
|
784
962
|
filePath,
|
|
785
963
|
endOffset,
|
|
786
964
|
mtime: currentMtime,
|
|
787
|
-
...(
|
|
788
|
-
? { context: cloneCodexContext(
|
|
965
|
+
...(scanContext
|
|
966
|
+
? { context: cloneCodexContext(scanContext) }
|
|
789
967
|
: {}),
|
|
790
968
|
});
|
|
791
969
|
}
|
|
@@ -796,12 +974,12 @@ export async function collectAndSendTelemetry(opts) {
|
|
|
796
974
|
filePath,
|
|
797
975
|
endOffset,
|
|
798
976
|
mtime: currentMtime,
|
|
799
|
-
...(
|
|
800
|
-
? { context: cloneCodexContext(
|
|
977
|
+
...(scanContext
|
|
978
|
+
? { context: cloneCodexContext(scanContext) }
|
|
801
979
|
: {}),
|
|
802
980
|
});
|
|
803
|
-
if (
|
|
804
|
-
|
|
981
|
+
if (scanContext?.discarding_partial_line) {
|
|
982
|
+
scanContext.discarding_partial_line = false;
|
|
805
983
|
batchSources.push(source());
|
|
806
984
|
continue;
|
|
807
985
|
}
|
|
@@ -818,65 +996,83 @@ export async function collectAndSendTelemetry(opts) {
|
|
|
818
996
|
batchSources.push(source());
|
|
819
997
|
continue;
|
|
820
998
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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;
|
|
999
|
+
let adaptedRows;
|
|
1000
|
+
if (kind === "codex") {
|
|
1001
|
+
const adapted = codexUsageRow(parsed, scanContext, filePath, startOffset, endOffset);
|
|
1002
|
+
adaptedRows = adapted ? [adapted] : [];
|
|
835
1003
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
batchSources.push(source());
|
|
839
|
-
continue;
|
|
1004
|
+
else if (kind === "grok") {
|
|
1005
|
+
adaptedRows = grokUsageRows(parsed, grokSessionMeta ?? {});
|
|
840
1006
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
if (
|
|
845
|
-
log("[telemetry] oversized row dropped before send (" +
|
|
846
|
-
filePath +
|
|
847
|
-
":" +
|
|
848
|
-
(i + 1) +
|
|
849
|
-
")");
|
|
1007
|
+
else {
|
|
1008
|
+
adaptedRows = [parsed];
|
|
1009
|
+
}
|
|
1010
|
+
if (adaptedRows.length === 0) {
|
|
850
1011
|
batchSources.push(source());
|
|
851
1012
|
continue;
|
|
852
1013
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
(
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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;
|
|
868
1030
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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;
|
|
872
1041
|
}
|
|
873
|
-
|
|
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;
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
batchBytes += addCost;
|
|
1066
|
+
}
|
|
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++;
|
|
874
1072
|
}
|
|
875
|
-
|
|
876
|
-
|
|
1073
|
+
if (emittedFromLine === 0) {
|
|
1074
|
+
batchSources.push(source());
|
|
877
1075
|
}
|
|
878
|
-
batchEvents.push(bounded);
|
|
879
|
-
batchSources.push(source());
|
|
880
1076
|
}
|
|
881
1077
|
}
|
|
882
1078
|
if (!uploadFailed && !batchLimitReached) {
|