@indigoai-us/hq-cloud 6.14.29 → 6.14.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.
@@ -81,6 +81,7 @@ function makeClient(opts: {
81
81
  interface TestEnv {
82
82
  root: string;
83
83
  claudeProjects: string;
84
+ codexRoot: string;
84
85
  cursorPath: string;
85
86
  menubarPath: string;
86
87
  }
@@ -88,11 +89,15 @@ interface TestEnv {
88
89
  function setupEnv(): TestEnv {
89
90
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "hq-telemetry-test-"));
90
91
  const claudeProjects = path.join(root, ".claude", "projects");
92
+ const codexRoot = path.join(root, ".codex");
91
93
  fs.mkdirSync(claudeProjects, { recursive: true });
94
+ fs.mkdirSync(path.join(codexRoot, "sessions"), { recursive: true });
95
+ fs.mkdirSync(path.join(codexRoot, "archived_sessions"), { recursive: true });
92
96
  fs.mkdirSync(path.join(root, ".hq"), { recursive: true });
93
97
  return {
94
98
  root,
95
99
  claudeProjects,
100
+ codexRoot,
96
101
  cursorPath: path.join(root, ".hq", "telemetry-cursor.json"),
97
102
  menubarPath: path.join(root, ".hq", "menubar.json"),
98
103
  };
@@ -120,6 +125,9 @@ function makeOpts(env: TestEnv, client: TelemetryClientSurface): CollectTelemetr
120
125
  machineId: "test-machine",
121
126
  installerVersion: "test-version",
122
127
  claudeProjectsRoot: env.claudeProjects,
128
+ codexRoot: env.codexRoot,
129
+ maxScanBytesPerSource: Number.MAX_SAFE_INTEGER,
130
+ maxBatchesPerRun: Number.MAX_SAFE_INTEGER,
123
131
  cursorPath: env.cursorPath,
124
132
  menubarPath: env.menubarPath,
125
133
  };
@@ -634,6 +642,284 @@ describe("collectAndSendTelemetry", () => {
634
642
  });
635
643
  });
636
644
 
