@indigoai-us/hq-cloud 6.14.28 → 6.14.30
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 +4 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +3 -0
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-planning.d.ts +7 -6
- package/dist/bin/sync-runner-planning.d.ts.map +1 -1
- package/dist/bin/sync-runner-planning.js +1 -1
- package/dist/bin/sync-runner-planning.js.map +1 -1
- package/dist/bin/sync-runner-planning.test.js +23 -0
- package/dist/bin/sync-runner-planning.test.js.map +1 -1
- package/dist/bin/sync-runner-rollup.d.ts.map +1 -1
- package/dist/bin/sync-runner-rollup.js +3 -1
- package/dist/bin/sync-runner-rollup.js.map +1 -1
- package/dist/bin/sync-runner-rollup.test.d.ts +2 -0
- package/dist/bin/sync-runner-rollup.test.d.ts.map +1 -0
- package/dist/bin/sync-runner-rollup.test.js +24 -0
- package/dist/bin/sync-runner-rollup.test.js.map +1 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +15 -1
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +11 -7
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +51 -9
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +97 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +3 -1
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/reindex.d.ts.map +1 -1
- package/dist/cli/reindex.js +24 -13
- package/dist/cli/reindex.js.map +1 -1
- package/dist/cli/rescue-core.js +3 -1
- package/dist/cli/rescue-core.js.map +1 -1
- package/dist/cli/share.d.ts +35 -0
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +29 -11
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +46 -0
- package/dist/cli/share.test.js.map +1 -1
- package/dist/ignore.d.ts.map +1 -1
- package/dist/ignore.js +5 -0
- package/dist/ignore.js.map +1 -1
- package/dist/ignore.test.js +9 -0
- package/dist/ignore.test.js.map +1 -1
- package/dist/outcome-telemetry.d.ts +167 -0
- package/dist/outcome-telemetry.d.ts.map +1 -0
- package/dist/outcome-telemetry.js +479 -0
- package/dist/outcome-telemetry.js.map +1 -0
- package/dist/outcome-telemetry.test.d.ts +9 -0
- package/dist/outcome-telemetry.test.d.ts.map +1 -0
- package/dist/outcome-telemetry.test.js +412 -0
- package/dist/outcome-telemetry.test.js.map +1 -0
- package/dist/qmd-reindex.d.ts +48 -35
- package/dist/qmd-reindex.d.ts.map +1 -1
- package/dist/qmd-reindex.js +188 -60
- package/dist/qmd-reindex.js.map +1 -1
- package/dist/qmd-reindex.test.d.ts +3 -3
- package/dist/qmd-reindex.test.js +203 -41
- package/dist/qmd-reindex.test.js.map +1 -1
- package/dist/telemetry.d.ts +5 -4
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +187 -13
- package/dist/telemetry.js.map +1 -1
- package/dist/telemetry.test.js +157 -0
- package/dist/telemetry.test.js.map +1 -1
- package/dist/vault-client.d.ts +34 -0
- package/dist/vault-client.d.ts.map +1 -1
- package/dist/vault-client.js +23 -0
- package/dist/vault-client.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner-company.ts +4 -0
- package/src/bin/sync-runner-planning.test.ts +26 -0
- package/src/bin/sync-runner-planning.ts +8 -7
- package/src/bin/sync-runner-rollup.test.ts +37 -0
- package/src/bin/sync-runner-rollup.ts +3 -1
- package/src/bin/sync-runner-watch-loop.ts +22 -1
- package/src/bin/sync-runner.test.ts +111 -0
- package/src/bin/sync-runner.ts +62 -17
- package/src/cli/doctor.ts +3 -1
- package/src/cli/reindex.ts +24 -12
- package/src/cli/rescue-core.ts +3 -1
- package/src/cli/share.test.ts +60 -0
- package/src/cli/share.ts +29 -11
- package/src/ignore.test.ts +10 -0
- package/src/ignore.ts +5 -0
- package/src/outcome-telemetry.test.ts +498 -0
- package/src/outcome-telemetry.ts +639 -0
- package/src/qmd-reindex.test.ts +226 -40
- package/src/qmd-reindex.ts +209 -61
- package/src/telemetry.test.ts +194 -0
- package/src/telemetry.ts +233 -14
- package/src/vault-client.ts +55 -0
package/src/telemetry.ts
CHANGED
|
@@ -7,9 +7,8 @@
|
|
|
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
|
-
* file against
|
|
12
|
-
* sanitizes new rows through a tight allowlist that matches the server's
|
|
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
|
|
13
12
|
* KEEP_FIELDS set in `apps/hq-pro/src/vault-service/handlers/usage.ts`,
|
|
14
13
|
* batches into server-sized POST bodies, and ships them to `/v1/usage`.
|
|
15
14
|
*
|
|
@@ -23,6 +22,7 @@
|
|
|
23
22
|
* transient outage retries automatically on the next sync.
|
|
24
23
|
*/
|
|
25
24
|
|
|
25
|
+
import { createHash } from "node:crypto";
|
|
26
26
|
import { promises as fs } from "node:fs";
|
|
27
27
|
import * as os from "node:os";
|
|
28
28
|
import * as path from "node:path";
|
|
@@ -75,6 +75,8 @@ export interface CollectTelemetryOptions {
|
|
|
75
75
|
hqRoot?: string;
|
|
76
76
|
/** Override `~/.claude/projects` for tests. */
|
|
77
77
|
claudeProjectsRoot?: string;
|
|
78
|
+
/** Override `~/.codex` for tests. Both live and archived rollouts are scanned. */
|
|
79
|
+
codexRoot?: string;
|
|
78
80
|
/** Override `~/.hq/telemetry-cursor.json` for tests. */
|
|
79
81
|
cursorPath?: string;
|
|
80
82
|
/** Override `~/.hq/menubar.json` (the offline opt-in fallback) for tests. */
|
|
@@ -98,14 +100,27 @@ export interface CollectTelemetryResult {
|
|
|
98
100
|
|
|
99
101
|
// ── Cursor schema ─────────────────────────────────────────────────────────────
|
|
100
102
|
|
|
103
|
+
interface CodexUsageContext {
|
|
104
|
+
session_id?: string;
|
|
105
|
+
cwd?: string;
|
|
106
|
+
git_branch?: string;
|
|
107
|
+
session_model?: string;
|
|
108
|
+
collaboration_model?: string;
|
|
109
|
+
turn_model?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
101
112
|
interface CursorEntry {
|
|
102
113
|
offset: number;
|
|
103
114
|
mtime: number;
|
|
115
|
+
/** Runtime context needed to resume a Codex rollout from a byte offset. */
|
|
116
|
+
context?: CodexUsageContext;
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
interface TelemetryCursor {
|
|
107
120
|
version: string;
|
|
108
121
|
files: Record<string, CursorEntry>;
|
|
122
|
+
/** Preserved for cursor compatibility with the desktop collector. */
|
|
123
|
+
codex_next_rollout?: string;
|
|
109
124
|
}
|
|
110
125
|
|
|
111
126
|
function emptyCursor(): TelemetryCursor {
|
|
@@ -117,7 +132,13 @@ async function loadCursor(cursorPath: string): Promise<TelemetryCursor> {
|
|
|
117
132
|
const raw = await fs.readFile(cursorPath, "utf-8");
|
|
118
133
|
const parsed = JSON.parse(raw) as Partial<TelemetryCursor>;
|
|
119
134
|
if (parsed && typeof parsed === "object" && parsed.files && typeof parsed.files === "object") {
|
|
120
|
-
return {
|
|
135
|
+
return {
|
|
136
|
+
version: parsed.version ?? "1",
|
|
137
|
+
files: parsed.files as Record<string, CursorEntry>,
|
|
138
|
+
...(typeof parsed.codex_next_rollout === "string"
|
|
139
|
+
? { codex_next_rollout: parsed.codex_next_rollout }
|
|
140
|
+
: {}),
|
|
141
|
+
};
|
|
121
142
|
}
|
|
122
143
|
} catch {
|
|
123
144
|
// Missing / unparseable — start fresh.
|
|
@@ -377,6 +398,163 @@ async function listJsonlFiles(root: string): Promise<string[]> {
|
|
|
377
398
|
return out;
|
|
378
399
|
}
|
|
379
400
|
|
|
401
|
+
// ── Codex rollout adapter ────────────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
const MAX_ID_BYTES = 256;
|
|
404
|
+
const MAX_TIMESTAMP_BYTES = 128;
|
|
405
|
+
const MAX_PATH_BYTES = 4 * 1024;
|
|
406
|
+
const MAX_MODEL_BYTES = 256;
|
|
407
|
+
|
|
408
|
+
function boundedString(value: unknown, maxBytes: number): string | undefined {
|
|
409
|
+
return typeof value === "string" &&
|
|
410
|
+
value.length > 0 &&
|
|
411
|
+
Buffer.byteLength(value, "utf-8") <= maxBytes
|
|
412
|
+
? value
|
|
413
|
+
: undefined;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function tokenCount(value: unknown): number {
|
|
417
|
+
return typeof value === "number" &&
|
|
418
|
+
Number.isSafeInteger(value) &&
|
|
419
|
+
value >= 0
|
|
420
|
+
? value
|
|
421
|
+
: 0;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function cloneCodexContext(context: CodexUsageContext): CodexUsageContext {
|
|
425
|
+
return { ...context };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function codexModel(context: CodexUsageContext): string | undefined {
|
|
429
|
+
return context.turn_model ?? context.collaboration_model ?? context.session_model;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function codexGitBranch(payload: Record<string, unknown>): string | undefined {
|
|
433
|
+
const direct =
|
|
434
|
+
boundedString(payload.gitBranch, MAX_PATH_BYTES) ??
|
|
435
|
+
boundedString(payload.git_branch, MAX_PATH_BYTES);
|
|
436
|
+
if (direct) return direct;
|
|
437
|
+
const git = payload.git;
|
|
438
|
+
return git && typeof git === "object" && !Array.isArray(git)
|
|
439
|
+
? boundedString((git as Record<string, unknown>).branch, MAX_PATH_BYTES)
|
|
440
|
+
: undefined;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function collaborationModel(
|
|
444
|
+
payload: Record<string, unknown>,
|
|
445
|
+
): string | undefined {
|
|
446
|
+
const settings = payload.settings;
|
|
447
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
return boundedString((settings as Record<string, unknown>).model, MAX_MODEL_BYTES);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function stableCodexEventId(
|
|
454
|
+
rolloutIdentity: string,
|
|
455
|
+
startOffset: number,
|
|
456
|
+
endOffset: number,
|
|
457
|
+
): string {
|
|
458
|
+
const offsets = Buffer.alloc(16);
|
|
459
|
+
offsets.writeBigUInt64BE(BigInt(startOffset), 0);
|
|
460
|
+
offsets.writeBigUInt64BE(BigInt(endOffset), 8);
|
|
461
|
+
return "codex-" + createHash("sha256")
|
|
462
|
+
.update(rolloutIdentity)
|
|
463
|
+
.update(Buffer.from([0]))
|
|
464
|
+
.update(offsets)
|
|
465
|
+
.digest("hex");
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Adapt a Codex rollout record to the Claude-shaped sanitizer input. */
|
|
469
|
+
function codexUsageRow(
|
|
470
|
+
row: unknown,
|
|
471
|
+
context: CodexUsageContext,
|
|
472
|
+
rolloutIdentity: string,
|
|
473
|
+
startOffset: number,
|
|
474
|
+
endOffset: number,
|
|
475
|
+
): Record<string, unknown> | null {
|
|
476
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) return null;
|
|
477
|
+
const obj = row as Record<string, unknown>;
|
|
478
|
+
const kind = obj.type;
|
|
479
|
+
const rawPayload = obj.payload;
|
|
480
|
+
const payload =
|
|
481
|
+
rawPayload && typeof rawPayload === "object" && !Array.isArray(rawPayload)
|
|
482
|
+
? (rawPayload as Record<string, unknown>)
|
|
483
|
+
: undefined;
|
|
484
|
+
|
|
485
|
+
if (kind === "session_meta") {
|
|
486
|
+
if (!payload) return null;
|
|
487
|
+
context.session_id = boundedString(payload.id, MAX_ID_BYTES) ?? context.session_id;
|
|
488
|
+
context.cwd = boundedString(payload.cwd, MAX_PATH_BYTES);
|
|
489
|
+
context.git_branch = codexGitBranch(payload);
|
|
490
|
+
context.session_model = boundedString(payload.model, MAX_MODEL_BYTES);
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
if (kind === "turn_context") {
|
|
494
|
+
context.turn_model = payload
|
|
495
|
+
? boundedString(payload.model, MAX_MODEL_BYTES)
|
|
496
|
+
: undefined;
|
|
497
|
+
const mode = payload?.collaboration_mode;
|
|
498
|
+
if (mode && typeof mode === "object" && !Array.isArray(mode)) {
|
|
499
|
+
const model = collaborationModel(mode as Record<string, unknown>);
|
|
500
|
+
if (model) context.collaboration_model = model;
|
|
501
|
+
}
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
if (kind === "collaboration_mode") {
|
|
505
|
+
context.collaboration_model = payload
|
|
506
|
+
? collaborationModel(payload)
|
|
507
|
+
: undefined;
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
if (kind !== "event_msg" || !payload) return null;
|
|
511
|
+
if (payload.type === "collaboration_mode") {
|
|
512
|
+
context.collaboration_model = collaborationModel(payload);
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
if (payload.type !== "token_count") return null;
|
|
516
|
+
|
|
517
|
+
const info = payload.info;
|
|
518
|
+
if (!info || typeof info !== "object" || Array.isArray(info)) return null;
|
|
519
|
+
const last = (info as Record<string, unknown>).last_token_usage;
|
|
520
|
+
if (!last || typeof last !== "object" || Array.isArray(last)) return null;
|
|
521
|
+
const usage = last as Record<string, unknown>;
|
|
522
|
+
const outputTokens =
|
|
523
|
+
tokenCount(usage.output_tokens) + tokenCount(usage.reasoning_output_tokens);
|
|
524
|
+
const model = codexModel(context);
|
|
525
|
+
const normalized: Record<string, unknown> = {
|
|
526
|
+
uuid:
|
|
527
|
+
boundedString(obj.uuid, MAX_ID_BYTES) ??
|
|
528
|
+
stableCodexEventId(rolloutIdentity, startOffset, endOffset),
|
|
529
|
+
message: {
|
|
530
|
+
usage: {
|
|
531
|
+
input_tokens: tokenCount(usage.input_tokens),
|
|
532
|
+
output_tokens: outputTokens,
|
|
533
|
+
...(typeof usage.cached_input_tokens === "number"
|
|
534
|
+
? { cache_read_input_tokens: tokenCount(usage.cached_input_tokens) }
|
|
535
|
+
: {}),
|
|
536
|
+
},
|
|
537
|
+
...(model ? { model } : {}),
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
if (context.session_id) normalized.sessionId = context.session_id;
|
|
541
|
+
const timestamp = boundedString(obj.timestamp, MAX_TIMESTAMP_BYTES);
|
|
542
|
+
if (timestamp) normalized.timestamp = timestamp;
|
|
543
|
+
if (context.cwd) normalized.cwd = context.cwd;
|
|
544
|
+
if (context.git_branch) normalized.gitBranch = context.git_branch;
|
|
545
|
+
return normalized;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function listCodexRolloutFiles(codexRoot: string): Promise<string[]> {
|
|
549
|
+
const [live, archived] = await Promise.all([
|
|
550
|
+
listJsonlFiles(path.join(codexRoot, "sessions")),
|
|
551
|
+
listJsonlFiles(path.join(codexRoot, "archived_sessions")),
|
|
552
|
+
]);
|
|
553
|
+
return [...live, ...archived].filter((file) =>
|
|
554
|
+
path.basename(file).startsWith("rollout-"),
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
380
558
|
// ── Batching primitives ───────────────────────────────────────────────────────
|
|
381
559
|
|
|
382
560
|
const MAX_BATCH_EVENTS = 100;
|
|
@@ -387,6 +565,7 @@ interface RowSource {
|
|
|
387
565
|
filePath: string;
|
|
388
566
|
endOffset: number;
|
|
389
567
|
mtime: number;
|
|
568
|
+
context?: CodexUsageContext;
|
|
390
569
|
}
|
|
391
570
|
|
|
392
571
|
/**
|
|
@@ -455,7 +634,7 @@ function boundRowForPost(
|
|
|
455
634
|
// ── Main entry point ──────────────────────────────────────────────────────────
|
|
456
635
|
|
|
457
636
|
/**
|
|
458
|
-
* Scan, sanitize, and POST
|
|
637
|
+
* Scan, sanitize, and POST new Claude Code and Codex usage rows.
|
|
459
638
|
*
|
|
460
639
|
* Fire-and-forget from the caller's perspective: errors are caught internally
|
|
461
640
|
* and surfaced only via `log`. The returned summary lets observers (e.g.
|
|
@@ -467,6 +646,7 @@ export async function collectAndSendTelemetry(
|
|
|
467
646
|
): Promise<CollectTelemetryResult> {
|
|
468
647
|
const home = os.homedir();
|
|
469
648
|
const claudeProjectsRoot = opts.claudeProjectsRoot ?? path.join(home, ".claude", "projects");
|
|
649
|
+
const codexRoot = opts.codexRoot ?? path.join(home, ".codex");
|
|
470
650
|
const cursorPath = opts.cursorPath ?? path.join(home, ".hq", "telemetry-cursor.json");
|
|
471
651
|
const menubarPath = opts.menubarPath ?? path.join(home, ".hq", "menubar.json");
|
|
472
652
|
const log = opts.log ?? (() => {});
|
|
@@ -565,7 +745,14 @@ export async function collectAndSendTelemetry(
|
|
|
565
745
|
const rotationResets: Record<string, CursorEntry> = {};
|
|
566
746
|
const newlyCommitted: Record<string, CursorEntry> = {};
|
|
567
747
|
|
|
568
|
-
const
|
|
748
|
+
const [claudeFiles, codexFiles] = await Promise.all([
|
|
749
|
+
listJsonlFiles(claudeProjectsRoot),
|
|
750
|
+
listCodexRolloutFiles(codexRoot),
|
|
751
|
+
]);
|
|
752
|
+
const files: Array<{ filePath: string; kind: "claude" | "codex" }> = [
|
|
753
|
+
...claudeFiles.map((filePath) => ({ filePath, kind: "claude" as const })),
|
|
754
|
+
...codexFiles.map((filePath) => ({ filePath, kind: "codex" as const })),
|
|
755
|
+
];
|
|
569
756
|
|
|
570
757
|
// 3. Walk each file, sanitize new rows, batch, flush at the server contract.
|
|
571
758
|
//
|
|
@@ -599,15 +786,26 @@ export async function collectAndSendTelemetry(
|
|
|
599
786
|
batchesSent++;
|
|
600
787
|
eventsSent += events.length;
|
|
601
788
|
// Advance cursor to max(endOffset) per file in this batch.
|
|
602
|
-
const maxPerFile = new Map<
|
|
789
|
+
const maxPerFile = new Map<
|
|
790
|
+
string,
|
|
791
|
+
{ mtime: number; offset: number; context?: CodexUsageContext }
|
|
792
|
+
>();
|
|
603
793
|
for (const src of sources) {
|
|
604
794
|
const cur = maxPerFile.get(src.filePath);
|
|
605
795
|
if (!cur || src.endOffset > cur.offset) {
|
|
606
|
-
maxPerFile.set(src.filePath, {
|
|
796
|
+
maxPerFile.set(src.filePath, {
|
|
797
|
+
mtime: src.mtime,
|
|
798
|
+
offset: src.endOffset,
|
|
799
|
+
...(src.context ? { context: cloneCodexContext(src.context) } : {}),
|
|
800
|
+
});
|
|
607
801
|
}
|
|
608
802
|
}
|
|
609
803
|
for (const [fp, entry] of maxPerFile) {
|
|
610
|
-
newlyCommitted[fp] = {
|
|
804
|
+
newlyCommitted[fp] = {
|
|
805
|
+
offset: entry.offset,
|
|
806
|
+
mtime: entry.mtime,
|
|
807
|
+
...(entry.context ? { context: entry.context } : {}),
|
|
808
|
+
};
|
|
611
809
|
}
|
|
612
810
|
} catch (err) {
|
|
613
811
|
log(`[telemetry] postUsage failed (${(err as Error).message ?? err}) — cursor not advanced for ${sources.length} rows`);
|
|
@@ -615,7 +813,7 @@ export async function collectAndSendTelemetry(
|
|
|
615
813
|
}
|
|
616
814
|
};
|
|
617
815
|
|
|
618
|
-
for (const filePath of files) {
|
|
816
|
+
for (const { filePath, kind } of files) {
|
|
619
817
|
let stat;
|
|
620
818
|
try {
|
|
621
819
|
stat = await fs.stat(filePath);
|
|
@@ -627,11 +825,14 @@ export async function collectAndSendTelemetry(
|
|
|
627
825
|
|
|
628
826
|
const stored = cursor.files[filePath] ?? { offset: 0, mtime: 0 };
|
|
629
827
|
let offset = stored.offset;
|
|
828
|
+
let codexContext: CodexUsageContext | undefined =
|
|
829
|
+
kind === "codex" ? cloneCodexContext(stored.context ?? {}) : undefined;
|
|
630
830
|
|
|
631
831
|
const rotated =
|
|
632
832
|
currentSize < offset || (stored.mtime > 0 && currentMtime < stored.mtime);
|
|
633
833
|
if (rotated) {
|
|
634
834
|
offset = 0;
|
|
835
|
+
if (kind === "codex") codexContext = {};
|
|
635
836
|
rotationResets[filePath] = { offset: 0, mtime: currentMtime };
|
|
636
837
|
}
|
|
637
838
|
|
|
@@ -671,17 +872,28 @@ export async function collectAndSendTelemetry(
|
|
|
671
872
|
} catch {
|
|
672
873
|
continue;
|
|
673
874
|
}
|
|
875
|
+
const startOffset = i === 0 ? offset : lineEndOffsets[i - 1];
|
|
876
|
+
const sourceRow =
|
|
877
|
+
kind === "codex"
|
|
878
|
+
? codexUsageRow(
|
|
879
|
+
parsed,
|
|
880
|
+
codexContext!,
|
|
881
|
+
filePath,
|
|
882
|
+
startOffset,
|
|
883
|
+
lineEndOffsets[i],
|
|
884
|
+
)
|
|
885
|
+
: parsed;
|
|
674
886
|
// Resolve this row's cwd → owning company (cmp_* uid) before sanitizing,
|
|
675
887
|
// using the per-run map. Unresolved → undefined → companyUid omitted.
|
|
676
888
|
const rowCwd =
|
|
677
|
-
|
|
678
|
-
? (
|
|
889
|
+
sourceRow && typeof sourceRow === "object" && !Array.isArray(sourceRow)
|
|
890
|
+
? (sourceRow as Record<string, unknown>).cwd
|
|
679
891
|
: undefined;
|
|
680
892
|
const companyUid = resolveCompanyForCwd(
|
|
681
893
|
typeof rowCwd === "string" ? rowCwd : undefined,
|
|
682
894
|
repoCompanyMap,
|
|
683
895
|
);
|
|
684
|
-
const sanitized = sanitizeRow(
|
|
896
|
+
const sanitized = sanitizeRow(sourceRow, companyUid);
|
|
685
897
|
if (!sanitized) continue;
|
|
686
898
|
const maxRowBytes = MAX_BATCH_BYTES - ENVELOPE_BYTES;
|
|
687
899
|
const wasOversized = jsonBytes(sanitized) > maxRowBytes;
|
|
@@ -722,6 +934,7 @@ export async function collectAndSendTelemetry(
|
|
|
722
934
|
filePath,
|
|
723
935
|
endOffset: lineEndOffsets[i],
|
|
724
936
|
mtime: currentMtime,
|
|
937
|
+
...(codexContext ? { context: cloneCodexContext(codexContext) } : {}),
|
|
725
938
|
});
|
|
726
939
|
}
|
|
727
940
|
}
|
|
@@ -733,7 +946,13 @@ export async function collectAndSendTelemetry(
|
|
|
733
946
|
for (const [fp, entry] of Object.entries(rotationResets)) finalFiles[fp] = entry;
|
|
734
947
|
for (const [fp, entry] of Object.entries(newlyCommitted)) finalFiles[fp] = entry;
|
|
735
948
|
|
|
736
|
-
await saveCursor(cursorPath, {
|
|
949
|
+
await saveCursor(cursorPath, {
|
|
950
|
+
version: "1",
|
|
951
|
+
files: finalFiles,
|
|
952
|
+
...(cursor.codex_next_rollout
|
|
953
|
+
? { codex_next_rollout: cursor.codex_next_rollout }
|
|
954
|
+
: {}),
|
|
955
|
+
});
|
|
737
956
|
|
|
738
957
|
return {
|
|
739
958
|
enabled: true,
|
package/src/vault-client.ts
CHANGED
|
@@ -513,6 +513,34 @@ export interface SkillInvocationBatch {
|
|
|
513
513
|
/** Same wire shape as `UsageIngestResult`; aliased for call-site clarity. */
|
|
514
514
|
export type SkillInvocationIngestResult = UsageIngestResult;
|
|
515
515
|
|
|
516
|
+
// ---------------------------------------------------------------------------
|
|
517
|
+
// Outcome events (hq-pro `/v1/outcome-events`)
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
|
|
520
|
+
export interface OutcomeEventsBatch {
|
|
521
|
+
/**
|
|
522
|
+
* Outcome-event rows. Each row is a plain object containing only the fields
|
|
523
|
+
* in the server's KEEP allowlist (type, occurredAt, companyUid, repo, branch,
|
|
524
|
+
* dedupeKey, and the type-specific refs projectName / storyId). Unlike the
|
|
525
|
+
* usage/skill batch, this batch carries NO machineId/installerVersion — the
|
|
526
|
+
* ingest handler (`apps/hq-pro/src/vault-service/handlers/outcome-events.ts`)
|
|
527
|
+
* accepts only `{ events }`. `personUid` MUST NOT appear on the batch or any
|
|
528
|
+
* event — hq-pro resolves the caller server-side from the Cognito JWT and
|
|
529
|
+
* REJECTS a body-supplied person identifier. Any field outside the allowlist
|
|
530
|
+
* is rejected with a 4xx `invalid-event`, so the emitter in
|
|
531
|
+
* `./outcome-telemetry.ts` is the only thing allowed to produce these.
|
|
532
|
+
*/
|
|
533
|
+
events: Array<Record<string, unknown>>;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export interface OutcomeEventsIngestResult {
|
|
537
|
+
ok: boolean;
|
|
538
|
+
/** Rows newly written this request. */
|
|
539
|
+
written: number;
|
|
540
|
+
/** Rows collapsed by the server-side conditional put (idempotent replay). */
|
|
541
|
+
deduped: number;
|
|
542
|
+
}
|
|
543
|
+
|
|
516
544
|
// ---------------------------------------------------------------------------
|
|
517
545
|
// Retry config
|
|
518
546
|
// ---------------------------------------------------------------------------
|
|
@@ -692,6 +720,16 @@ const usageIngestResultSchema: VaultResponseSchema<UsageIngestResult> = z
|
|
|
692
720
|
})
|
|
693
721
|
.strip();
|
|
694
722
|
|
|
723
|
+
const outcomeEventsIngestResultSchema: VaultResponseSchema<OutcomeEventsIngestResult> = z
|
|
724
|
+
.object({
|
|
725
|
+
ok: z.boolean(),
|
|
726
|
+
written: z.number().nonnegative(),
|
|
727
|
+
// Older/partial servers may omit `deduped`; default to 0 so a 2xx that
|
|
728
|
+
// reports only `written` still decodes (best-effort, matches usage rollout).
|
|
729
|
+
deduped: z.number().nonnegative().optional().transform((v) => v ?? 0),
|
|
730
|
+
})
|
|
731
|
+
.strip() as unknown as VaultResponseSchema<OutcomeEventsIngestResult>;
|
|
732
|
+
|
|
695
733
|
/**
|
|
696
734
|
* Accept the canonical v1 response and the previous raw-event aliases, then
|
|
697
735
|
* normalize at this boundary so telemetry remains best-effort during rollout.
|
|
@@ -1446,6 +1484,23 @@ export class VaultClient {
|
|
|
1446
1484
|
return this.post("/v1/skill-invocations", batch, usageIngestResultSchema);
|
|
1447
1485
|
}
|
|
1448
1486
|
|
|
1487
|
+
/**
|
|
1488
|
+
* `POST /v1/outcome-events` — upload a batch of delivery-outcome events
|
|
1489
|
+
* (story-completed / project-shipped, per outcome-leaderboard US-004).
|
|
1490
|
+
*
|
|
1491
|
+
* Same trust model as `postUsage`: `personUid` MUST NOT appear on the batch
|
|
1492
|
+
* or any event — hq-pro resolves the caller from the JWT and rejects a
|
|
1493
|
+
* body-supplied person identifier. Gated by the same telemetry opt-in as
|
|
1494
|
+
* `/v1/usage`. Server-side conditional PutItem on the composite dedupeKey
|
|
1495
|
+
* makes re-syncs and multi-machine replays idempotent (they count as
|
|
1496
|
+
* `deduped`, never a double-write).
|
|
1497
|
+
*/
|
|
1498
|
+
async postOutcomeEvents(
|
|
1499
|
+
batch: OutcomeEventsBatch,
|
|
1500
|
+
): Promise<OutcomeEventsIngestResult> {
|
|
1501
|
+
return this.post("/v1/outcome-events", batch, outcomeEventsIngestResultSchema);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1449
1504
|
/**
|
|
1450
1505
|
* `POST /v1/telemetry/events` — upload a capped batch of hq-cloud ACTION
|
|
1451
1506
|
* events.
|