645
+ // ── Codex rollout collection ────────────────────────────────────────────────
646
+
647
+ function codexLine(value: Record<string, unknown>): string {
648
+ return JSON.stringify(value);
649
+ }
650
+
651
+ function writeCodexRollout(
652
+ env: TestEnv,
653
+ location: "sessions" | "archived_sessions",
654
+ name: string,
655
+ lines: Array<Record<string, unknown>>,
656
+ ): string {
657
+ const dir =
658
+ location === "sessions"
659
+ ? path.join(env.codexRoot, location, "2026", "07", "30")
660
+ : path.join(env.codexRoot, location);
661
+ fs.mkdirSync(dir, { recursive: true });
662
+ const file = path.join(dir, name);
663
+ fs.writeFileSync(file, lines.map(codexLine).join("\n") + "\n", "utf-8");
664
+ return file;
665
+ }
666
+
667
+ function codexToken(
668
+ timestamp: string,
669
+ uuid: string,
670
+ input: number,
671
+ output: number,
672
+ reasoning = 0,
673
+ cached = 0,
674
+ ): Record<string, unknown> {
675
+ return {
676
+ timestamp,
677
+ uuid,
678
+ type: "event_msg",
679
+ payload: {
680
+ type: "token_count",
681
+ info: {
682
+ last_token_usage: {
683
+ input_tokens: input,
684
+ output_tokens: output,
685
+ reasoning_output_tokens: reasoning,
686
+ cached_input_tokens: cached,
687
+ },
688
+ total_token_usage: {
689
+ input_tokens: 999_999,
690
+ output_tokens: 999_999,
691
+ },
692
+ },
693
+ raw_content: "must never leave the machine",
694
+ },
695
+ };
696
+ }
697
+
698
+ describe("collectAndSendTelemetry — Codex rollouts", () => {
699
+ let env: TestEnv;
700
+ beforeEach(() => {
701
+ env = setupEnv();
702
+ });
703
+ afterEach(() => teardownEnv(env));
704
+
705
+ it("collects token deltas from live and archived Linux rollouts", async () => {
706
+ writeCodexRollout(env, "sessions", "rollout-live.jsonl", [
707
+ {
708
+ timestamp: "2026-07-30T10:00:00Z",
709
+ type: "session_meta",
710
+ payload: {
711
+ id: "codex-live",
712
+ cwd: "/home/yousuf/hq/repos/private/hq-cloud",
713
+ git: { branch: "feature/live" },
714
+ model: "session-model",
715
+ instructions: "private prompt",
716
+ },
717
+ },
718
+ {
719
+ timestamp: "2026-07-30T10:00:01Z",
720
+ type: "turn_context",
721
+ payload: { model: "codex-auto-review" },
722
+ },
723
+ codexToken("2026-07-30T10:00:02Z", "codex-event-live", 11, 7, 5, 3),
724
+ ]);
725
+ writeCodexRollout(env, "archived_sessions", "rollout-archived.jsonl", [
726
+ {
727
+ timestamp: "2026-07-29T10:00:00Z",
728
+ type: "session_meta",
729
+ payload: { id: "codex-archived", cwd: "/repo", model: "gpt-5-codex" },
730
+ },
731
+ codexToken("2026-07-29T10:00:01Z", "codex-event-archived", 2, 3, 1, 1),
732
+ ]);
733
+
734
+ const client = makeClient();
735
+ const result = await collectAndSendTelemetry(makeOpts(env, client));
736
+ const events = client.posts.flatMap((post) => post.events);
737
+
738
+ expect(result.filesScanned).toBe(2);
739
+ expect(result.eventsSent).toBe(2);
740
+ expect(events).toEqual(
741
+ expect.arrayContaining([
742
+ {
743
+ sessionId: "codex-live",
744
+ timestamp: "2026-07-30T10:00:02Z",
745
+ uuid: "codex-event-live",
746
+ cwd: "/home/yousuf/hq/repos/private/hq-cloud",
747
+ gitBranch: "feature/live",
748
+ model: "codex-auto-review",
749
+ inputTokens: 11,
750
+ outputTokens: 12,
751
+ cacheReadInputTokens: 3,
752
+ },
753
+ expect.objectContaining({
754
+ sessionId: "codex-archived",
755
+ model: "gpt-5-codex",
756
+ inputTokens: 2,
757
+ outputTokens: 4,
758
+ cacheReadInputTokens: 1,
759
+ }),
760
+ ]),
761
+ );
762
+ expect(JSON.stringify(events)).not.toContain("private prompt");
763
+ expect(JSON.stringify(events)).not.toContain("raw_content");
764
+ expect(JSON.stringify(events)).not.toContain("999999");
765
+
766
+ await collectAndSendTelemetry(makeOpts(env, client));
767
+ expect(client.posts.flatMap((post) => post.events)).toHaveLength(2);
768
+ });
769
+
770
+ it("persists model context for incremental rollouts and generates stable IDs", async () => {
771
+ const file = writeCodexRollout(env, "sessions", "rollout-incremental.jsonl", [
772
+ {
773
+ type: "session_meta",
774
+ payload: { id: "incremental", cwd: "/repo", model: "session-model" },
775
+ },
776
+ { type: "turn_context", payload: { model: "model-a" } },
777
+ codexToken("2026-07-30T13:00:00Z", "first-event", 1, 2),
778
+ ]);
779
+ fs.writeFileSync(
780
+ env.cursorPath,
781
+ JSON.stringify({ version: "1", files: {}, codex_next_rollout: file }),
782
+ );
783
+ const client = makeClient();
784
+
785
+ await collectAndSendTelemetry(makeOpts(env, client));
786
+ fs.appendFileSync(
787
+ file,
788
+ [
789
+ codexLine({ type: "turn_context", payload: { model: "model-b" } }),
790
+ codexLine({
791
+ ...codexToken("2026-07-30T13:01:00Z", "temporary", 3, 4),
792
+ uuid: undefined,
793
+ }),
794
+ ].join("\n") + "\n",
795
+ );
796
+ await collectAndSendTelemetry(makeOpts(env, client));
797
+
798
+ const events = client.posts.flatMap((post) => post.events);
799
+ expect(events).toHaveLength(2);
800
+ expect(events[1]).toMatchObject({
801
+ sessionId: "incremental",
802
+ cwd: "/repo",
803
+ model: "model-b",
804
+ inputTokens: 3,
805
+ outputTokens: 4,
806
+ });
807
+ expect(events[1].uuid).toMatch(/^codex-[a-f0-9]{64}$/);
808
+ const cursor = JSON.parse(fs.readFileSync(env.cursorPath, "utf-8"));
809
+ expect(cursor.files[file].context).toMatchObject({
810
+ session_id: "incremental",
811
+ cwd: "/repo",
812
+ session_model: "session-model",
813
+ turn_model: "model-b",
814
+ });
815
+ expect(cursor.codex_next_rollout).toBe(file);
816
+ });
817
+
818
+ it("bounds a quiet first backfill and resumes until the token event", async () => {
819
+ const file = writeCodexRollout(env, "sessions", "rollout-bounded.jsonl", [
820
+ { type: "session_meta", payload: { id: "bounded", model: "gpt-5-codex" } },
821
+ ...Array.from({ length: 8 }, (_, index) => ({
822
+ type: "response_item",
823
+ payload: { type: "message", content: "x".repeat(140), index },
824
+ })),
825
+ codexToken("2026-07-30T14:00:00Z", "bounded-event", 5, 7),
826
+ ]);
827
+ const client = makeClient();
828
+ const opts = {
829
+ ...makeOpts(env, client),
830
+ maxScanBytesPerSource: 350,
831
+ };
832
+
833
+ const first = await collectAndSendTelemetry(opts);
834
+
835
+ expect(first.eventsSent).toBe(0);
836
+ const firstOffset = readCursor(env).files[file]?.offset ?? 0;
837
+ expect(firstOffset).toBeGreaterThan(0);
838
+ expect(firstOffset).toBeLessThan(fs.statSync(file).size);
839
+
840
+ for (let run = 0; run < 10 && client.posts.length === 0; run++) {
841
+ await collectAndSendTelemetry(opts);
842
+ }
843
+ expect(client.posts.flatMap((post) => post.events)).toEqual([
844
+ expect.objectContaining({
845
+ sessionId: "bounded",
846
+ uuid: "bounded-event",
847
+ model: "gpt-5-codex",
848
+ }),
849
+ ]);
850
+ });
851
+
852
+ it("discards an oversized UTF-8 row without corrupting resume offsets", async () => {
853
+ const file = writeCodexRollout(env, "sessions", "rollout-utf8.jsonl", [
854
+ { type: "session_meta", payload: { id: "utf8", model: "gpt-5-codex" } },
855
+ {
856
+ type: "response_item",
857
+ payload: { type: "message", content: "😀".repeat(20_000) },
858
+ },
859
+ codexToken("2026-07-30T14:00:01Z", "utf8-event", 2, 3),
860
+ ]);
861
+ const client = makeClient();
862
+ const opts = {
863
+ ...makeOpts(env, client),
864
+ maxScanBytesPerSource: 64 * 1024,
865
+ };
866
+
867
+ for (let run = 0; run < 5 && client.posts.length === 0; run++) {
868
+ await collectAndSendTelemetry(opts);
869
+ }
870
+
871
+ const events = client.posts.flatMap((post) => post.events);
872
+ expect(events).toEqual([
873
+ expect.objectContaining({ uuid: "utf8-event", sessionId: "utf8" }),
874
+ ]);
875
+ expect(readCursor(env).files[file].offset).toBeLessThanOrEqual(
876
+ fs.statSync(file).size,
877
+ );
878
+ });
879
+
880
+ it("caps upload batches and resumes remaining token events", async () => {
881
+ writeCodexRollout(env, "sessions", "rollout-batch-cap.jsonl", [
882
+ { type: "session_meta", payload: { id: "batch-cap", model: "gpt-5-codex" } },
883
+ ...Array.from({ length: 201 }, (_, index) =>
884
+ codexToken(
885
+ "2026-07-30T14:01:" + String(index % 60).padStart(2, "0") + "Z",
886
+ "batch-cap-" + index,
887
+ 1,
888
+ 1,
889
+ ),
890
+ ),
891
+ ]);
892
+ const client = makeClient();
893
+ const opts = {
894
+ ...makeOpts(env, client),
895
+ maxScanBytesPerSource: 1_000_000,
896
+ maxBatchesPerRun: 2,
897
+ };
898
+
899
+ const first = await collectAndSendTelemetry(opts);
900
+ expect(first.batchesSent).toBe(2);
901
+ expect(first.eventsSent).toBe(200);
902
+
903
+ const second = await collectAndSendTelemetry(opts);
904
+ expect(second.eventsSent).toBe(1);
905
+ expect(client.posts.flatMap((post) => post.events)).toHaveLength(201);
906
+ });
907
+
908
+ it("does not advance a Codex rollout cursor when upload fails", async () => {
909
+ const file = writeCodexRollout(env, "sessions", "rollout-retry.jsonl", [
910
+ { type: "session_meta", payload: { id: "retry", model: "gpt-5-codex" } },
911
+ codexToken("2026-07-30T12:00:00Z", "retry-event", 1, 2),
912
+ ]);
913
+ const client = makeClient({ postResponse: new Error("server 500") });
914
+
915
+ const result = await collectAndSendTelemetry(makeOpts(env, client));
916
+
917
+ expect(result.eventsSent).toBe(0);
918
+ const cursor = readCursor(env);
919
+ expect(cursor.files[file]?.offset ?? 0).toBe(0);
920
+ });
921
+ });
922
+
637
923
  // ── companyUid edge attribution (US-002) ────────────────────────────────────────
638
924
 
639
925
  describe("collectAndSendTelemetry — companyUid attribution", () => {