@cookielab.io/oopst 0.6.1 → 0.6.3

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/index.js CHANGED
@@ -7,7 +7,7 @@ import { realpathSync } from "fs";
7
7
  import process4 from "process";
8
8
  import { fileURLToPath as fileURLToPath2 } from "url";
9
9
  import { NodeRuntime, NodeServices as NodeServices2 } from "@effect/platform-node";
10
- import { Effect as Effect26, Layer as Layer12, Option as Option4, Runtime } from "effect";
10
+ import { Effect as Effect27, Layer as Layer12, Option as Option4, Runtime } from "effect";
11
11
  import { CliOutput, Command, Flag } from "effect/unstable/cli";
12
12
 
13
13
  // src/config.ts
@@ -524,220 +524,74 @@ var ParserSourcesLive = Layer2.effect(
524
524
  import { Context as Context6, Effect as Effect10, Layer as Layer5 } from "effect";
525
525
 
526
526
  // src/sync/run.ts
527
- import { Effect as Effect9, Stream as Stream4 } from "effect";
528
-
529
- // src/sync/aggregate.ts
530
- import { createHmac } from "crypto";
531
- import { Effect as Effect4, Schema as Schema3, Stream as Stream2 } from "effect";
532
- var UsageAggregationErrorFields = {
533
- message: Schema3.String,
534
- cause: Schema3.optionalKey(Schema3.Defect())
535
- };
536
- var UsageAggregationError = class extends Schema3.TaggedErrorClass()(
537
- "UsageAggregationError",
538
- UsageAggregationErrorFields
539
- ) {
540
- };
541
- function toIso(hourUtc) {
542
- return hourUtc instanceof Date ? hourUtc.toISOString() : hourUtc;
543
- }
544
- function key(b) {
545
- return `${b.harness} ${b.model} ${toIso(b.hourUtc)}`;
546
- }
547
- function finalize(acc) {
548
- return {
549
- harness: acc.harness,
550
- model: acc.model,
551
- hourUtc: acc.hourUtc,
552
- inputTokens: acc.inputTokens,
553
- outputTokens: acc.outputTokens,
554
- cacheReadTokens: acc.cacheReadTokens,
555
- cacheCreateTokens: acc.cacheCreateTokens,
556
- sessionCount: acc.sessionIds.size
557
- };
558
- }
559
- var SESSION_KEY_SALT_PATTERN = /^[a-f0-9]{64}$/u;
560
- function createSessionKey(harness, localSessionId, sessionKeySalt) {
561
- if (!SESSION_KEY_SALT_PATTERN.test(sessionKeySalt)) {
562
- throw new Error("invalid session key salt");
563
- }
564
- return createHmac("sha256", Buffer.from(sessionKeySalt, "hex")).update("oopst/session-key/v1").update("\0").update(harness).update("\0").update(localSessionId).digest("hex");
565
- }
566
- function sessionMapKey(harness, sessionKey) {
567
- return `${harness} ${sessionKey}`;
568
- }
569
- function finalizeSession(acc) {
570
- return {
571
- harness: acc.harness,
572
- sessionKey: acc.sessionKey,
573
- startedAt: acc.startedAt.toISOString(),
574
- lastSeenAt: acc.lastSeenAt.toISOString()
575
- };
576
- }
577
- function addSessionAccumulator(sessionAccumulators, bucket, localSessionId, sessionKeySalt) {
578
- const sessionKey = createSessionKey(bucket.harness, localSessionId, sessionKeySalt);
579
- const sessionAccumulatorKey = sessionMapKey(bucket.harness, sessionKey);
580
- const existingSession = sessionAccumulators.get(sessionAccumulatorKey);
581
- if (existingSession) {
582
- if (bucket.sessionStartedAt.getTime() < existingSession.startedAt.getTime()) {
583
- existingSession.startedAt = bucket.sessionStartedAt;
584
- }
585
- if (bucket.sessionLastSeenAt.getTime() > existingSession.lastSeenAt.getTime()) {
586
- existingSession.lastSeenAt = bucket.sessionLastSeenAt;
587
- }
588
- return;
589
- }
590
- sessionAccumulators.set(sessionAccumulatorKey, {
591
- harness: bucket.harness,
592
- sessionKey,
593
- startedAt: bucket.sessionStartedAt,
594
- lastSeenAt: bucket.sessionLastSeenAt
595
- });
596
- }
597
- function createUsageAggregationState() {
598
- return {
599
- accumulators: /* @__PURE__ */ new Map(),
600
- sessionAccumulators: /* @__PURE__ */ new Map()
601
- };
602
- }
603
- function addBucketToAggregationState(state, bucket, sessionKeySalt) {
604
- const k = key(bucket);
605
- let acc = state.accumulators.get(k);
606
- if (!acc) {
607
- acc = {
608
- harness: bucket.harness,
609
- model: bucket.model,
610
- hourUtc: toIso(bucket.hourUtc),
611
- inputTokens: 0,
612
- outputTokens: 0,
613
- cacheReadTokens: 0,
614
- cacheCreateTokens: 0,
615
- sessionIds: /* @__PURE__ */ new Set()
616
- };
617
- state.accumulators.set(k, acc);
618
- }
619
- acc.inputTokens += bucket.inputTokens;
620
- acc.outputTokens += bucket.outputTokens;
621
- acc.cacheReadTokens += bucket.cacheReadTokens;
622
- acc.cacheCreateTokens += bucket.cacheCreateTokens;
623
- for (const id of bucket.sessionIds) {
624
- acc.sessionIds.add(id);
625
- addSessionAccumulator(state.sessionAccumulators, bucket, id, sessionKeySalt);
626
- }
627
- return state;
628
- }
629
- function addBucketToAggregationStateEffect(state, bucket, sessionKeySalt) {
630
- return Effect4.try({
631
- try: () => addBucketToAggregationState(state, bucket, sessionKeySalt),
632
- catch: toUsageAggregationError
633
- });
634
- }
635
- function finalizeUsageBatches(state, batchSize) {
636
- const finalized = [];
637
- for (const acc of state.accumulators.values()) {
638
- finalized.push(finalize(acc));
639
- }
640
- const finalizedSessions = [];
641
- for (const acc of state.sessionAccumulators.values()) {
642
- finalizedSessions.push(finalizeSession(acc));
643
- }
644
- const batches = [];
645
- const maxLength = Math.max(finalized.length, finalizedSessions.length);
646
- for (let i = 0; i < maxLength; i += batchSize) {
647
- batches.push({
648
- buckets: finalized.slice(i, i + batchSize),
649
- sessions: finalizedSessions.slice(i, i + batchSize)
650
- });
651
- }
652
- return batches;
653
- }
654
- function toUsageAggregationError(cause) {
655
- return new UsageAggregationError({
656
- message: `usage aggregation failed: ${errorMessage(cause)}`,
657
- cause
658
- });
659
- }
660
- function errorMessage(cause) {
661
- return cause instanceof Error ? cause.message : "unknown aggregation error";
662
- }
663
- function aggregateUsageStream(source, options) {
664
- return Stream2.fromIterableEffect(
665
- source.pipe(
666
- Stream2.runFoldEffect(
667
- () => createUsageAggregationState(),
668
- (state, bucket) => addBucketToAggregationStateEffect(state, bucket, options.sessionKeySalt)
669
- ),
670
- Effect4.map((state) => finalizeUsageBatches(state, options.batchSize))
671
- )
672
- );
673
- }
527
+ import { createHash } from "crypto";
674
528
 
675
529
  // ../../packages/api/src/errors.ts
676
- import { Schema as Schema4 } from "effect";
530
+ import { Schema as Schema3 } from "effect";
677
531
  var ErrorFields = {
678
- message: Schema4.String
532
+ message: Schema3.String
679
533
  };
680
534
  var InfrastructureErrorFields = {
681
535
  ...ErrorFields,
682
- cause: Schema4.optionalKey(Schema4.Defect())
536
+ cause: Schema3.optionalKey(Schema3.Defect())
683
537
  };
684
- var UnauthorizedError = class extends Schema4.TaggedErrorClass()("UnauthorizedError", ErrorFields) {
538
+ var UnauthorizedError = class extends Schema3.TaggedErrorClass()("UnauthorizedError", ErrorFields) {
685
539
  };
686
- var ForbiddenError = class extends Schema4.TaggedErrorClass()("ForbiddenError", ErrorFields) {
540
+ var ForbiddenError = class extends Schema3.TaggedErrorClass()("ForbiddenError", ErrorFields) {
687
541
  };
688
- var NotFoundError = class extends Schema4.TaggedErrorClass()("NotFoundError", ErrorFields) {
542
+ var NotFoundError = class extends Schema3.TaggedErrorClass()("NotFoundError", ErrorFields) {
689
543
  };
690
- var ConflictError = class extends Schema4.TaggedErrorClass()("ConflictError", ErrorFields) {
544
+ var ConflictError = class extends Schema3.TaggedErrorClass()("ConflictError", ErrorFields) {
691
545
  };
692
- var ValidationError = class extends Schema4.TaggedErrorClass()("ValidationError", ErrorFields) {
546
+ var ValidationError = class extends Schema3.TaggedErrorClass()("ValidationError", ErrorFields) {
693
547
  };
694
- var RateLimitedError = class extends Schema4.TaggedErrorClass()("RateLimitedError", ErrorFields) {
548
+ var RateLimitedError = class extends Schema3.TaggedErrorClass()("RateLimitedError", ErrorFields) {
695
549
  };
696
- var ExternalHttpError = class extends Schema4.TaggedErrorClass()(
550
+ var ExternalHttpError = class extends Schema3.TaggedErrorClass()(
697
551
  "ExternalHttpError",
698
552
  InfrastructureErrorFields
699
553
  ) {
700
554
  };
701
- var DatabaseError = class extends Schema4.TaggedErrorClass()(
555
+ var DatabaseError = class extends Schema3.TaggedErrorClass()(
702
556
  "DatabaseError",
703
557
  InfrastructureErrorFields
704
558
  ) {
705
559
  };
706
- var AppConfigError = class extends Schema4.TaggedErrorClass()(
560
+ var AppConfigError = class extends Schema3.TaggedErrorClass()(
707
561
  "AppConfigError",
708
562
  InfrastructureErrorFields
709
563
  ) {
710
564
  };
711
- var PricingRefreshError = class extends Schema4.TaggedErrorClass()(
565
+ var PricingRefreshError = class extends Schema3.TaggedErrorClass()(
712
566
  "PricingRefreshError",
713
567
  InfrastructureErrorFields
714
568
  ) {
715
569
  };
716
- var ParserSourceError2 = class extends Schema4.TaggedErrorClass()(
570
+ var ParserSourceError2 = class extends Schema3.TaggedErrorClass()(
717
571
  "ParserSourceError",
718
572
  InfrastructureErrorFields
719
573
  ) {
720
574
  };
721
- var SyncQuotaExceededError = class extends Schema4.TaggedErrorClass()(
575
+ var SyncQuotaExceededError = class extends Schema3.TaggedErrorClass()(
722
576
  "SyncQuotaExceededError",
723
577
  ErrorFields
724
578
  ) {
725
579
  };
726
- var SyncTimestampRejectedError = class extends Schema4.TaggedErrorClass()(
580
+ var SyncTimestampRejectedError = class extends Schema3.TaggedErrorClass()(
727
581
  "SyncTimestampRejectedError",
728
582
  ErrorFields
729
583
  ) {
730
584
  };
731
585
 
732
586
  // ../../packages/api/src/rpc/auth.ts
733
- import { Context as Context3, Schema as Schema7 } from "effect";
587
+ import { Context as Context3, Schema as Schema6 } from "effect";
734
588
  import { Rpc, RpcGroup } from "effect/unstable/rpc";
735
589
 
736
590
  // ../../packages/api/src/schemas/auth.ts
737
- import { Schema as Schema5, SchemaGetter } from "effect";
591
+ import { Schema as Schema4, SchemaGetter } from "effect";
738
592
  var MAX_DISPLAY_NAME_LENGTH = 80;
739
- var displayNameInput = Schema5.String.check(Schema5.isMaxLength(MAX_DISPLAY_NAME_LENGTH)).pipe(
740
- Schema5.decodeTo(Schema5.NullOr(Schema5.String), {
593
+ var displayNameInput = Schema4.String.check(Schema4.isMaxLength(MAX_DISPLAY_NAME_LENGTH)).pipe(
594
+ Schema4.decodeTo(Schema4.NullOr(Schema4.String), {
741
595
  decode: SchemaGetter.transform((value) => {
742
596
  const trimmed = value.trim();
743
597
  return trimmed.length === 0 ? null : trimmed;
@@ -745,12 +599,12 @@ var displayNameInput = Schema5.String.check(Schema5.isMaxLength(MAX_DISPLAY_NAME
745
599
  encode: SchemaGetter.transform((value) => value ?? "")
746
600
  })
747
601
  );
748
- var updateProfileInput = Schema5.Struct({
602
+ var updateProfileInput = Schema4.Struct({
749
603
  displayName: displayNameInput
750
604
  });
751
605
 
752
606
  // ../../packages/api/src/schemas/common.ts
753
- import { Schema as Schema6 } from "effect";
607
+ import { Schema as Schema5 } from "effect";
754
608
  var ISO_UTC_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u;
755
609
  var CIVIL_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/u;
756
610
  var HEX64_PATTERN = /^[0-9a-f]{64}$/u;
@@ -795,49 +649,49 @@ var isApiUrl = (value) => {
795
649
  return false;
796
650
  }
797
651
  };
798
- var nonEmptyTrimmedString = Schema6.String.check(
799
- Schema6.isTrimmed(),
800
- Schema6.isNonEmpty({ message: "Expected a non-empty string" })
652
+ var nonEmptyTrimmedString = Schema5.String.check(
653
+ Schema5.isTrimmed(),
654
+ Schema5.isNonEmpty({ message: "Expected a non-empty string" })
801
655
  );
802
- var safeInteger = Schema6.Number.check(
803
- Schema6.isInt(),
804
- Schema6.isBetween({ minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER })
656
+ var safeInteger = Schema5.Number.check(
657
+ Schema5.isInt(),
658
+ Schema5.isBetween({ minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER })
805
659
  );
806
- var nonNegativeSafeInteger = safeInteger.check(Schema6.isGreaterThanOrEqualTo(0));
807
- var isoUtcString = Schema6.String.check(
808
- Schema6.makeFilter(isExactUtcInstant, { expected: "an ISO-8601 UTC timestamp with millisecond precision" })
660
+ var nonNegativeSafeInteger = safeInteger.check(Schema5.isGreaterThanOrEqualTo(0));
661
+ var isoUtcString = Schema5.String.check(
662
+ Schema5.makeFilter(isExactUtcInstant, { expected: "an ISO-8601 UTC timestamp with millisecond precision" })
809
663
  );
810
- var civilDate = Schema6.String.check(
811
- Schema6.makeFilter(isCivilDate, { expected: "a valid calendar date formatted as YYYY-MM-DD" })
664
+ var civilDate = Schema5.String.check(
665
+ Schema5.makeFilter(isCivilDate, { expected: "a valid calendar date formatted as YYYY-MM-DD" })
812
666
  );
813
- var ianaTimezone = Schema6.String.check(
814
- Schema6.makeFilter(isIanaTimezone, { expected: "an IANA timezone identifier" })
667
+ var ianaTimezone = Schema5.String.check(
668
+ Schema5.makeFilter(isIanaTimezone, { expected: "an IANA timezone identifier" })
815
669
  );
816
- var uuidV7 = Schema6.String.check(Schema6.isUUID(UUID_V7_VERSION)).pipe(Schema6.brand("UuidV7"));
817
- var generalUuid = Schema6.String.check(Schema6.isUUID()).pipe(Schema6.brand("GeneralUuid"));
818
- var hex64 = Schema6.String.check(
819
- Schema6.isPattern(HEX64_PATTERN, { expected: "a 64-character lowercase hex string" })
670
+ var uuidV7 = Schema5.String.check(Schema5.isUUID(UUID_V7_VERSION)).pipe(Schema5.brand("UuidV7"));
671
+ var generalUuid = Schema5.String.check(Schema5.isUUID()).pipe(Schema5.brand("GeneralUuid"));
672
+ var hex64 = Schema5.String.check(
673
+ Schema5.isPattern(HEX64_PATTERN, { expected: "a 64-character lowercase hex string" })
820
674
  );
821
- var apiUrl = Schema6.String.check(Schema6.makeFilter(isApiUrl, { expected: "an HTTP(S) URL" }));
675
+ var apiUrl = Schema5.String.check(Schema5.makeFilter(isApiUrl, { expected: "an HTTP(S) URL" }));
822
676
 
823
677
  // ../../packages/api/src/rpc/auth.ts
824
- var AuthUserResponse = Schema7.Struct({
825
- user: Schema7.Struct({
826
- id: Schema7.String,
827
- email: Schema7.String,
828
- displayName: Schema7.NullOr(Schema7.String),
678
+ var AuthUserResponse = Schema6.Struct({
679
+ user: Schema6.Struct({
680
+ id: Schema6.String,
681
+ email: Schema6.String,
682
+ displayName: Schema6.NullOr(Schema6.String),
829
683
  createdAt: isoUtcString
830
684
  })
831
685
  });
832
- var LogoutResponse = Schema7.Struct({
833
- ok: Schema7.Boolean
686
+ var LogoutResponse = Schema6.Struct({
687
+ ok: Schema6.Boolean
834
688
  });
835
689
  var WebRpcContext = class extends Context3.Service()("@oopst/api/WebRpcContext") {
836
690
  };
837
691
  var AuthRpcs = RpcGroup.make(
838
692
  Rpc.make("auth.logout", {
839
693
  success: LogoutResponse,
840
- error: Schema7.Union([DatabaseError, UnauthorizedError])
694
+ error: Schema6.Union([DatabaseError, UnauthorizedError])
841
695
  }),
842
696
  Rpc.make("auth.me", {
843
697
  success: AuthUserResponse,
@@ -846,42 +700,42 @@ var AuthRpcs = RpcGroup.make(
846
700
  Rpc.make("auth.updateProfile", {
847
701
  payload: updateProfileInput,
848
702
  success: AuthUserResponse,
849
- error: Schema7.Union([DatabaseError, UnauthorizedError])
703
+ error: Schema6.Union([DatabaseError, UnauthorizedError])
850
704
  })
851
705
  );
852
706
 
853
707
  // ../../packages/api/src/rpc/devices.ts
854
- import { Schema as Schema9 } from "effect";
708
+ import { Schema as Schema8 } from "effect";
855
709
  import { Rpc as Rpc2, RpcGroup as RpcGroup2 } from "effect/unstable/rpc";
856
710
 
857
711
  // ../../packages/api/src/schemas/devices.ts
858
- import { Schema as Schema8 } from "effect";
712
+ import { Schema as Schema7 } from "effect";
859
713
  var MIN_DEVICE_NAME_LENGTH = 1;
860
714
  var MAX_DEVICE_NAME_LENGTH = 80;
861
715
  var UUID_V7_VERSION2 = 7;
862
- var deviceNameSchema = Schema8.String.check(
863
- Schema8.isMinLength(MIN_DEVICE_NAME_LENGTH),
864
- Schema8.isMaxLength(MAX_DEVICE_NAME_LENGTH)
716
+ var deviceNameSchema = Schema7.String.check(
717
+ Schema7.isMinLength(MIN_DEVICE_NAME_LENGTH),
718
+ Schema7.isMaxLength(MAX_DEVICE_NAME_LENGTH)
865
719
  );
866
- var editableDeviceNameSchema = Schema8.Trim.check(
867
- Schema8.isMinLength(MIN_DEVICE_NAME_LENGTH),
868
- Schema8.isMaxLength(MAX_DEVICE_NAME_LENGTH)
720
+ var editableDeviceNameSchema = Schema7.Trim.check(
721
+ Schema7.isMinLength(MIN_DEVICE_NAME_LENGTH),
722
+ Schema7.isMaxLength(MAX_DEVICE_NAME_LENGTH)
869
723
  );
870
- var deviceIdSchema = Schema8.String.check(Schema8.isUUID(UUID_V7_VERSION2));
871
- var renameDeviceInput = Schema8.Struct({
724
+ var deviceIdSchema = Schema7.String.check(Schema7.isUUID(UUID_V7_VERSION2));
725
+ var renameDeviceInput = Schema7.Struct({
872
726
  id: deviceIdSchema,
873
727
  name: editableDeviceNameSchema
874
728
  });
875
- var deleteDeviceInput = Schema8.Struct({
729
+ var deleteDeviceInput = Schema7.Struct({
876
730
  id: deviceIdSchema
877
731
  });
878
- var renameDeviceOutput = Schema8.Struct({
732
+ var renameDeviceOutput = Schema7.Struct({
879
733
  id: deviceIdSchema,
880
- name: Schema8.String,
734
+ name: Schema7.String,
881
735
  lastSeen: isoUtcString
882
736
  });
883
- var deleteDeviceOutput = Schema8.Struct({
884
- ok: Schema8.Boolean
737
+ var deleteDeviceOutput = Schema7.Struct({
738
+ ok: Schema7.Boolean
885
739
  });
886
740
 
887
741
  // ../../packages/api/src/rpc/devices.ts
@@ -889,27 +743,27 @@ var DevicesRpcs = RpcGroup2.make(
889
743
  Rpc2.make("devices.rename", {
890
744
  payload: renameDeviceInput,
891
745
  success: renameDeviceOutput,
892
- error: Schema9.Union([ConflictError, DatabaseError, NotFoundError, UnauthorizedError])
746
+ error: Schema8.Union([ConflictError, DatabaseError, NotFoundError, UnauthorizedError])
893
747
  }),
894
748
  Rpc2.make("devices.delete", {
895
749
  payload: deleteDeviceInput,
896
750
  success: deleteDeviceOutput,
897
- error: Schema9.Union([DatabaseError, NotFoundError, UnauthorizedError])
751
+ error: Schema8.Union([DatabaseError, NotFoundError, UnauthorizedError])
898
752
  })
899
753
  );
900
754
 
901
755
  // ../../packages/api/src/rpc/stats.ts
902
- import { Schema as Schema11 } from "effect";
756
+ import { Schema as Schema10 } from "effect";
903
757
  import { Rpc as Rpc3, RpcGroup as RpcGroup3 } from "effect/unstable/rpc";
904
758
 
905
759
  // ../../packages/api/src/schemas/stats.ts
906
- import { Effect as Effect5, Schema as Schema10 } from "effect";
907
- var scopeEnum = Schema10.Literals(["all", "me"]);
908
- var costStackByValueEnum = Schema10.Literals(["device", "harness", "model"]);
909
- var costStackByEnum = Schema10.optional(costStackByValueEnum);
910
- var usageDetailStackByEnum = Schema10.Literals(["device", "harness", "model"]);
911
- var stackByValueEnum = Schema10.Literals(["device", "harness"]);
912
- var stackByEnum = Schema10.optional(stackByValueEnum);
760
+ import { Effect as Effect4, Schema as Schema9 } from "effect";
761
+ var scopeEnum = Schema9.Literals(["all", "me"]);
762
+ var costStackByValueEnum = Schema9.Literals(["device", "harness", "model"]);
763
+ var costStackByEnum = Schema9.optional(costStackByValueEnum);
764
+ var usageDetailStackByEnum = Schema9.Literals(["device", "harness", "model"]);
765
+ var stackByValueEnum = Schema9.Literals(["device", "harness"]);
766
+ var stackByEnum = Schema9.optional(stackByValueEnum);
913
767
  var MAX_TIME_ZONE_LENGTH = 64;
914
768
  var DEFAULT_DAYS = 90;
915
769
  var MAX_DAYS = 365;
@@ -927,16 +781,16 @@ var MAX_LEADERBOARD_LIMIT = 50;
927
781
  var DEFAULT_LEADERBOARD_LIMIT = 10;
928
782
  var UUID_V7_VERSION3 = 7;
929
783
  var CIVIL_DATE_PATTERN2 = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/u;
930
- var timeZoneSchema = ianaTimezone.check(Schema10.isMaxLength(MAX_TIME_ZONE_LENGTH));
784
+ var timeZoneSchema = ianaTimezone.check(Schema9.isMaxLength(MAX_TIME_ZONE_LENGTH));
931
785
  var civilDateSchema = civilDate;
932
- var daysSchema = safeInteger.check(Schema10.isBetween({ minimum: MIN_LEADERBOARD_LIMIT, maximum: MAX_DAYS })).pipe(Schema10.withDecodingDefaultTypeKey(Effect5.succeed(DEFAULT_DAYS)));
933
- var leaderboardLimitSchema = safeInteger.check(Schema10.isBetween({ minimum: MIN_LEADERBOARD_LIMIT, maximum: MAX_LEADERBOARD_LIMIT })).pipe(Schema10.withDecodingDefaultTypeKey(Effect5.succeed(DEFAULT_LEADERBOARD_LIMIT)));
934
- var defaultTimeZoneSchema = timeZoneSchema.pipe(Schema10.withDecodingDefaultTypeKey(Effect5.succeed("UTC")));
935
- var uuidSchema = Schema10.String.check(Schema10.isUUID(UUID_V7_VERSION3));
786
+ var daysSchema = safeInteger.check(Schema9.isBetween({ minimum: MIN_LEADERBOARD_LIMIT, maximum: MAX_DAYS })).pipe(Schema9.withDecodingDefaultTypeKey(Effect4.succeed(DEFAULT_DAYS)));
787
+ var leaderboardLimitSchema = safeInteger.check(Schema9.isBetween({ minimum: MIN_LEADERBOARD_LIMIT, maximum: MAX_LEADERBOARD_LIMIT })).pipe(Schema9.withDecodingDefaultTypeKey(Effect4.succeed(DEFAULT_LEADERBOARD_LIMIT)));
788
+ var defaultTimeZoneSchema = timeZoneSchema.pipe(Schema9.withDecodingDefaultTypeKey(Effect4.succeed("UTC")));
789
+ var uuidSchema = Schema9.String.check(Schema9.isUUID(UUID_V7_VERSION3));
936
790
  var rangeFields = {
937
791
  days: daysSchema,
938
- endDate: Schema10.optional(civilDateSchema),
939
- startDate: Schema10.optional(civilDateSchema)
792
+ endDate: Schema9.optional(civilDateSchema),
793
+ startDate: Schema9.optional(civilDateSchema)
940
794
  };
941
795
  function validateDateRange(input) {
942
796
  if (!(input.startDate || input.endDate)) {
@@ -976,216 +830,216 @@ function parseCivilDateUtcMs(date) {
976
830
  }
977
831
  return parsed.getTime();
978
832
  }
979
- var dailyCostInput = Schema10.Struct({
833
+ var dailyCostInput = Schema9.Struct({
980
834
  scope: scopeEnum,
981
835
  stackBy: costStackByEnum,
982
836
  ...rangeFields,
983
837
  timeZone: defaultTimeZoneSchema
984
- }).check(Schema10.makeFilter(validateDateRange));
985
- var dailyPointSchema = Schema10.Struct({
838
+ }).check(Schema9.makeFilter(validateDateRange));
839
+ var dailyPointSchema = Schema9.Struct({
986
840
  /** Local civil date in YYYY-MM-DD for the requested stats timezone. */
987
841
  date: civilDateSchema,
988
842
  /** null when stackBy is undefined. */
989
- seriesName: Schema10.NullOr(Schema10.String),
843
+ seriesName: Schema9.NullOr(Schema9.String),
990
844
  /** USD; 0 or positive. */
991
- usd: Schema10.Number
845
+ usd: Schema9.Number
992
846
  });
993
- var dailyCostOutput = Schema10.Array(dailyPointSchema);
994
- var dailySessionsInput = Schema10.Struct({
847
+ var dailyCostOutput = Schema9.Array(dailyPointSchema);
848
+ var dailySessionsInput = Schema9.Struct({
995
849
  scope: scopeEnum,
996
850
  stackBy: stackByEnum,
997
851
  ...rangeFields,
998
852
  timeZone: defaultTimeZoneSchema
999
- }).check(Schema10.makeFilter(validateDateRange));
1000
- var dailySessionsPointSchema = Schema10.Struct({
853
+ }).check(Schema9.makeFilter(validateDateRange));
854
+ var dailySessionsPointSchema = Schema9.Struct({
1001
855
  date: civilDateSchema,
1002
- seriesName: Schema10.NullOr(Schema10.String),
856
+ seriesName: Schema9.NullOr(Schema9.String),
1003
857
  sessions: nonNegativeSafeInteger
1004
858
  });
1005
- var dailySessionsOutput = Schema10.Array(dailySessionsPointSchema);
859
+ var dailySessionsOutput = Schema9.Array(dailySessionsPointSchema);
1006
860
  var weekdaySessionsInput = dailySessionsInput;
1007
- var weekdaySessionsPointSchema = Schema10.Struct({
861
+ var weekdaySessionsPointSchema = Schema9.Struct({
1008
862
  /** Local ISO weekday for the requested stats timezone: 1=Monday, 7=Sunday. */
1009
- weekday: safeInteger.check(Schema10.isBetween({ minimum: MIN_ISO_WEEKDAY, maximum: MAX_ISO_WEEKDAY })),
1010
- seriesName: Schema10.NullOr(Schema10.String),
863
+ weekday: safeInteger.check(Schema9.isBetween({ minimum: MIN_ISO_WEEKDAY, maximum: MAX_ISO_WEEKDAY })),
864
+ seriesName: Schema9.NullOr(Schema9.String),
1011
865
  sessions: nonNegativeSafeInteger
1012
866
  });
1013
- var weekdaySessionsOutput = Schema10.Array(weekdaySessionsPointSchema);
867
+ var weekdaySessionsOutput = Schema9.Array(weekdaySessionsPointSchema);
1014
868
  var hourlySessionsInput = dailySessionsInput;
1015
- var hourlySessionsPointSchema = Schema10.Struct({
869
+ var hourlySessionsPointSchema = Schema9.Struct({
1016
870
  /** Local hour of day for the requested stats timezone: 0-23. */
1017
- hour: safeInteger.check(Schema10.isBetween({ minimum: MIN_HOUR_OF_DAY, maximum: MAX_HOUR_OF_DAY })),
1018
- seriesName: Schema10.NullOr(Schema10.String),
871
+ hour: safeInteger.check(Schema9.isBetween({ minimum: MIN_HOUR_OF_DAY, maximum: MAX_HOUR_OF_DAY })),
872
+ seriesName: Schema9.NullOr(Schema9.String),
1019
873
  sessions: nonNegativeSafeInteger
1020
874
  });
1021
- var hourlySessionsOutput = Schema10.Array(hourlySessionsPointSchema);
1022
- var summaryInput = Schema10.Struct({
875
+ var hourlySessionsOutput = Schema9.Array(hourlySessionsPointSchema);
876
+ var summaryInput = Schema9.Struct({
1023
877
  ...rangeFields,
1024
878
  scope: scopeEnum,
1025
879
  timeZone: defaultTimeZoneSchema
1026
- }).check(Schema10.makeFilter(validateDateRange));
1027
- var summaryOutput = Schema10.Struct({
1028
- rangeUsd: Schema10.Number.check(Schema10.isGreaterThanOrEqualTo(0)),
880
+ }).check(Schema9.makeFilter(validateDateRange));
881
+ var summaryOutput = Schema9.Struct({
882
+ rangeUsd: Schema9.Number.check(Schema9.isGreaterThanOrEqualTo(0)),
1029
883
  rangeSessions: nonNegativeSafeInteger,
1030
884
  rangeSubjectCount: nonNegativeSafeInteger,
1031
- totalUsd: Schema10.Number.check(Schema10.isGreaterThanOrEqualTo(0)),
885
+ totalUsd: Schema9.Number.check(Schema9.isGreaterThanOrEqualTo(0)),
1032
886
  totalSessions: nonNegativeSafeInteger
1033
887
  });
1034
- var deviceSummarySchema = Schema10.Struct({
888
+ var deviceSummarySchema = Schema9.Struct({
1035
889
  id: uuidSchema,
1036
- name: Schema10.String,
890
+ name: Schema9.String,
1037
891
  /** ISO-8601 UTC. */
1038
892
  lastSeen: isoUtcString
1039
893
  });
1040
- var devicesOutput = Schema10.Array(deviceSummarySchema);
1041
- var participantSyncSchema = Schema10.Struct({
894
+ var devicesOutput = Schema9.Array(deviceSummarySchema);
895
+ var participantSyncSchema = Schema9.Struct({
1042
896
  userId: uuidSchema,
1043
- userLabel: Schema10.String,
897
+ userLabel: Schema9.String,
1044
898
  /** ISO-8601 UTC, null when the participant has no synced devices yet. */
1045
- latestSyncAt: Schema10.NullOr(isoUtcString)
899
+ latestSyncAt: Schema9.NullOr(isoUtcString)
1046
900
  });
1047
- var participantsOutput = Schema10.Array(participantSyncSchema);
1048
- var leaderboardInput = Schema10.Struct({
901
+ var participantsOutput = Schema9.Array(participantSyncSchema);
902
+ var leaderboardInput = Schema9.Struct({
1049
903
  ...rangeFields,
1050
904
  limit: leaderboardLimitSchema,
1051
905
  timeZone: defaultTimeZoneSchema
1052
- }).check(Schema10.makeFilter(validateDateRange));
1053
- var leaderboardRowSchema = Schema10.Struct({
1054
- rank: safeInteger.check(Schema10.isGreaterThanOrEqualTo(1)),
906
+ }).check(Schema9.makeFilter(validateDateRange));
907
+ var leaderboardRowSchema = Schema9.Struct({
908
+ rank: safeInteger.check(Schema9.isGreaterThanOrEqualTo(1)),
1055
909
  userId: uuidSchema,
1056
- userLabel: Schema10.String,
1057
- usd: Schema10.Number.check(Schema10.isGreaterThanOrEqualTo(0)),
910
+ userLabel: Schema9.String,
911
+ usd: Schema9.Number.check(Schema9.isGreaterThanOrEqualTo(0)),
1058
912
  tokens: nonNegativeSafeInteger,
1059
913
  sessions: nonNegativeSafeInteger
1060
914
  });
1061
- var leaderboardOutput = Schema10.Array(leaderboardRowSchema);
915
+ var leaderboardOutput = Schema9.Array(leaderboardRowSchema);
1062
916
  var topDimensionsInput = leaderboardInput;
1063
- var topDimensionRowSchema = Schema10.Struct({
1064
- rank: safeInteger.check(Schema10.isGreaterThanOrEqualTo(1)),
1065
- name: Schema10.String,
1066
- usd: Schema10.Number.check(Schema10.isGreaterThanOrEqualTo(0)),
917
+ var topDimensionRowSchema = Schema9.Struct({
918
+ rank: safeInteger.check(Schema9.isGreaterThanOrEqualTo(1)),
919
+ name: Schema9.String,
920
+ usd: Schema9.Number.check(Schema9.isGreaterThanOrEqualTo(0)),
1067
921
  tokens: nonNegativeSafeInteger,
1068
- sessions: Schema10.NullOr(nonNegativeSafeInteger)
922
+ sessions: Schema9.NullOr(nonNegativeSafeInteger)
1069
923
  });
1070
- var topDimensionsOutput = Schema10.Struct({
1071
- harnesses: Schema10.Array(topDimensionRowSchema),
1072
- models: Schema10.Array(topDimensionRowSchema)
924
+ var topDimensionsOutput = Schema9.Struct({
925
+ harnesses: Schema9.Array(topDimensionRowSchema),
926
+ models: Schema9.Array(topDimensionRowSchema)
1073
927
  });
1074
- var usageDetailsInput = Schema10.Struct({
928
+ var usageDetailsInput = Schema9.Struct({
1075
929
  ...rangeFields,
1076
930
  stackBy: usageDetailStackByEnum,
1077
931
  timeZone: defaultTimeZoneSchema
1078
- }).check(Schema10.makeFilter(validateDateRange));
1079
- var usageDetailsRowSchema = Schema10.Struct({
1080
- id: Schema10.String,
1081
- name: Schema10.String,
932
+ }).check(Schema9.makeFilter(validateDateRange));
933
+ var usageDetailsRowSchema = Schema9.Struct({
934
+ id: Schema9.String,
935
+ name: Schema9.String,
1082
936
  tokens: nonNegativeSafeInteger,
1083
- sessions: Schema10.NullOr(nonNegativeSafeInteger),
1084
- usd: Schema10.Number.check(Schema10.isGreaterThanOrEqualTo(0)),
1085
- totalUsd: Schema10.Number.check(Schema10.isGreaterThanOrEqualTo(0))
937
+ sessions: Schema9.NullOr(nonNegativeSafeInteger),
938
+ usd: Schema9.Number.check(Schema9.isGreaterThanOrEqualTo(0)),
939
+ totalUsd: Schema9.Number.check(Schema9.isGreaterThanOrEqualTo(0))
1086
940
  });
1087
- var usageDetailsOutput = Schema10.Array(usageDetailsRowSchema);
941
+ var usageDetailsOutput = Schema9.Array(usageDetailsRowSchema);
1088
942
 
1089
943
  // ../../packages/api/src/rpc/stats.ts
1090
944
  var StatsRpcs = RpcGroup3.make(
1091
945
  Rpc3.make("stats.devices", {
1092
946
  success: devicesOutput,
1093
- error: Schema11.Union([DatabaseError, UnauthorizedError])
947
+ error: Schema10.Union([DatabaseError, UnauthorizedError])
1094
948
  }),
1095
949
  Rpc3.make("stats.participants", {
1096
950
  success: participantsOutput,
1097
- error: Schema11.Union([DatabaseError, UnauthorizedError])
951
+ error: Schema10.Union([DatabaseError, UnauthorizedError])
1098
952
  }),
1099
953
  Rpc3.make("stats.leaderboard", {
1100
954
  payload: leaderboardInput,
1101
955
  success: leaderboardOutput,
1102
- error: Schema11.Union([DatabaseError, UnauthorizedError])
956
+ error: Schema10.Union([DatabaseError, UnauthorizedError])
1103
957
  }),
1104
958
  Rpc3.make("stats.topDimensions", {
1105
959
  payload: topDimensionsInput,
1106
960
  success: topDimensionsOutput,
1107
- error: Schema11.Union([DatabaseError, UnauthorizedError])
961
+ error: Schema10.Union([DatabaseError, UnauthorizedError])
1108
962
  }),
1109
963
  Rpc3.make("stats.summary", {
1110
964
  payload: summaryInput,
1111
965
  success: summaryOutput,
1112
- error: Schema11.Union([DatabaseError, UnauthorizedError])
966
+ error: Schema10.Union([DatabaseError, UnauthorizedError])
1113
967
  }),
1114
968
  Rpc3.make("stats.dailyCost", {
1115
969
  payload: dailyCostInput,
1116
970
  success: dailyCostOutput,
1117
- error: Schema11.Union([DatabaseError, UnauthorizedError])
971
+ error: Schema10.Union([DatabaseError, UnauthorizedError])
1118
972
  }),
1119
973
  Rpc3.make("stats.dailySessions", {
1120
974
  payload: dailySessionsInput,
1121
975
  success: dailySessionsOutput,
1122
- error: Schema11.Union([DatabaseError, UnauthorizedError])
976
+ error: Schema10.Union([DatabaseError, UnauthorizedError])
1123
977
  }),
1124
978
  Rpc3.make("stats.weekdaySessions", {
1125
979
  payload: weekdaySessionsInput,
1126
980
  success: weekdaySessionsOutput,
1127
- error: Schema11.Union([DatabaseError, UnauthorizedError])
981
+ error: Schema10.Union([DatabaseError, UnauthorizedError])
1128
982
  }),
1129
983
  Rpc3.make("stats.hourlySessions", {
1130
984
  payload: hourlySessionsInput,
1131
985
  success: hourlySessionsOutput,
1132
- error: Schema11.Union([DatabaseError, UnauthorizedError])
986
+ error: Schema10.Union([DatabaseError, UnauthorizedError])
1133
987
  }),
1134
988
  Rpc3.make("stats.usageDetails", {
1135
989
  payload: usageDetailsInput,
1136
990
  success: usageDetailsOutput,
1137
- error: Schema11.Union([DatabaseError, UnauthorizedError])
991
+ error: Schema10.Union([DatabaseError, UnauthorizedError])
1138
992
  })
1139
993
  );
1140
994
 
1141
995
  // ../../packages/api/src/rpc/tokens.ts
1142
- import { Schema as Schema13 } from "effect";
996
+ import { Schema as Schema12 } from "effect";
1143
997
  import { Rpc as Rpc4, RpcGroup as RpcGroup4 } from "effect/unstable/rpc";
1144
998
 
1145
999
  // ../../packages/api/src/schemas/tokens.ts
1146
- import { Schema as Schema12 } from "effect";
1000
+ import { Schema as Schema11 } from "effect";
1147
1001
  var MIN_TOKEN_NAME_LENGTH = 1;
1148
1002
  var MAX_TOKEN_NAME_LENGTH = 80;
1149
- var createTokenInput = Schema12.Struct({
1150
- name: Schema12.String.check(Schema12.isMinLength(MIN_TOKEN_NAME_LENGTH), Schema12.isMaxLength(MAX_TOKEN_NAME_LENGTH))
1003
+ var createTokenInput = Schema11.Struct({
1004
+ name: Schema11.String.check(Schema11.isMinLength(MIN_TOKEN_NAME_LENGTH), Schema11.isMaxLength(MAX_TOKEN_NAME_LENGTH))
1151
1005
  });
1152
- var revokeTokenInput = Schema12.Struct({
1006
+ var revokeTokenInput = Schema11.Struct({
1153
1007
  id: uuidV7
1154
1008
  });
1155
- var tokenMetadata = Schema12.Struct({
1009
+ var tokenMetadata = Schema11.Struct({
1156
1010
  id: uuidV7,
1157
- name: Schema12.String,
1158
- prefix: Schema12.String,
1011
+ name: Schema11.String,
1012
+ prefix: Schema11.String,
1159
1013
  createdAt: isoUtcString,
1160
- lastUsedAt: Schema12.NullOr(isoUtcString),
1161
- revokedAt: Schema12.NullOr(isoUtcString)
1014
+ lastUsedAt: Schema11.NullOr(isoUtcString),
1015
+ revokedAt: Schema11.NullOr(isoUtcString)
1162
1016
  });
1163
- var listTokensOutput = Schema12.Struct({
1164
- tokens: Schema12.Array(tokenMetadata)
1017
+ var listTokensOutput = Schema11.Struct({
1018
+ tokens: Schema11.Array(tokenMetadata)
1165
1019
  });
1166
- var createTokenOutput = Schema12.Struct({
1167
- token: Schema12.String,
1020
+ var createTokenOutput = Schema11.Struct({
1021
+ token: Schema11.String,
1168
1022
  metadata: tokenMetadata
1169
1023
  });
1170
- var revokeTokenOutput = Schema12.Struct({
1171
- ok: Schema12.Boolean
1024
+ var revokeTokenOutput = Schema11.Struct({
1025
+ ok: Schema11.Boolean
1172
1026
  });
1173
1027
 
1174
1028
  // ../../packages/api/src/rpc/tokens.ts
1175
1029
  var TokensRpcs = RpcGroup4.make(
1176
1030
  Rpc4.make("tokens.list", {
1177
1031
  success: listTokensOutput,
1178
- error: Schema13.Union([DatabaseError, UnauthorizedError])
1032
+ error: Schema12.Union([DatabaseError, UnauthorizedError])
1179
1033
  }),
1180
1034
  Rpc4.make("tokens.create", {
1181
1035
  payload: createTokenInput,
1182
1036
  success: createTokenOutput,
1183
- error: Schema13.Union([DatabaseError, UnauthorizedError])
1037
+ error: Schema12.Union([DatabaseError, UnauthorizedError])
1184
1038
  }),
1185
1039
  Rpc4.make("tokens.revoke", {
1186
1040
  payload: revokeTokenInput,
1187
1041
  success: revokeTokenOutput,
1188
- error: Schema13.Union([DatabaseError, NotFoundError, UnauthorizedError])
1042
+ error: Schema12.Union([DatabaseError, NotFoundError, UnauthorizedError])
1189
1043
  })
1190
1044
  );
1191
1045
 
@@ -1193,11 +1047,11 @@ var TokensRpcs = RpcGroup4.make(
1193
1047
  var WebRpcs = AuthRpcs.merge(StatsRpcs).merge(TokensRpcs).merge(DevicesRpcs);
1194
1048
 
1195
1049
  // ../../packages/api/src/rpc/sync.ts
1196
- import { Schema as Schema15 } from "effect";
1050
+ import { Schema as Schema14 } from "effect";
1197
1051
  import { Rpc as Rpc5, RpcGroup as RpcGroup5 } from "effect/unstable/rpc";
1198
1052
 
1199
1053
  // ../../packages/api/src/schemas/sync.ts
1200
- import { Effect as Effect6, Schema as Schema14 } from "effect";
1054
+ import { Effect as Effect5, Schema as Schema13 } from "effect";
1201
1055
  var MIN_DEVICE_NAME_LENGTH2 = 1;
1202
1056
  var MAX_DEVICE_NAME_LENGTH2 = 80;
1203
1057
  var MIN_MODEL_LENGTH = 1;
@@ -1208,28 +1062,31 @@ var MAX_BUCKETS_PER_SYNC = 5e3;
1208
1062
  var MAX_SESSIONS_PER_SYNC = 5e3;
1209
1063
  var MAX_BUCKET_SESSION_COUNT = 1e3;
1210
1064
  var MAX_BUCKET_TOKEN_COUNT = null;
1065
+ var MAX_REPLACEMENT_BATCHES = 1e4;
1066
+ var MAX_REPLACEMENT_FACTS = 1e5;
1211
1067
  var HARNESSES = ["codex", "claude_code", "pi", "hermes", "opencode"];
1212
- var harnessEnum = Schema14.Literals(HARNESSES);
1213
- var deviceNameSchema2 = Schema14.String.check(
1214
- Schema14.isMinLength(MIN_DEVICE_NAME_LENGTH2),
1215
- Schema14.isMaxLength(MAX_DEVICE_NAME_LENGTH2)
1068
+ var MAX_REPLACEMENT_HARNESSES = HARNESSES.length;
1069
+ var harnessEnum = Schema13.Literals(HARNESSES);
1070
+ var deviceNameSchema2 = Schema13.String.check(
1071
+ Schema13.isMinLength(MIN_DEVICE_NAME_LENGTH2),
1072
+ Schema13.isMaxLength(MAX_DEVICE_NAME_LENGTH2)
1216
1073
  );
1217
- var getWatermarkInput = Schema14.Struct({
1074
+ var getWatermarkInput = Schema13.Struct({
1218
1075
  device: deviceNameSchema2
1219
1076
  });
1220
- var getWatermarkOutput = Schema14.Struct({
1221
- watermarkHourUtc: Schema14.NullOr(isoUtcString),
1222
- sessionKeySalt: hex64.check(Schema14.isLengthBetween(SESSION_KEY_SALT_LENGTH, SESSION_KEY_SALT_LENGTH))
1077
+ var getWatermarkOutput = Schema13.Struct({
1078
+ watermarkHourUtc: Schema13.NullOr(isoUtcString),
1079
+ sessionKeySalt: hex64.check(Schema13.isLengthBetween(SESSION_KEY_SALT_LENGTH, SESSION_KEY_SALT_LENGTH))
1223
1080
  });
1224
1081
  function boundedInteger(max) {
1225
1082
  if (max === null) {
1226
1083
  return nonNegativeSafeInteger;
1227
1084
  }
1228
- return nonNegativeSafeInteger.check(Schema14.isLessThanOrEqualTo(max));
1085
+ return nonNegativeSafeInteger.check(Schema13.isLessThanOrEqualTo(max));
1229
1086
  }
1230
- var bucketInput = Schema14.Struct({
1087
+ var bucketInput = Schema13.Struct({
1231
1088
  harness: harnessEnum,
1232
- model: Schema14.String.check(Schema14.isMinLength(MIN_MODEL_LENGTH), Schema14.isMaxLength(MAX_MODEL_LENGTH)),
1089
+ model: Schema13.String.check(Schema13.isMinLength(MIN_MODEL_LENGTH), Schema13.isMaxLength(MAX_MODEL_LENGTH)),
1233
1090
  hourUtc: isoUtcString,
1234
1091
  inputTokens: boundedInteger(MAX_BUCKET_TOKEN_COUNT),
1235
1092
  outputTokens: boundedInteger(MAX_BUCKET_TOKEN_COUNT),
@@ -1237,149 +1094,458 @@ var bucketInput = Schema14.Struct({
1237
1094
  cacheCreateTokens: boundedInteger(MAX_BUCKET_TOKEN_COUNT),
1238
1095
  sessionCount: boundedInteger(MAX_BUCKET_SESSION_COUNT)
1239
1096
  });
1240
- var sessionInput = Schema14.Struct({
1097
+ var sessionInput = Schema13.Struct({
1241
1098
  harness: harnessEnum,
1242
- sessionKey: hex64.check(Schema14.isLengthBetween(SESSION_KEY_LENGTH, SESSION_KEY_LENGTH)),
1099
+ sessionKey: hex64.check(Schema13.isLengthBetween(SESSION_KEY_LENGTH, SESSION_KEY_LENGTH)),
1243
1100
  startedAt: isoUtcString,
1244
1101
  lastSeenAt: isoUtcString
1245
1102
  });
1246
- var upsertBucketsInput = Schema14.Struct({
1103
+ var upsertBucketsInput = Schema13.Struct({
1247
1104
  device: deviceNameSchema2,
1248
- buckets: Schema14.Array(bucketInput).check(Schema14.isMaxLength(MAX_BUCKETS_PER_SYNC)),
1249
- sessions: Schema14.Array(sessionInput).check(Schema14.isMaxLength(MAX_SESSIONS_PER_SYNC)).pipe(Schema14.withDecodingDefaultTypeKey(Effect6.succeed([])))
1105
+ buckets: Schema13.Array(bucketInput).check(Schema13.isMaxLength(MAX_BUCKETS_PER_SYNC)),
1106
+ sessions: Schema13.Array(sessionInput).check(Schema13.isMaxLength(MAX_SESSIONS_PER_SYNC)).pipe(Schema13.withDecodingDefaultTypeKey(Effect5.succeed([])))
1250
1107
  });
1251
- var upsertBucketsOutput = Schema14.Struct({
1108
+ var upsertBucketsOutput = Schema13.Struct({
1252
1109
  upserted: boundedInteger(null),
1253
1110
  upsertedSessions: boundedInteger(null)
1254
1111
  });
1112
+ var replacementId = uuidV7;
1113
+ var createUsageReplacementInput = Schema13.Struct({
1114
+ device: deviceNameSchema2,
1115
+ startHourUtc: isoUtcString,
1116
+ endHourUtc: isoUtcString,
1117
+ harnesses: Schema13.Array(harnessEnum).check(Schema13.isMinLength(1), Schema13.isMaxLength(MAX_REPLACEMENT_HARNESSES))
1118
+ });
1119
+ var createUsageReplacementOutput = Schema13.Struct({
1120
+ replacementId,
1121
+ startHourUtc: isoUtcString,
1122
+ endHourUtc: isoUtcString,
1123
+ harnesses: Schema13.Array(harnessEnum),
1124
+ expiresAt: isoUtcString
1125
+ });
1126
+ var stageUsageReplacementBatchInput = Schema13.Struct({
1127
+ device: deviceNameSchema2,
1128
+ replacementId,
1129
+ batchIndex: boundedInteger(MAX_REPLACEMENT_BATCHES),
1130
+ batchDigest: hex64,
1131
+ buckets: Schema13.Array(bucketInput).check(Schema13.isMaxLength(MAX_BUCKETS_PER_SYNC)),
1132
+ sessions: Schema13.Array(sessionInput).check(Schema13.isMaxLength(MAX_SESSIONS_PER_SYNC))
1133
+ });
1134
+ var stageUsageReplacementBatchOutput = Schema13.Struct({
1135
+ stagedBuckets: boundedInteger(MAX_BUCKETS_PER_SYNC),
1136
+ stagedSessions: boundedInteger(MAX_SESSIONS_PER_SYNC),
1137
+ alreadyStaged: Schema13.Boolean
1138
+ });
1139
+ var finalizeUsageReplacementInput = Schema13.Struct({
1140
+ device: deviceNameSchema2,
1141
+ replacementId,
1142
+ expectedBatchCount: boundedInteger(MAX_REPLACEMENT_BATCHES),
1143
+ expectedBucketCount: boundedInteger(MAX_REPLACEMENT_FACTS),
1144
+ expectedSessionCount: boundedInteger(MAX_REPLACEMENT_FACTS),
1145
+ digest: hex64
1146
+ });
1147
+ var finalizeUsageReplacementOutput = Schema13.Struct({
1148
+ replacementId,
1149
+ committedAt: isoUtcString,
1150
+ replacedBuckets: boundedInteger(MAX_REPLACEMENT_FACTS),
1151
+ replacedSessions: boundedInteger(MAX_REPLACEMENT_FACTS),
1152
+ digest: hex64
1153
+ });
1255
1154
 
1256
1155
  // ../../packages/api/src/rpc/sync.ts
1156
+ var replacementErrors = Schema14.Union([
1157
+ ConflictError,
1158
+ DatabaseError,
1159
+ NotFoundError,
1160
+ SyncQuotaExceededError,
1161
+ SyncTimestampRejectedError,
1162
+ UnauthorizedError,
1163
+ ValidationError
1164
+ ]);
1257
1165
  var SyncRpcs = RpcGroup5.make(
1258
1166
  Rpc5.make("sync.getWatermark", {
1259
1167
  payload: getWatermarkInput,
1260
1168
  success: getWatermarkOutput,
1261
- error: Schema15.Union([DatabaseError, UnauthorizedError])
1169
+ error: Schema14.Union([DatabaseError, UnauthorizedError])
1262
1170
  }),
1263
1171
  Rpc5.make("sync.upsertBuckets", {
1264
1172
  payload: upsertBucketsInput,
1265
1173
  success: upsertBucketsOutput,
1266
- error: Schema15.Union([DatabaseError, SyncQuotaExceededError, SyncTimestampRejectedError, UnauthorizedError])
1174
+ error: Schema14.Union([DatabaseError, SyncQuotaExceededError, SyncTimestampRejectedError, UnauthorizedError])
1175
+ }),
1176
+ Rpc5.make("sync.createUsageReplacement", {
1177
+ payload: createUsageReplacementInput,
1178
+ success: createUsageReplacementOutput,
1179
+ error: replacementErrors
1180
+ }),
1181
+ Rpc5.make("sync.stageUsageReplacementBatch", {
1182
+ payload: stageUsageReplacementBatchInput,
1183
+ success: stageUsageReplacementBatchOutput,
1184
+ error: replacementErrors
1185
+ }),
1186
+ Rpc5.make("sync.finalizeUsageReplacement", {
1187
+ payload: finalizeUsageReplacementInput,
1188
+ success: finalizeUsageReplacementOutput,
1189
+ error: replacementErrors
1267
1190
  })
1268
1191
  );
1269
1192
 
1270
- // src/sync/client.ts
1271
- import { Context as Context4, Effect as Effect7, Layer as Layer3 } from "effect";
1272
- import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
1273
- import { RpcClient, RpcSerialization } from "effect/unstable/rpc";
1274
- var AUTHORIZATION_HEADER = "authorization";
1275
- var SYNC_RPC_PATH = "/rpc/sync";
1276
- var SyncRpcClient = class extends Context4.Service()("@oopst/cli/SyncRpcClient") {
1277
- };
1278
- function syncRpcClientFromPromiseClient(client) {
1279
- const getWatermark = Effect7.fn("SyncRpcClient.fromPromise.getWatermark")(
1280
- (input) => Effect7.tryPromise({
1281
- try: () => client.sync.getWatermark.mutate(input),
1282
- catch: (err) => err
1283
- })
1284
- );
1285
- const upsertBuckets = Effect7.fn("SyncRpcClient.fromPromise.upsertBuckets")(
1286
- (input) => Effect7.tryPromise({
1287
- try: () => client.sync.upsertBuckets.mutate(input),
1288
- catch: (err) => err
1289
- })
1290
- );
1291
- return SyncRpcClient.of({ getWatermark, upsertBuckets });
1292
- }
1293
- function makeSyncRpcClientLayer(options) {
1294
- return Layer3.effect(
1295
- SyncRpcClient,
1296
- Effect7.gen(function* () {
1297
- const client = yield* RpcClient.make(SyncRpcs);
1298
- const getWatermark = Effect7.fn("SyncRpcClient.getWatermark")(
1299
- (input) => client["sync.getWatermark"](input)
1300
- );
1301
- const upsertBuckets = Effect7.fn("SyncRpcClient.upsertBuckets")(
1302
- (input) => client["sync.upsertBuckets"](input)
1303
- );
1304
- return SyncRpcClient.of({ getWatermark, upsertBuckets });
1305
- })
1306
- ).pipe(Layer3.provide(makeClientLive(options)));
1307
- }
1308
- function makeClientLive(options) {
1309
- let clientLive = RpcClient.layerProtocolHttp({
1310
- url: stripTrailingSlash(options.serverUrl),
1311
- transformClient: HttpClient.mapRequest(
1312
- (request) => request.pipe(
1313
- HttpClientRequest.appendUrl(SYNC_RPC_PATH),
1314
- HttpClientRequest.setHeader(AUTHORIZATION_HEADER, `Bearer ${options.token}`)
1315
- )
1193
+ // ../../packages/api/src/sync-replacement-canonical.ts
1194
+ function normalizedIso(value) {
1195
+ return new Date(value).toISOString();
1196
+ }
1197
+ function canonicalReplacementLines(facts) {
1198
+ const lines = [
1199
+ ...facts.buckets.map(
1200
+ (bucket) => JSON.stringify([
1201
+ "bucket",
1202
+ bucket.harness,
1203
+ bucket.model,
1204
+ normalizedIso(bucket.hourUtc),
1205
+ bucket.inputTokens,
1206
+ bucket.outputTokens,
1207
+ bucket.cacheReadTokens,
1208
+ bucket.cacheCreateTokens,
1209
+ bucket.sessionCount
1210
+ ])
1211
+ ),
1212
+ ...facts.sessions.map(
1213
+ (session) => JSON.stringify([
1214
+ "session",
1215
+ session.harness,
1216
+ session.sessionKey,
1217
+ normalizedIso(session.startedAt),
1218
+ normalizedIso(session.lastSeenAt)
1219
+ ])
1316
1220
  )
1317
- }).pipe(Layer3.provide(RpcSerialization.layerJson), Layer3.provide(FetchHttpClient.layer));
1318
- if (options.fetch) {
1319
- clientLive = clientLive.pipe(Layer3.provide(Layer3.succeed(FetchHttpClient.Fetch, options.fetch)));
1320
- }
1321
- return clientLive;
1221
+ ];
1222
+ return lines.sort();
1322
1223
  }
1323
- function stripTrailingSlash(url) {
1324
- return url.endsWith("/") ? url.slice(0, -1) : url;
1224
+ function canonicalReplacementPayload(facts) {
1225
+ return canonicalReplacementLines(facts).join("\n");
1325
1226
  }
1326
1227
 
1327
- // src/sync/errors.ts
1328
- import { Schema as Schema16 } from "effect";
1329
- var SUCCESS_EXIT_CODE = 0;
1330
- var ZERO_BUCKETS_EXIT_CODE = 1;
1331
- var UNAUTHORIZED_EXIT_CODE = 2;
1332
- var NETWORK_ERROR_EXIT_CODE = 3;
1333
- var INVALID_ARGUMENTS_EXIT_CODE = 4;
1334
- var GENERAL_FAILURE_EXIT_CODE = 5;
1335
- var EXIT_CODES = [
1336
- SUCCESS_EXIT_CODE,
1337
- ZERO_BUCKETS_EXIT_CODE,
1338
- UNAUTHORIZED_EXIT_CODE,
1339
- NETWORK_ERROR_EXIT_CODE,
1340
- INVALID_ARGUMENTS_EXIT_CODE,
1341
- GENERAL_FAILURE_EXIT_CODE
1342
- ];
1343
- var EXIT = {
1344
- success: SUCCESS_EXIT_CODE,
1345
- zeroBucketsButSourcesExist: ZERO_BUCKETS_EXIT_CODE,
1346
- unauthorized: UNAUTHORIZED_EXIT_CODE,
1347
- networkError: NETWORK_ERROR_EXIT_CODE,
1348
- invalidArguments: INVALID_ARGUMENTS_EXIT_CODE,
1349
- generalFailure: GENERAL_FAILURE_EXIT_CODE
1228
+ // src/sync/run.ts
1229
+ import { Effect as Effect9, Stream as Stream4 } from "effect";
1230
+
1231
+ // src/sync/aggregate.ts
1232
+ import { createHmac } from "crypto";
1233
+ import { Effect as Effect6, Schema as Schema15, Stream as Stream2 } from "effect";
1234
+ var UsageAggregationErrorFields = {
1235
+ message: Schema15.String,
1236
+ cause: Schema15.optionalKey(Schema15.Defect())
1350
1237
  };
1351
- var SyncError = class extends Schema16.TaggedErrorClass()("SyncError", {
1352
- message: Schema16.String,
1353
- exitCode: Schema16.Literals(EXIT_CODES),
1354
- cause: Schema16.optionalKey(Schema16.Defect())
1355
- }) {
1356
- constructor(exitCode, message, options = {}) {
1357
- super(
1358
- options.cause === void 0 ? { message, exitCode } : { message, exitCode, cause: options.cause }
1359
- );
1360
- }
1238
+ var UsageAggregationError = class extends Schema15.TaggedErrorClass()(
1239
+ "UsageAggregationError",
1240
+ UsageAggregationErrorFields
1241
+ ) {
1361
1242
  };
1362
- function isUnauthorizedError(err) {
1363
- if (err instanceof UnauthorizedError) {
1364
- return true;
1365
- }
1366
- if (typeof err !== "object" || err === null) {
1367
- return false;
1368
- }
1369
- const candidate = err;
1370
- return candidate._tag === "UnauthorizedError" || candidate.data?.code === "UNAUTHORIZED";
1243
+ function toIso(hourUtc) {
1244
+ return hourUtc instanceof Date ? hourUtc.toISOString() : hourUtc;
1371
1245
  }
1372
- var NETWORK_ERROR_CODES = ["ECONNREFUSED", "ENOTFOUND", "ECONNRESET", "EAI_AGAIN", "ETIMEDOUT"];
1373
- var NETWORK_ERROR_PATTERN = /fetch failed|network|ECONN|ENOTFOUND|ETIMEDOUT/iu;
1374
- var NETWORK_HTTP_ERROR_KINDS = ["InvalidUrlError", "TransportError"];
1375
- function isNetworkError(err) {
1376
- if (typeof err !== "object" || err === null) {
1377
- return false;
1378
- }
1379
- const rpcReason = err.reason;
1380
- if (rpcReason?._tag === "HttpError" && rpcReason.kind && NETWORK_HTTP_ERROR_KINDS.includes(rpcReason.kind)) {
1381
- return true;
1382
- }
1246
+ function key(b) {
1247
+ return `${b.harness} ${b.model} ${toIso(b.hourUtc)}`;
1248
+ }
1249
+ function finalize(acc) {
1250
+ return {
1251
+ harness: acc.harness,
1252
+ model: acc.model,
1253
+ hourUtc: acc.hourUtc,
1254
+ inputTokens: acc.inputTokens,
1255
+ outputTokens: acc.outputTokens,
1256
+ cacheReadTokens: acc.cacheReadTokens,
1257
+ cacheCreateTokens: acc.cacheCreateTokens,
1258
+ sessionCount: acc.sessionIds.size
1259
+ };
1260
+ }
1261
+ var SESSION_KEY_SALT_PATTERN = /^[a-f0-9]{64}$/u;
1262
+ function createSessionKey(harness, localSessionId, sessionKeySalt) {
1263
+ if (!SESSION_KEY_SALT_PATTERN.test(sessionKeySalt)) {
1264
+ throw new Error("invalid session key salt");
1265
+ }
1266
+ return createHmac("sha256", Buffer.from(sessionKeySalt, "hex")).update("oopst/session-key/v1").update("\0").update(harness).update("\0").update(localSessionId).digest("hex");
1267
+ }
1268
+ function sessionMapKey(harness, sessionKey) {
1269
+ return `${harness} ${sessionKey}`;
1270
+ }
1271
+ function finalizeSession(acc) {
1272
+ return {
1273
+ harness: acc.harness,
1274
+ sessionKey: acc.sessionKey,
1275
+ startedAt: acc.startedAt.toISOString(),
1276
+ lastSeenAt: acc.lastSeenAt.toISOString()
1277
+ };
1278
+ }
1279
+ function addSessionAccumulator(sessionAccumulators, bucket, localSessionId, sessionKeySalt) {
1280
+ const sessionKey = createSessionKey(bucket.harness, localSessionId, sessionKeySalt);
1281
+ const sessionAccumulatorKey = sessionMapKey(bucket.harness, sessionKey);
1282
+ const existingSession = sessionAccumulators.get(sessionAccumulatorKey);
1283
+ if (existingSession) {
1284
+ if (bucket.sessionStartedAt.getTime() < existingSession.startedAt.getTime()) {
1285
+ existingSession.startedAt = bucket.sessionStartedAt;
1286
+ }
1287
+ if (bucket.sessionLastSeenAt.getTime() > existingSession.lastSeenAt.getTime()) {
1288
+ existingSession.lastSeenAt = bucket.sessionLastSeenAt;
1289
+ }
1290
+ return;
1291
+ }
1292
+ sessionAccumulators.set(sessionAccumulatorKey, {
1293
+ harness: bucket.harness,
1294
+ sessionKey,
1295
+ startedAt: bucket.sessionStartedAt,
1296
+ lastSeenAt: bucket.sessionLastSeenAt
1297
+ });
1298
+ }
1299
+ function createUsageAggregationState() {
1300
+ return {
1301
+ accumulators: /* @__PURE__ */ new Map(),
1302
+ sessionAccumulators: /* @__PURE__ */ new Map()
1303
+ };
1304
+ }
1305
+ function addBucketToAggregationState(state, bucket, sessionKeySalt, replacementRange) {
1306
+ const includeBucket = isReplacementBucketEligible(bucket, replacementRange);
1307
+ if (includeBucket) {
1308
+ const k = key(bucket);
1309
+ let acc = state.accumulators.get(k);
1310
+ if (!acc) {
1311
+ acc = {
1312
+ harness: bucket.harness,
1313
+ model: bucket.model,
1314
+ hourUtc: toIso(bucket.hourUtc),
1315
+ inputTokens: 0,
1316
+ outputTokens: 0,
1317
+ cacheReadTokens: 0,
1318
+ cacheCreateTokens: 0,
1319
+ sessionIds: /* @__PURE__ */ new Set()
1320
+ };
1321
+ state.accumulators.set(k, acc);
1322
+ }
1323
+ acc.inputTokens += bucket.inputTokens;
1324
+ acc.outputTokens += bucket.outputTokens;
1325
+ acc.cacheReadTokens += bucket.cacheReadTokens;
1326
+ acc.cacheCreateTokens += bucket.cacheCreateTokens;
1327
+ for (const id of bucket.sessionIds) {
1328
+ acc.sessionIds.add(id);
1329
+ }
1330
+ }
1331
+ if (isReplacementSessionEligible(bucket, replacementRange)) {
1332
+ for (const id of bucket.sessionIds) {
1333
+ addSessionAccumulator(state.sessionAccumulators, bucket, id, sessionKeySalt);
1334
+ }
1335
+ }
1336
+ return state;
1337
+ }
1338
+ function isReplacementBucketEligible(bucket, range) {
1339
+ if (!range) {
1340
+ return true;
1341
+ }
1342
+ const hourMs = bucket.hourUtc.getTime();
1343
+ return range.harnesses.has(bucket.harness) && hourMs >= range.startHourUtc.getTime() && hourMs < range.endHourUtc.getTime();
1344
+ }
1345
+ function isReplacementSessionEligible(bucket, range) {
1346
+ if (!range) {
1347
+ return true;
1348
+ }
1349
+ return range.harnesses.has(bucket.harness) && bucket.sessionStartedAt.getTime() < range.endHourUtc.getTime() && bucket.sessionLastSeenAt.getTime() >= range.startHourUtc.getTime();
1350
+ }
1351
+ function addBucketToAggregationStateEffect(state, bucket, sessionKeySalt, replacementRange) {
1352
+ return Effect6.try({
1353
+ try: () => addBucketToAggregationState(state, bucket, sessionKeySalt, replacementRange),
1354
+ catch: toUsageAggregationError
1355
+ });
1356
+ }
1357
+ function finalizeUsageBatches(state, batchSize) {
1358
+ const finalized = [];
1359
+ for (const acc of state.accumulators.values()) {
1360
+ finalized.push(finalize(acc));
1361
+ }
1362
+ const finalizedSessions = [];
1363
+ for (const acc of state.sessionAccumulators.values()) {
1364
+ finalizedSessions.push(finalizeSession(acc));
1365
+ }
1366
+ const batches = [];
1367
+ const maxLength = Math.max(finalized.length, finalizedSessions.length);
1368
+ for (let i = 0; i < maxLength; i += batchSize) {
1369
+ batches.push({
1370
+ buckets: finalized.slice(i, i + batchSize),
1371
+ sessions: finalizedSessions.slice(i, i + batchSize)
1372
+ });
1373
+ }
1374
+ return batches;
1375
+ }
1376
+ function toUsageAggregationError(cause) {
1377
+ return new UsageAggregationError({
1378
+ message: `usage aggregation failed: ${errorMessage(cause)}`,
1379
+ cause
1380
+ });
1381
+ }
1382
+ function errorMessage(cause) {
1383
+ return cause instanceof Error ? cause.message : "unknown aggregation error";
1384
+ }
1385
+ function aggregateUsageStream(source, options) {
1386
+ return Stream2.fromIterableEffect(
1387
+ source.pipe(
1388
+ Stream2.runFoldEffect(
1389
+ () => createUsageAggregationState(),
1390
+ (state, bucket) => addBucketToAggregationStateEffect(state, bucket, options.sessionKeySalt, options.replacementRange)
1391
+ ),
1392
+ Effect6.map((state) => finalizeUsageBatches(state, options.batchSize))
1393
+ )
1394
+ );
1395
+ }
1396
+
1397
+ // src/sync/client.ts
1398
+ import { Context as Context4, Effect as Effect7, Layer as Layer3 } from "effect";
1399
+ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
1400
+ import { RpcClient, RpcSerialization } from "effect/unstable/rpc";
1401
+ var AUTHORIZATION_HEADER = "authorization";
1402
+ var SYNC_RPC_PATH = "/rpc/sync";
1403
+ var SyncRpcClient = class extends Context4.Service()("@oopst/cli/SyncRpcClient") {
1404
+ };
1405
+ function syncRpcClientFromPromiseClient(client) {
1406
+ const getWatermark = Effect7.fn("SyncRpcClient.fromPromise.getWatermark")(
1407
+ (input) => Effect7.tryPromise({
1408
+ try: () => client.sync.getWatermark.mutate(input),
1409
+ catch: (err) => err
1410
+ })
1411
+ );
1412
+ const upsertBuckets = Effect7.fn("SyncRpcClient.fromPromise.upsertBuckets")(
1413
+ (input) => Effect7.tryPromise({
1414
+ try: () => client.sync.upsertBuckets.mutate(input),
1415
+ catch: (err) => err
1416
+ })
1417
+ );
1418
+ const createUsageReplacement = Effect7.fn("SyncRpcClient.fromPromise.createUsageReplacement")(
1419
+ (input) => Effect7.tryPromise({
1420
+ try: () => client.sync.createUsageReplacement.mutate(input),
1421
+ catch: (err) => err
1422
+ })
1423
+ );
1424
+ const stageUsageReplacementBatch = Effect7.fn("SyncRpcClient.fromPromise.stageUsageReplacementBatch")(
1425
+ (input) => Effect7.tryPromise({
1426
+ try: () => client.sync.stageUsageReplacementBatch.mutate(input),
1427
+ catch: (err) => err
1428
+ })
1429
+ );
1430
+ const finalizeUsageReplacement = Effect7.fn("SyncRpcClient.fromPromise.finalizeUsageReplacement")(
1431
+ (input) => Effect7.tryPromise({
1432
+ try: () => client.sync.finalizeUsageReplacement.mutate(input),
1433
+ catch: (err) => err
1434
+ })
1435
+ );
1436
+ return SyncRpcClient.of({
1437
+ createUsageReplacement,
1438
+ finalizeUsageReplacement,
1439
+ getWatermark,
1440
+ stageUsageReplacementBatch,
1441
+ upsertBuckets
1442
+ });
1443
+ }
1444
+ function makeSyncRpcClientLayer(options) {
1445
+ return Layer3.effect(
1446
+ SyncRpcClient,
1447
+ Effect7.gen(function* () {
1448
+ const client = yield* RpcClient.make(SyncRpcs);
1449
+ const getWatermark = Effect7.fn("SyncRpcClient.getWatermark")(
1450
+ (input) => client["sync.getWatermark"](input)
1451
+ );
1452
+ const upsertBuckets = Effect7.fn("SyncRpcClient.upsertBuckets")(
1453
+ (input) => client["sync.upsertBuckets"](input)
1454
+ );
1455
+ const createUsageReplacement = Effect7.fn("SyncRpcClient.createUsageReplacement")(
1456
+ (input) => client["sync.createUsageReplacement"](input)
1457
+ );
1458
+ const stageUsageReplacementBatch = Effect7.fn("SyncRpcClient.stageUsageReplacementBatch")(
1459
+ (input) => client["sync.stageUsageReplacementBatch"](input)
1460
+ );
1461
+ const finalizeUsageReplacement = Effect7.fn("SyncRpcClient.finalizeUsageReplacement")(
1462
+ (input) => client["sync.finalizeUsageReplacement"](input)
1463
+ );
1464
+ return SyncRpcClient.of({
1465
+ createUsageReplacement,
1466
+ finalizeUsageReplacement,
1467
+ getWatermark,
1468
+ stageUsageReplacementBatch,
1469
+ upsertBuckets
1470
+ });
1471
+ })
1472
+ ).pipe(Layer3.provide(makeClientLive(options)));
1473
+ }
1474
+ function makeClientLive(options) {
1475
+ let clientLive = RpcClient.layerProtocolHttp({
1476
+ url: stripTrailingSlash(options.serverUrl),
1477
+ transformClient: HttpClient.mapRequest(
1478
+ (request) => request.pipe(
1479
+ HttpClientRequest.appendUrl(SYNC_RPC_PATH),
1480
+ HttpClientRequest.setHeader(AUTHORIZATION_HEADER, `Bearer ${options.token}`)
1481
+ )
1482
+ )
1483
+ }).pipe(Layer3.provide(RpcSerialization.layerJson), Layer3.provide(FetchHttpClient.layer));
1484
+ if (options.fetch) {
1485
+ clientLive = clientLive.pipe(Layer3.provide(Layer3.succeed(FetchHttpClient.Fetch, options.fetch)));
1486
+ }
1487
+ return clientLive;
1488
+ }
1489
+ function stripTrailingSlash(url) {
1490
+ return url.endsWith("/") ? url.slice(0, -1) : url;
1491
+ }
1492
+
1493
+ // src/sync/errors.ts
1494
+ import { Schema as Schema16 } from "effect";
1495
+ var SUCCESS_EXIT_CODE = 0;
1496
+ var ZERO_BUCKETS_EXIT_CODE = 1;
1497
+ var UNAUTHORIZED_EXIT_CODE = 2;
1498
+ var NETWORK_ERROR_EXIT_CODE = 3;
1499
+ var INVALID_ARGUMENTS_EXIT_CODE = 4;
1500
+ var GENERAL_FAILURE_EXIT_CODE = 5;
1501
+ var EXIT_CODES = [
1502
+ SUCCESS_EXIT_CODE,
1503
+ ZERO_BUCKETS_EXIT_CODE,
1504
+ UNAUTHORIZED_EXIT_CODE,
1505
+ NETWORK_ERROR_EXIT_CODE,
1506
+ INVALID_ARGUMENTS_EXIT_CODE,
1507
+ GENERAL_FAILURE_EXIT_CODE
1508
+ ];
1509
+ var EXIT = {
1510
+ success: SUCCESS_EXIT_CODE,
1511
+ zeroBucketsButSourcesExist: ZERO_BUCKETS_EXIT_CODE,
1512
+ unauthorized: UNAUTHORIZED_EXIT_CODE,
1513
+ networkError: NETWORK_ERROR_EXIT_CODE,
1514
+ invalidArguments: INVALID_ARGUMENTS_EXIT_CODE,
1515
+ generalFailure: GENERAL_FAILURE_EXIT_CODE
1516
+ };
1517
+ var SyncError = class extends Schema16.TaggedErrorClass()("SyncError", {
1518
+ message: Schema16.String,
1519
+ exitCode: Schema16.Literals(EXIT_CODES),
1520
+ cause: Schema16.optionalKey(Schema16.Defect())
1521
+ }) {
1522
+ constructor(exitCode, message, options = {}) {
1523
+ super(
1524
+ options.cause === void 0 ? { message, exitCode } : { message, exitCode, cause: options.cause }
1525
+ );
1526
+ }
1527
+ };
1528
+ function isUnauthorizedError(err) {
1529
+ if (err instanceof UnauthorizedError) {
1530
+ return true;
1531
+ }
1532
+ if (typeof err !== "object" || err === null) {
1533
+ return false;
1534
+ }
1535
+ const candidate = err;
1536
+ return candidate._tag === "UnauthorizedError" || candidate.data?.code === "UNAUTHORIZED";
1537
+ }
1538
+ var NETWORK_ERROR_CODES = ["ECONNREFUSED", "ENOTFOUND", "ECONNRESET", "EAI_AGAIN", "ETIMEDOUT"];
1539
+ var NETWORK_ERROR_PATTERN = /fetch failed|network|ECONN|ENOTFOUND|ETIMEDOUT/iu;
1540
+ var NETWORK_HTTP_ERROR_KINDS = ["InvalidUrlError", "TransportError"];
1541
+ function isNetworkError(err) {
1542
+ if (typeof err !== "object" || err === null) {
1543
+ return false;
1544
+ }
1545
+ const rpcReason = err.reason;
1546
+ if (rpcReason?._tag === "HttpError" && rpcReason.kind && NETWORK_HTTP_ERROR_KINDS.includes(rpcReason.kind)) {
1547
+ return true;
1548
+ }
1383
1549
  const code = err.cause?.code;
1384
1550
  if (code && NETWORK_ERROR_CODES.includes(code)) {
1385
1551
  return true;
@@ -1524,6 +1690,7 @@ function runSyncEffect(options) {
1524
1690
  function runSyncProgram(options) {
1525
1691
  return Effect9.gen(function* () {
1526
1692
  yield* validateRunSyncOptions(options);
1693
+ const replacement = yield* validateUsageReplacement(options.replacement, options.harness);
1527
1694
  const client = yield* SyncRpcClient;
1528
1695
  const parserCollection = yield* ParserCollection;
1529
1696
  const color = options.color === true;
@@ -1532,42 +1699,94 @@ function runSyncProgram(options) {
1532
1699
  return watermarkResult.result;
1533
1700
  }
1534
1701
  const { sessionKeySalt, watermark } = watermarkResult.value;
1535
- const cutoff = new Date((watermark?.getTime() ?? 0) - options.lookbackDays * MS_PER_DAY2);
1702
+ const cutoff = replacement ? /* @__PURE__ */ new Date(0) : new Date((watermark?.getTime() ?? 0) - options.lookbackDays * MS_PER_DAY2);
1536
1703
  if (options.verbose) {
1537
1704
  yield* logInfo2(options.logger, `watermark=${watermark?.toISOString() ?? "null"} cutoff=${cutoff.toISOString()}`);
1538
1705
  }
1539
1706
  const parsers = yield* Effect9.sync(() => options.parserFactory({ cutoff, device: options.device }));
1540
- const sources = selectSources(options.harness, parsers);
1707
+ const sources = selectSources(options.harness, parsers, replacement?.harnesses);
1541
1708
  const parserStream = yield* parserCollection.stream({
1542
1709
  sources,
1543
1710
  logger: options.logger,
1544
1711
  color
1545
1712
  });
1546
- const usageBatches = usageBatchStream(parserStream, sessionKeySalt);
1713
+ const usageBatches = usageBatchStream(parserStream, sessionKeySalt, replacement?.aggregationRange);
1714
+ if (replacement) {
1715
+ return yield* runReplacementUsage({
1716
+ options,
1717
+ client,
1718
+ color,
1719
+ replacement,
1720
+ usageBatches
1721
+ });
1722
+ }
1723
+ return yield* runOrdinaryUsage(options, client, color, usageBatches);
1724
+ });
1725
+ }
1726
+ function runReplacementUsage(input) {
1727
+ return Effect9.gen(function* () {
1728
+ const { client, color, options, replacement, usageBatches } = input;
1729
+ const batchesResult = yield* recoverSyncFailure(usageBatches.pipe(Stream4.runCollect), options.logger);
1730
+ if (batchesResult._tag === "SyncFailure") {
1731
+ return batchesResult.result;
1732
+ }
1733
+ const batches = batchesResult.value;
1734
+ const plan = summarizeUsageBatchArray(batches);
1547
1735
  if (options.dryRun) {
1548
- const planResult = yield* recoverSyncFailure(summarizeUsageBatchStream(usageBatches), options.logger);
1549
- if (planResult._tag === "SyncFailure") {
1550
- return planResult.result;
1551
- }
1552
- const plan2 = planResult.value;
1553
- const dryRunResult = yield* finishDryRun(options, plan2, color);
1554
- if (dryRunResult) {
1555
- return dryRunResult;
1556
- }
1557
1736
  yield* logInfo2(
1558
1737
  options.logger,
1559
- phase2(`Aggregated usage by hour and model: ${usageRecordText(plan2.bucketCount)} ready.`, color)
1738
+ `${dryRunPrefix(color)} atomic replacement would stage ${usageRecordText(plan.bucketCount)} and ${localSessionText2(plan.sessionCount)} in ${uploadRequestText(plan.batchCount)}.`
1560
1739
  );
1561
- const dryRunSummary = `${dryRunPrefix(color)} ${usageRecordText(plan2.bucketCount)} from ${localSessionText2(plan2.sessionCount)} would be uploaded in ${uploadRequestText(plan2.batchCount)}.`;
1562
- yield* logInfo2(options.logger, dryRunSummary);
1563
1740
  yield* logInfo2(options.logger, muted("No usage data was uploaded.", color));
1564
- return {
1565
- exitCode: EXIT.success,
1566
- batchesUploaded: plan2.batchCount,
1567
- bucketsUploaded: plan2.bucketCount,
1568
- sessionsUploaded: plan2.sessionCount
1569
- };
1741
+ return resultFromPlan(plan);
1570
1742
  }
1743
+ const uploadResult = yield* recoverSyncFailure(
1744
+ uploadUsageReplacement({
1745
+ batches,
1746
+ client,
1747
+ color,
1748
+ device: options.device,
1749
+ logger: options.logger,
1750
+ replacement
1751
+ }),
1752
+ options.logger
1753
+ );
1754
+ if (uploadResult._tag === "SyncFailure") {
1755
+ return uploadResult.result;
1756
+ }
1757
+ yield* logInfo2(
1758
+ options.logger,
1759
+ `${successPrefix(color)} atomically replaced ${usageRecordText(plan.bucketCount)} and ${localSessionText2(plan.sessionCount)}.`
1760
+ );
1761
+ return uploadResult.value;
1762
+ });
1763
+ }
1764
+ function runOrdinaryUsage(options, client, color, usageBatches) {
1765
+ return options.dryRun ? runOrdinaryDryRun(options, color, usageBatches) : uploadOrdinaryUsage(options, client, color, usageBatches);
1766
+ }
1767
+ function runOrdinaryDryRun(options, color, usageBatches) {
1768
+ return Effect9.gen(function* () {
1769
+ const planResult = yield* recoverSyncFailure(summarizeUsageBatchStream(usageBatches), options.logger);
1770
+ if (planResult._tag === "SyncFailure") {
1771
+ return planResult.result;
1772
+ }
1773
+ const plan = planResult.value;
1774
+ const earlyResult = yield* finishDryRun(options, plan, color);
1775
+ if (earlyResult) {
1776
+ return earlyResult;
1777
+ }
1778
+ yield* logInfo2(
1779
+ options.logger,
1780
+ phase2(`Aggregated usage by hour and model: ${usageRecordText(plan.bucketCount)} ready.`, color)
1781
+ );
1782
+ const summary = `${dryRunPrefix(color)} ${usageRecordText(plan.bucketCount)} from ${localSessionText2(plan.sessionCount)} would be uploaded in ${uploadRequestText(plan.batchCount)}.`;
1783
+ yield* logInfo2(options.logger, summary);
1784
+ yield* logInfo2(options.logger, muted("No usage data was uploaded.", color));
1785
+ return resultFromPlan(plan);
1786
+ });
1787
+ }
1788
+ function uploadOrdinaryUsage(options, client, color, usageBatches) {
1789
+ return Effect9.gen(function* () {
1571
1790
  const uploadResult = yield* recoverSyncFailure(
1572
1791
  uploadUsageBatchStream({
1573
1792
  batches: usageBatches,
@@ -1597,8 +1816,10 @@ function runSyncProgram(options) {
1597
1816
  return emptySuccessResult();
1598
1817
  }
1599
1818
  const verboseUploadSuffix = options.verbose ? ` in ${uploadRequestText(totals.batchesUploaded)}` : "";
1600
- const uploadSummary = `${successPrefix(color)} uploaded ${usageRecordText(totals.bucketsUploaded)} from ${localSessionText2(totals.sessionsUploaded)}${verboseUploadSuffix}.`;
1601
- yield* logInfo2(options.logger, uploadSummary);
1819
+ yield* logInfo2(
1820
+ options.logger,
1821
+ `${successPrefix(color)} uploaded ${usageRecordText(totals.bucketsUploaded)} from ${localSessionText2(totals.sessionsUploaded)}${verboseUploadSuffix}.`
1822
+ );
1602
1823
  return {
1603
1824
  exitCode: EXIT.success,
1604
1825
  batchesUploaded: totals.batchesUploaded,
@@ -1607,6 +1828,14 @@ function runSyncProgram(options) {
1607
1828
  };
1608
1829
  });
1609
1830
  }
1831
+ function resultFromPlan(plan) {
1832
+ return {
1833
+ exitCode: EXIT.success,
1834
+ batchesUploaded: plan.batchCount,
1835
+ bucketsUploaded: plan.bucketCount,
1836
+ sessionsUploaded: plan.sessionCount
1837
+ };
1838
+ }
1610
1839
  function validateRunSyncOptions(options) {
1611
1840
  return Effect9.gen(function* () {
1612
1841
  if (!options.token) {
@@ -1617,6 +1846,45 @@ function validateRunSyncOptions(options) {
1617
1846
  }
1618
1847
  });
1619
1848
  }
1849
+ function validateUsageReplacement(replacement, harness) {
1850
+ if (!replacement) {
1851
+ return Effect9.succeed(void 0);
1852
+ }
1853
+ return Effect9.try({
1854
+ try: () => {
1855
+ if (harness !== void 0) {
1856
+ throw new SyncError(EXIT.invalidArguments, "--harness cannot be combined with replacement harnesses");
1857
+ }
1858
+ const start = parseUtcHour("replacement start", replacement.startHourUtc);
1859
+ const end = parseUtcHour("replacement end", replacement.endHourUtc);
1860
+ if (end.getTime() <= start.getTime()) {
1861
+ throw new SyncError(EXIT.invalidArguments, "replacement end must be later than replacement start");
1862
+ }
1863
+ const harnesses = [...new Set(replacement.harnesses)].sort();
1864
+ if (harnesses.length === 0 || harnesses.length !== replacement.harnesses.length) {
1865
+ throw new SyncError(EXIT.invalidArguments, "replacement harnesses must be a non-empty set without duplicates");
1866
+ }
1867
+ return {
1868
+ startHourUtc: start.toISOString(),
1869
+ endHourUtc: end.toISOString(),
1870
+ harnesses,
1871
+ aggregationRange: {
1872
+ startHourUtc: start,
1873
+ endHourUtc: end,
1874
+ harnesses: new Set(harnesses)
1875
+ }
1876
+ };
1877
+ },
1878
+ catch: (error) => error instanceof SyncError ? error : new SyncError(EXIT.invalidArguments, "replacement bounds must be exact UTC hours")
1879
+ });
1880
+ }
1881
+ function parseUtcHour(field, value) {
1882
+ const parsed = new Date(value);
1883
+ if (Number.isNaN(parsed.getTime()) || parsed.toISOString() !== value || parsed.getUTCMinutes() !== 0 || parsed.getUTCSeconds() !== 0 || parsed.getUTCMilliseconds() !== 0) {
1884
+ throw new SyncError(EXIT.invalidArguments, `${field} must be an ISO UTC timestamp aligned to an hour`);
1885
+ }
1886
+ return parsed;
1887
+ }
1620
1888
  function fetchWatermark(options, client, color) {
1621
1889
  return Effect9.gen(function* () {
1622
1890
  yield* logInfo2(options.logger, phase2("Checking server sync point...", color));
@@ -1627,29 +1895,24 @@ function fetchWatermark(options, client, color) {
1627
1895
  };
1628
1896
  });
1629
1897
  }
1630
- function selectSources(harness, parsers) {
1631
- const sources = [];
1632
- if (harness === void 0 || harness === "codex") {
1633
- sources.push({ label: "Codex", source: parsers.codex });
1634
- }
1635
- if (harness === void 0 || harness === "claude_code") {
1636
- sources.push({ label: "Claude Code", source: parsers.claudeCode });
1637
- }
1638
- if (harness === void 0 || harness === "pi") {
1639
- sources.push({ label: "Pi", source: parsers.pi });
1640
- }
1641
- if (harness === void 0 || harness === "hermes") {
1642
- sources.push({ label: "Hermes", source: parsers.hermes });
1643
- }
1644
- if (harness === void 0 || harness === "opencode") {
1645
- sources.push({ label: "OpenCode", source: parsers.opencode });
1646
- }
1647
- return sources;
1898
+ function selectSources(harness, parsers, replacementHarnesses) {
1899
+ const replacementSet = replacementHarnesses ? new Set(replacementHarnesses) : void 0;
1900
+ const candidates = [
1901
+ { harness: "codex", label: "Codex", source: parsers.codex },
1902
+ { harness: "claude_code", label: "Claude Code", source: parsers.claudeCode },
1903
+ { harness: "pi", label: "Pi", source: parsers.pi },
1904
+ { harness: "hermes", label: "Hermes", source: parsers.hermes },
1905
+ { harness: "opencode", label: "OpenCode", source: parsers.opencode }
1906
+ ];
1907
+ return candidates.filter(
1908
+ (candidate) => replacementSet ? replacementSet.has(candidate.harness) : harness === void 0 || harness === candidate.harness
1909
+ ).map(({ label, source }) => ({ label, source }));
1648
1910
  }
1649
- function usageBatchStream(source, sessionKeySalt) {
1911
+ function usageBatchStream(source, sessionKeySalt, replacementRange) {
1650
1912
  return aggregateUsageStream(source, {
1651
1913
  batchSize: SYNC_BATCH_SIZE,
1652
- sessionKeySalt
1914
+ sessionKeySalt,
1915
+ replacementRange
1653
1916
  }).pipe(Stream4.filter((batch) => batch.buckets.length > 0 || batch.sessions.length > 0));
1654
1917
  }
1655
1918
  var summarizeUsageBatchStream = Effect9.fn("RunSync.summarizeUsageBatchStream")(
@@ -1738,6 +2001,51 @@ var uploadUsageBatchStream = Effect9.fn("RunSync.uploadUsageBatchStream")(
1738
2001
  return { plan, totals };
1739
2002
  })
1740
2003
  );
2004
+ var uploadUsageReplacement = Effect9.fn("RunSync.uploadUsageReplacement")(
2005
+ (options) => Effect9.gen(function* () {
2006
+ yield* logInfo2(options.logger, phase2("Creating atomic replacement scope...", options.color));
2007
+ const created = yield* options.client.createUsageReplacement({
2008
+ device: options.device,
2009
+ startHourUtc: options.replacement.startHourUtc,
2010
+ endHourUtc: options.replacement.endHourUtc,
2011
+ harnesses: options.replacement.harnesses
2012
+ });
2013
+ const buckets = options.batches.flatMap((batch) => batch.buckets);
2014
+ const sessions = options.batches.flatMap((batch) => batch.sessions);
2015
+ for (const [batchIndex, batch] of options.batches.entries()) {
2016
+ yield* logInfo2(
2017
+ options.logger,
2018
+ phase2(`Staging corrected usage... ${batchIndex + 1}/${options.batches.length}`, options.color)
2019
+ );
2020
+ yield* options.client.stageUsageReplacementBatch({
2021
+ device: options.device,
2022
+ replacementId: created.replacementId,
2023
+ batchIndex,
2024
+ batchDigest: usageReplacementDigest(batch),
2025
+ buckets: batch.buckets,
2026
+ sessions: batch.sessions
2027
+ });
2028
+ }
2029
+ yield* logInfo2(options.logger, phase2("Finalizing corrected usage atomically...", options.color));
2030
+ yield* options.client.finalizeUsageReplacement({
2031
+ device: options.device,
2032
+ replacementId: created.replacementId,
2033
+ expectedBatchCount: options.batches.length,
2034
+ expectedBucketCount: buckets.length,
2035
+ expectedSessionCount: sessions.length,
2036
+ digest: usageReplacementDigest({ buckets, sessions })
2037
+ });
2038
+ return {
2039
+ exitCode: EXIT.success,
2040
+ batchesUploaded: options.batches.length,
2041
+ bucketsUploaded: buckets.length,
2042
+ sessionsUploaded: sessions.length
2043
+ };
2044
+ })
2045
+ );
2046
+ function usageReplacementDigest(facts) {
2047
+ return createHash("sha256").update(canonicalReplacementPayload(facts)).digest("hex");
2048
+ }
1741
2049
  function recoverSyncFailure(effect, logger) {
1742
2050
  return Effect9.gen(function* () {
1743
2051
  return syncStepSuccess(yield* effect);
@@ -2270,7 +2578,7 @@ import { dirname as dirname3, isAbsolute as isAbsolute3, join as join8, matchesG
2270
2578
 
2271
2579
  // ../../packages/parsers/src/claude/index.ts
2272
2580
  import { basename, dirname as dirname2 } from "path";
2273
- import { Effect as Effect17, Stream as Stream10 } from "effect";
2581
+ import { Effect as Effect18, Stream as Stream11 } from "effect";
2274
2582
 
2275
2583
  // ../../packages/parsers/src/config.ts
2276
2584
  import { homedir as homedir2 } from "os";
@@ -2490,27 +2798,199 @@ var stderrLogger = createWarningLogger(stderr);
2490
2798
 
2491
2799
  // ../../packages/parsers/src/claude/bucketize.ts
2492
2800
  import { createReadStream as createReadStream2 } from "fs";
2801
+ import { normalize as normalize3 } from "path";
2493
2802
  import { createInterface as createInterface3 } from "readline";
2494
- import { Cause as Cause2, Effect as Effect16, Queue as Queue2, Stream as Stream9 } from "effect";
2803
+ import { Cause as Cause2, Effect as Effect17, Queue as Queue2, Stream as Stream10 } from "effect";
2495
2804
 
2496
2805
  // ../../packages/parsers/src/codex/bucketize.ts
2497
- import { createReadStream } from "fs";
2498
- import { createInterface as createInterface2 } from "readline";
2499
- import { Cause, Effect as Effect15, Queue, Stream as Stream8 } from "effect";
2806
+ import { Effect as Effect16, Stream as Stream9 } from "effect";
2500
2807
 
2501
- // ../../packages/parsers/src/types.ts
2502
- import { Schema as Schema21, Stream as Stream7 } from "effect";
2503
- var ParserSourceReadError = class extends Schema21.TaggedErrorClass()("ParserSourceReadError", {
2504
- message: Schema21.String,
2505
- path: Schema21.String,
2506
- cause: Schema21.optionalKey(Schema21.Defect())
2507
- }) {
2808
+ // ../../packages/parsers/src/codex/counters.ts
2809
+ var SEEN_RAW_TOTALS_LIMIT = 64;
2810
+ var ZERO_CODEX_USAGE = {
2811
+ inputTokens: 0,
2812
+ outputTokens: 0,
2813
+ cacheReadTokens: 0
2508
2814
  };
2509
- var makeParserSourceReadError = (path, error) => new ParserSourceReadError({
2510
- message: `Parser source read failed: ${error instanceof Error ? error.message : String(error)}`,
2511
- path,
2512
- cause: error
2513
- });
2815
+ function normalizeCodexUsage(usage) {
2816
+ return {
2817
+ inputTokens: Math.max(0, usage.input_tokens ?? 0),
2818
+ outputTokens: Math.max(0, usage.output_tokens ?? 0),
2819
+ cacheReadTokens: Math.max(0, usage.cached_input_tokens ?? usage.cache_read_input_tokens ?? 0)
2820
+ };
2821
+ }
2822
+ function sameCodexUsage(a, b) {
2823
+ return a.inputTokens === b.inputTokens && a.outputTokens === b.outputTokens && a.cacheReadTokens === b.cacheReadTokens;
2824
+ }
2825
+ function positiveCodexUsage(usage) {
2826
+ return usage.inputTokens > 0 || usage.outputTokens > 0 || usage.cacheReadTokens > 0;
2827
+ }
2828
+ function addCodexUsage(a, b) {
2829
+ return {
2830
+ inputTokens: a.inputTokens + b.inputTokens,
2831
+ outputTokens: a.outputTokens + b.outputTokens,
2832
+ cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens
2833
+ };
2834
+ }
2835
+ function minCodexUsage(a, b) {
2836
+ return {
2837
+ inputTokens: Math.min(a.inputTokens, b.inputTokens),
2838
+ outputTokens: Math.min(a.outputTokens, b.outputTokens),
2839
+ cacheReadTokens: Math.min(a.cacheReadTokens, b.cacheReadTokens)
2840
+ };
2841
+ }
2842
+ function maxCodexUsage(a, b) {
2843
+ if (a === void 0) {
2844
+ return b;
2845
+ }
2846
+ return {
2847
+ inputTokens: Math.max(a.inputTokens, b.inputTokens),
2848
+ outputTokens: Math.max(a.outputTokens, b.outputTokens),
2849
+ cacheReadTokens: Math.max(a.cacheReadTokens, b.cacheReadTokens)
2850
+ };
2851
+ }
2852
+ function subtractCodexUsageFloorZero(current, baseline) {
2853
+ return {
2854
+ inputTokens: Math.max(0, current.inputTokens - baseline.inputTokens),
2855
+ outputTokens: Math.max(0, current.outputTokens - baseline.outputTokens),
2856
+ cacheReadTokens: Math.max(0, current.cacheReadTokens - baseline.cacheReadTokens)
2857
+ };
2858
+ }
2859
+ function codexUsageContains(current, baseline) {
2860
+ return current.inputTokens >= baseline.inputTokens && current.outputTokens >= baseline.outputTokens && current.cacheReadTokens >= baseline.cacheReadTokens;
2861
+ }
2862
+ function codexUsageAtMost(current, ceiling) {
2863
+ return current.inputTokens <= ceiling.inputTokens && current.outputTokens <= ceiling.outputTokens && current.cacheReadTokens <= ceiling.cacheReadTokens;
2864
+ }
2865
+ function makeCodexContainmentState() {
2866
+ return {
2867
+ countedUsage: void 0,
2868
+ rawBaseline: void 0,
2869
+ watermark: void 0,
2870
+ seenRawTotals: [],
2871
+ sawDivergentTotals: false,
2872
+ sawInterleavedTotals: false,
2873
+ sawUnresolvedTotal: false
2874
+ };
2875
+ }
2876
+ function containedCodexDelta(watermark, counted, current) {
2877
+ const water = watermark ?? ZERO_CODEX_USAGE;
2878
+ const accepted = counted ?? ZERO_CODEX_USAGE;
2879
+ const component = (highWatermark, countedUsage, currentUsage) => currentUsage >= highWatermark ? Math.max(0, currentUsage - Math.max(highWatermark, countedUsage)) : Math.max(0, currentUsage - countedUsage);
2880
+ return {
2881
+ inputTokens: component(water.inputTokens, accepted.inputTokens, current.inputTokens),
2882
+ outputTokens: component(water.outputTokens, accepted.outputTokens, current.outputTokens),
2883
+ cacheReadTokens: component(water.cacheReadTokens, accepted.cacheReadTokens, current.cacheReadTokens)
2884
+ };
2885
+ }
2886
+ function divergentCodexDelta(rawBaseline, countedBaseline, current) {
2887
+ const raw = rawBaseline ?? ZERO_CODEX_USAGE;
2888
+ const counted = countedBaseline ?? ZERO_CODEX_USAGE;
2889
+ const component = (rawUsage, countedUsage, currentUsage) => currentUsage >= rawUsage ? Math.max(0, currentUsage - rawUsage) : Math.max(0, currentUsage - countedUsage);
2890
+ return {
2891
+ inputTokens: component(raw.inputTokens, counted.inputTokens, current.inputTokens),
2892
+ outputTokens: component(raw.outputTokens, counted.outputTokens, current.outputTokens),
2893
+ cacheReadTokens: component(raw.cacheReadTokens, counted.cacheReadTokens, current.cacheReadTokens)
2894
+ };
2895
+ }
2896
+ function rememberCodexTotal(state, total) {
2897
+ state.watermark = maxCodexUsage(state.watermark, total);
2898
+ if (state.seenRawTotals.some((seen) => sameCodexUsage(seen, total))) {
2899
+ return;
2900
+ }
2901
+ state.seenRawTotals.push(total);
2902
+ if (state.seenRawTotals.length > SEEN_RAW_TOTALS_LIMIT) {
2903
+ state.seenRawTotals.splice(0, state.seenRawTotals.length - SEEN_RAW_TOTALS_LIMIT);
2904
+ }
2905
+ }
2906
+ function applyCodexSnapshotWithLast(state, last, total, watermarkBaseline) {
2907
+ const base = state.countedUsage ?? ZERO_CODEX_USAGE;
2908
+ if (total === void 0) {
2909
+ state.countedUsage = addCodexUsage(base, last);
2910
+ state.rawBaseline = state.countedUsage;
2911
+ state.watermark = maxCodexUsage(state.watermark, state.countedUsage);
2912
+ return last;
2913
+ }
2914
+ let delta = last;
2915
+ if (state.sawInterleavedTotals) {
2916
+ delta = minCodexUsage(last, containedCodexDelta(watermarkBaseline, state.countedUsage, total));
2917
+ } else {
2918
+ const totalDelta = subtractCodexUsageFloorZero(total, watermarkBaseline ?? ZERO_CODEX_USAGE);
2919
+ const useTotalDelta = !state.sawDivergentTotals && watermarkBaseline !== void 0 && codexUsageContains(total, watermarkBaseline) && codexUsageAtMost(totalDelta, last);
2920
+ if (useTotalDelta) {
2921
+ delta = totalDelta;
2922
+ }
2923
+ }
2924
+ state.countedUsage = addCodexUsage(base, delta);
2925
+ state.rawBaseline = total;
2926
+ state.sawDivergentTotals ||= !sameCodexUsage(total, state.countedUsage);
2927
+ return delta;
2928
+ }
2929
+ function applyCodexTotalOnlySnapshot(state, total, watermarkBaseline) {
2930
+ let delta;
2931
+ if (state.sawInterleavedTotals) {
2932
+ delta = containedCodexDelta(watermarkBaseline, state.countedUsage, total);
2933
+ } else if (state.sawDivergentTotals) {
2934
+ delta = divergentCodexDelta(watermarkBaseline, state.countedUsage, total);
2935
+ } else {
2936
+ delta = subtractCodexUsageFloorZero(total, watermarkBaseline ?? ZERO_CODEX_USAGE);
2937
+ }
2938
+ state.countedUsage = addCodexUsage(state.countedUsage ?? ZERO_CODEX_USAGE, delta);
2939
+ state.rawBaseline = total;
2940
+ state.sawDivergentTotals ||= !sameCodexUsage(total, state.countedUsage);
2941
+ return delta;
2942
+ }
2943
+ function applyCodexContainedSnapshot(state, last, total, options = {}) {
2944
+ if (total !== void 0 && state.seenRawTotals.some((seen) => sameCodexUsage(seen, total))) {
2945
+ return void 0;
2946
+ }
2947
+ if (total !== void 0 && state.watermark !== void 0 && !codexUsageContains(total, state.watermark)) {
2948
+ state.sawInterleavedTotals = true;
2949
+ }
2950
+ const watermarkBaseline = state.watermark ?? state.rawBaseline;
2951
+ const effectiveLast = total !== void 0 && options.totalOnlyBeforeInterleaving === true && !state.sawInterleavedTotals ? void 0 : last;
2952
+ let delta;
2953
+ if (effectiveLast !== void 0) {
2954
+ delta = applyCodexSnapshotWithLast(state, effectiveLast, total, watermarkBaseline);
2955
+ } else if (total !== void 0) {
2956
+ delta = applyCodexTotalOnlySnapshot(state, total, watermarkBaseline);
2957
+ }
2958
+ if (total !== void 0) {
2959
+ rememberCodexTotal(state, total);
2960
+ }
2961
+ return delta !== void 0 && positiveCodexUsage(delta) ? delta : void 0;
2962
+ }
2963
+ function applyCodexUnresolvedSnapshot(state, last, total) {
2964
+ if (total === void 0) {
2965
+ return applyCodexContainedSnapshot(state, last, void 0);
2966
+ }
2967
+ if (state.seenRawTotals.some((seen) => sameCodexUsage(seen, total))) {
2968
+ return void 0;
2969
+ }
2970
+ if (state.watermark !== void 0 && !codexUsageContains(total, state.watermark)) {
2971
+ state.sawInterleavedTotals = true;
2972
+ }
2973
+ const watermarkBaseline = state.watermark ?? state.rawBaseline;
2974
+ let delta;
2975
+ if (state.sawUnresolvedTotal && last !== void 0) {
2976
+ delta = minCodexUsage(last, subtractCodexUsageFloorZero(total, watermarkBaseline ?? ZERO_CODEX_USAGE));
2977
+ state.countedUsage = addCodexUsage(state.countedUsage ?? ZERO_CODEX_USAGE, delta);
2978
+ state.rawBaseline = state.countedUsage;
2979
+ }
2980
+ state.sawUnresolvedTotal = true;
2981
+ rememberCodexTotal(state, total);
2982
+ return delta !== void 0 && positiveCodexUsage(delta) ? delta : void 0;
2983
+ }
2984
+ function cumulativeCodexDelta(current, previous) {
2985
+ if (previous === void 0) {
2986
+ return current;
2987
+ }
2988
+ return {
2989
+ inputTokens: current.inputTokens < previous.inputTokens ? current.inputTokens : current.inputTokens - previous.inputTokens,
2990
+ outputTokens: current.outputTokens < previous.outputTokens ? current.outputTokens : current.outputTokens - previous.outputTokens,
2991
+ cacheReadTokens: current.cacheReadTokens < previous.cacheReadTokens ? current.cacheReadTokens : current.cacheReadTokens - previous.cacheReadTokens
2992
+ };
2993
+ }
2514
2994
 
2515
2995
  // ../../packages/parsers/src/codex/events.ts
2516
2996
  function isObject(value) {
@@ -2519,7 +2999,13 @@ function isObject(value) {
2519
2999
  function isFiniteNumber(value) {
2520
3000
  return typeof value === "number" && Number.isFinite(value);
2521
3001
  }
2522
- function isCodexUsageDelta(value) {
3002
+ var MAX_CODEX_SOURCE_ID_LENGTH = 512;
3003
+ var MAX_CODEX_TIMESTAMP_LENGTH = 128;
3004
+ var MAX_CODEX_LOCAL_PATH_LENGTH = 4096;
3005
+ function isOptionalBoundedString(value, maxLength) {
3006
+ return value === void 0 || typeof value === "string" && value.length <= maxLength;
3007
+ }
3008
+ function isCodexUsageCounters(value) {
2523
3009
  if (!isObject(value)) {
2524
3010
  return false;
2525
3011
  }
@@ -2549,6 +3035,11 @@ function isCodexTokenCountEvent(value) {
2549
3035
  if (payload["type"] !== "token_count") {
2550
3036
  return false;
2551
3037
  }
3038
+ for (const key6 of ["id", "turn_id", "turnId"]) {
3039
+ if (!isOptionalBoundedString(payload[key6], MAX_CODEX_SOURCE_ID_LENGTH)) {
3040
+ return false;
3041
+ }
3042
+ }
2552
3043
  const info = payload["info"];
2553
3044
  if (info !== void 0) {
2554
3045
  if (!isObject(info)) {
@@ -2557,20 +3048,47 @@ function isCodexTokenCountEvent(value) {
2557
3048
  if (info["model"] !== void 0 && typeof info["model"] !== "string") {
2558
3049
  return false;
2559
3050
  }
2560
- if (info["last_token_usage"] !== void 0 && !isCodexUsageDelta(info["last_token_usage"])) {
3051
+ for (const key6 of ["id", "turn_id", "turnId"]) {
3052
+ if (!isOptionalBoundedString(info[key6], MAX_CODEX_SOURCE_ID_LENGTH)) {
3053
+ return false;
3054
+ }
3055
+ }
3056
+ if (info["last_token_usage"] !== void 0 && !isCodexUsageCounters(info["last_token_usage"])) {
2561
3057
  return false;
2562
3058
  }
2563
- if (info["total_token_usage"] !== void 0 && !isCodexUsageDelta(info["total_token_usage"])) {
3059
+ if (info["total_token_usage"] !== void 0 && !isCodexUsageCounters(info["total_token_usage"])) {
3060
+ return false;
3061
+ }
3062
+ }
3063
+ if (payload["model"] !== void 0 && typeof payload["model"] !== "string") {
3064
+ return false;
3065
+ }
3066
+ if (value["model"] !== void 0 && typeof value["model"] !== "string") {
3067
+ return false;
3068
+ }
3069
+ return true;
3070
+ }
3071
+ function isCodexTaskStartedEvent(value) {
3072
+ if (!isObject(value) || value["type"] !== "event_msg" || typeof value["timestamp"] !== "string") {
3073
+ return false;
3074
+ }
3075
+ const payload = value["payload"];
3076
+ if (!isObject(payload) || payload["type"] !== "task_started") {
3077
+ return false;
3078
+ }
3079
+ for (const key6 of ["id", "turn_id", "turnId"]) {
3080
+ if (!isOptionalBoundedString(payload[key6], MAX_CODEX_SOURCE_ID_LENGTH)) {
2564
3081
  return false;
2565
3082
  }
2566
3083
  }
2567
- if (payload["model"] !== void 0 && typeof payload["model"] !== "string") {
2568
- return false;
3084
+ const info = payload["info"];
3085
+ if (info === void 0) {
3086
+ return true;
2569
3087
  }
2570
- if (value["model"] !== void 0 && typeof value["model"] !== "string") {
3088
+ if (!isObject(info)) {
2571
3089
  return false;
2572
3090
  }
2573
- return true;
3091
+ return ["id", "turn_id", "turnId"].every((key6) => isOptionalBoundedString(info[key6], MAX_CODEX_SOURCE_ID_LENGTH));
2574
3092
  }
2575
3093
  function isCodexTurnContextEvent(value) {
2576
3094
  if (!isObject(value)) {
@@ -2610,46 +3128,105 @@ function isCodexSessionMetaEvent(value) {
2610
3128
  if (value["type"] !== "session_meta") {
2611
3129
  return false;
2612
3130
  }
2613
- if (value["timestamp"] !== void 0 && typeof value["timestamp"] !== "string") {
3131
+ if (!isOptionalBoundedString(value["timestamp"], MAX_CODEX_TIMESTAMP_LENGTH)) {
2614
3132
  return false;
2615
3133
  }
3134
+ for (const key6 of ["id", "session_id", "sessionId"]) {
3135
+ if (!isOptionalBoundedString(value[key6], MAX_CODEX_SOURCE_ID_LENGTH)) {
3136
+ return false;
3137
+ }
3138
+ }
2616
3139
  const payload = value["payload"];
2617
3140
  if (!isObject(payload)) {
2618
3141
  return false;
2619
3142
  }
2620
- if (payload["cwd"] !== void 0 && typeof payload["cwd"] !== "string") {
3143
+ if (!isOptionalBoundedString(payload["cwd"], MAX_CODEX_LOCAL_PATH_LENGTH)) {
3144
+ return false;
3145
+ }
3146
+ for (const key6 of [
3147
+ "id",
3148
+ "session_id",
3149
+ "sessionId",
3150
+ "forked_from_id",
3151
+ "forkedFromId",
3152
+ "parent_thread_id",
3153
+ "parent_session_id",
3154
+ "parentSessionId"
3155
+ ]) {
3156
+ if (!isOptionalBoundedString(payload[key6], MAX_CODEX_SOURCE_ID_LENGTH)) {
3157
+ return false;
3158
+ }
3159
+ }
3160
+ if (!isOptionalBoundedString(payload["timestamp"], MAX_CODEX_TIMESTAMP_LENGTH)) {
2621
3161
  return false;
2622
3162
  }
2623
3163
  return true;
2624
3164
  }
2625
- function pickCodexDelta(event) {
2626
- return event.payload.info?.last_token_usage ?? event.payload.info?.total_token_usage;
3165
+ function nonEmpty(value) {
3166
+ const trimmed = value?.trim();
3167
+ return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
2627
3168
  }
2628
- function pickCodexEventModel(event) {
2629
- return event.payload.info?.model ?? event.payload.model ?? event.model;
2630
- }
2631
-
2632
- // ../../packages/parsers/src/codex/bucketize.ts
2633
- function startOfUtcHour(d) {
2634
- return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), 0, 0, 0));
2635
- }
2636
- function key2(hourUtc, model) {
2637
- return `${hourUtc.toISOString()}|${model}`;
2638
- }
2639
- function shouldExcludePath(path, excludePath) {
2640
- return path !== void 0 && excludePath?.(path) === true;
3169
+ function extractCodexSourceMetadata(event) {
3170
+ const sourceSessionId = nonEmpty(
3171
+ event.payload.id ?? event.id ?? event.payload.session_id ?? event.payload.sessionId ?? event.session_id ?? event.sessionId
3172
+ );
3173
+ const canonicalSessionId = nonEmpty(
3174
+ event.payload.session_id ?? event.payload.sessionId ?? event.session_id ?? event.sessionId
3175
+ );
3176
+ return {
3177
+ sourceSessionId,
3178
+ canonicalSessionId,
3179
+ lineageSessionId: canonicalSessionId ?? sourceSessionId,
3180
+ parentSourceSessionId: nonEmpty(
3181
+ event.payload.forked_from_id ?? event.payload.forkedFromId ?? event.payload.parent_session_id ?? event.payload.parentSessionId
3182
+ ),
3183
+ // Native agent files also carry parent_thread_id for structural thread
3184
+ // ownership. It is not evidence that token counters share a cumulative
3185
+ // scale, so retain it as bounded metadata without using it as a baseline.
3186
+ parentThreadId: nonEmpty(event.payload.parent_thread_id),
3187
+ forkedAt: nonEmpty(event.payload.timestamp ?? event.timestamp),
3188
+ cwd: event.payload.cwd
3189
+ };
2641
3190
  }
2642
- function makeCodexFileAccumulator() {
3191
+ function extractCodexUsageSnapshot(event) {
2643
3192
  return {
2644
- buckets: /* @__PURE__ */ new Map(),
2645
- currentModel: void 0,
2646
- excluded: false,
2647
- lineNo: 0,
2648
- sawTurnContext: false,
2649
- sessionLastSeenAt: void 0,
2650
- sessionStartedAt: void 0
3193
+ last: event.payload.info?.last_token_usage,
3194
+ total: event.payload.info?.total_token_usage
2651
3195
  };
2652
3196
  }
3197
+ function pickCodexEventModel(event) {
3198
+ return event.payload.info?.model ?? event.payload.model ?? event.model;
3199
+ }
3200
+ function pickCodexTurnId(event) {
3201
+ return nonEmpty(
3202
+ event.payload.turn_id ?? event.payload.turnId ?? event.payload.id ?? event.payload.info?.turn_id ?? event.payload.info?.turnId ?? event.payload.info?.id
3203
+ );
3204
+ }
3205
+
3206
+ // ../../packages/parsers/src/codex/lineage.ts
3207
+ import { createReadStream } from "fs";
3208
+ import { open, stat } from "fs/promises";
3209
+ import { normalize as normalize2 } from "path";
3210
+ import { createInterface as createInterface2 } from "readline";
3211
+ import { Cause, Effect as Effect15, Queue, Stream as Stream8 } from "effect";
3212
+
3213
+ // ../../packages/parsers/src/types.ts
3214
+ import { Schema as Schema21, Stream as Stream7 } from "effect";
3215
+ var ParserSourceReadError = class extends Schema21.TaggedErrorClass()("ParserSourceReadError", {
3216
+ message: Schema21.String,
3217
+ path: Schema21.String,
3218
+ cause: Schema21.optionalKey(Schema21.Defect())
3219
+ }) {
3220
+ };
3221
+ var makeParserSourceReadError = (path, error) => new ParserSourceReadError({
3222
+ message: `Parser source read failed: ${error instanceof Error ? error.message : String(error)}`,
3223
+ path,
3224
+ cause: error
3225
+ });
3226
+
3227
+ // ../../packages/parsers/src/codex/lineage.ts
3228
+ var CANDIDATE_TAIL_BYTES = 262144;
3229
+ var CANDIDATE_INSPECTION_CONCURRENCY = 8;
2653
3230
  function lineStreamFromFile(path) {
2654
3231
  return Stream8.callback(
2655
3232
  (queue) => Effect15.acquireRelease(
@@ -2694,10 +3271,445 @@ function lineStreamFromFile(path) {
2694
3271
  )
2695
3272
  );
2696
3273
  }
3274
+ async function inspectSourceMetadata(path) {
3275
+ const stream = createReadStream(path, { encoding: "utf8" });
3276
+ const lines = createInterface2({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
3277
+ let first;
3278
+ const sourceSessionIds = /* @__PURE__ */ new Set();
3279
+ try {
3280
+ for await (const line of lines) {
3281
+ if (!line.includes("session_meta")) {
3282
+ continue;
3283
+ }
3284
+ let parsed;
3285
+ try {
3286
+ parsed = JSON.parse(line);
3287
+ } catch {
3288
+ continue;
3289
+ }
3290
+ if (isCodexSessionMetaEvent(parsed)) {
3291
+ const metadata = extractCodexSourceMetadata(parsed);
3292
+ first ??= metadata;
3293
+ if (metadata.sourceSessionId !== void 0) {
3294
+ sourceSessionIds.add(metadata.sourceSessionId);
3295
+ }
3296
+ }
3297
+ }
3298
+ return { first, containsMultipleSessionIds: sourceSessionIds.size > 1 };
3299
+ } finally {
3300
+ lines.close();
3301
+ stream.destroy();
3302
+ }
3303
+ }
3304
+ async function hasCompleteTail(path, size) {
3305
+ if (size <= 0) {
3306
+ return false;
3307
+ }
3308
+ const handle = await open(path, "r");
3309
+ try {
3310
+ const bytesToRead = Math.min(size, CANDIDATE_TAIL_BYTES);
3311
+ const tail = Buffer.allocUnsafe(bytesToRead);
3312
+ await handle.read(tail, 0, bytesToRead, size - bytesToRead);
3313
+ const lastLine = tail.toString("utf8").split("\n").findLast((line) => line.trim().length > 0)?.trim();
3314
+ if (!lastLine) {
3315
+ return false;
3316
+ }
3317
+ try {
3318
+ JSON.parse(lastLine);
3319
+ return true;
3320
+ } catch {
3321
+ return false;
3322
+ }
3323
+ } finally {
3324
+ await handle.close();
3325
+ }
3326
+ }
3327
+ function inspectCodexSourceCandidate(path) {
3328
+ return Effect15.tryPromise({
3329
+ try: async () => {
3330
+ const info = await stat(path);
3331
+ const inspectedMetadata = await inspectSourceMetadata(path);
3332
+ return {
3333
+ path,
3334
+ normalizedPath: normalize2(path),
3335
+ metadata: inspectedMetadata.first,
3336
+ containsMultipleSessionIds: inspectedMetadata.containsMultipleSessionIds,
3337
+ complete: await hasCompleteTail(path, info.size),
3338
+ mtimeMs: info.mtimeMs,
3339
+ size: info.size
3340
+ };
3341
+ },
3342
+ catch: (error) => makeParserSourceReadError(path, error)
3343
+ });
3344
+ }
3345
+ function candidateComesFirst(a, b) {
3346
+ if (a.complete !== b.complete) {
3347
+ return a.complete ? -1 : 1;
3348
+ }
3349
+ if (a.size !== b.size) {
3350
+ return b.size - a.size;
3351
+ }
3352
+ if (a.mtimeMs !== b.mtimeMs) {
3353
+ return b.mtimeMs - a.mtimeMs;
3354
+ }
3355
+ return a.normalizedPath.localeCompare(b.normalizedPath);
3356
+ }
3357
+ function candidateConflictKind(candidates) {
3358
+ const parents = new Set(candidates.map((candidate) => candidate.metadata?.parentSourceSessionId ?? ""));
3359
+ if (parents.size > 1) {
3360
+ return "parent";
3361
+ }
3362
+ const forkBoundaries = new Set(candidates.map((candidate) => candidate.metadata?.forkedAt ?? ""));
3363
+ return forkBoundaries.size > 1 ? "fork_boundary" : void 0;
3364
+ }
3365
+ function cycleSessionIds(sources) {
3366
+ const parents = /* @__PURE__ */ new Map();
3367
+ const knownIds = new Set(
3368
+ sources.flatMap((source) => source.sourceSessionId === void 0 ? [] : [source.sourceSessionId])
3369
+ );
3370
+ for (const source of sources) {
3371
+ if (source.sourceSessionId !== void 0 && source.parentSourceSessionId !== void 0 && knownIds.has(source.parentSourceSessionId)) {
3372
+ parents.set(source.sourceSessionId, source.parentSourceSessionId);
3373
+ }
3374
+ }
3375
+ const cycles = /* @__PURE__ */ new Set();
3376
+ for (const sourceId of [...knownIds].sort()) {
3377
+ const positions = /* @__PURE__ */ new Map();
3378
+ const chain = [];
3379
+ let current = sourceId;
3380
+ while (current !== void 0) {
3381
+ const cycleStart = positions.get(current);
3382
+ if (cycleStart !== void 0) {
3383
+ for (const member of chain.slice(cycleStart)) {
3384
+ cycles.add(member);
3385
+ }
3386
+ break;
3387
+ }
3388
+ positions.set(current, chain.length);
3389
+ chain.push(current);
3390
+ current = parents.get(current);
3391
+ }
3392
+ }
3393
+ return cycles;
3394
+ }
3395
+ function groupCodexSourceCandidates(candidates) {
3396
+ const groups = /* @__PURE__ */ new Map();
3397
+ for (const candidate of candidates) {
3398
+ const sourceId = candidate.metadata?.sourceSessionId;
3399
+ const groupKey = sourceId === void 0 ? `path:${candidate.normalizedPath}` : `session:${sourceId}`;
3400
+ const group = groups.get(groupKey) ?? [];
3401
+ group.push(candidate);
3402
+ groups.set(groupKey, group);
3403
+ }
3404
+ return groups;
3405
+ }
3406
+ function indexedSourceFromCandidates(group, cutoffMs, logger) {
3407
+ group.sort(candidateComesFirst);
3408
+ const [canonical] = group;
3409
+ if (canonical === void 0) {
3410
+ return void 0;
3411
+ }
3412
+ const conflict = candidateConflictKind(group);
3413
+ if (conflict !== void 0) {
3414
+ logger.warn("conflicting codex source candidates", {
3415
+ candidateCount: group.length,
3416
+ conflict
3417
+ });
3418
+ }
3419
+ const sourceSessionId = canonical.metadata?.sourceSessionId;
3420
+ return {
3421
+ path: canonical.path,
3422
+ sourceIdentity: sourceSessionId ?? canonical.normalizedPath,
3423
+ sourceSessionId,
3424
+ lineageSessionId: canonical.metadata?.lineageSessionId,
3425
+ parentSourceSessionId: canonical.metadata?.parentSourceSessionId,
3426
+ forkedAt: canonical.metadata?.forkedAt,
3427
+ cwd: canonical.metadata?.cwd,
3428
+ containsMultipleSessionIds: canonical.containsMultipleSessionIds || canonical.metadata?.canonicalSessionId !== void 0,
3429
+ eligible: group.some((candidate) => candidate.mtimeMs >= cutoffMs)
3430
+ };
3431
+ }
3432
+ function indexCodexLineageSessions(sources) {
3433
+ const bySessionId = /* @__PURE__ */ new Map();
3434
+ for (const source of sources) {
3435
+ const { lineageSessionId } = source;
3436
+ if (lineageSessionId === void 0) {
3437
+ continue;
3438
+ }
3439
+ const existing = bySessionId.get(lineageSessionId);
3440
+ if (existing === void 0 || source.sourceSessionId === lineageSessionId && existing.sourceSessionId !== lineageSessionId) {
3441
+ bySessionId.set(lineageSessionId, source);
3442
+ }
3443
+ }
3444
+ return bySessionId;
3445
+ }
3446
+ var buildCodexSourceIndex = Effect15.fn("CodexLineage.buildSourceIndex")(function* (paths, cutoff, logger) {
3447
+ const uniquePaths = [...new Set(paths.map(normalize2))].sort();
3448
+ const candidates = yield* Effect15.forEach(uniquePaths, inspectCodexSourceCandidate, {
3449
+ concurrency: CANDIDATE_INSPECTION_CONCURRENCY
3450
+ });
3451
+ const groups = groupCodexSourceCandidates(candidates);
3452
+ const sources = [];
3453
+ for (const groupKey of [...groups.keys()].sort()) {
3454
+ const group = groups.get(groupKey) ?? [];
3455
+ const source = indexedSourceFromCandidates(group, cutoff.getTime(), logger);
3456
+ if (source !== void 0) {
3457
+ sources.push(source);
3458
+ }
3459
+ }
3460
+ sources.sort((a, b) => a.sourceIdentity.localeCompare(b.sourceIdentity));
3461
+ return {
3462
+ sources,
3463
+ bySessionId: indexCodexLineageSessions(sources),
3464
+ cycleSessionIds: cycleSessionIds(sources)
3465
+ };
3466
+ });
3467
+ function parseTimestamp(timestamp) {
3468
+ const parsed = Date.parse(timestamp);
3469
+ return Number.isFinite(parsed) ? parsed : void 0;
3470
+ }
3471
+ function parseSnapshotIncrement(state, last, total) {
3472
+ if (last !== void 0) {
3473
+ return normalizeCodexUsage(last);
3474
+ }
3475
+ return total === void 0 ? void 0 : cumulativeCodexDelta(total, state.previousTotal);
3476
+ }
3477
+ function accumulateCodexSnapshot(state, raw) {
3478
+ let parsed;
3479
+ try {
3480
+ parsed = JSON.parse(raw);
3481
+ } catch {
3482
+ return state;
3483
+ }
3484
+ if (!isCodexTokenCountEvent(parsed)) {
3485
+ return state;
3486
+ }
3487
+ const snapshot = extractCodexUsageSnapshot(parsed);
3488
+ const total = snapshot.total === void 0 ? void 0 : normalizeCodexUsage(snapshot.total);
3489
+ if (state.containment !== void 0) {
3490
+ const last = snapshot.last === void 0 ? void 0 : normalizeCodexUsage(snapshot.last);
3491
+ const increment2 = applyCodexContainedSnapshot(state.containment, last, total);
3492
+ if (increment2 === void 0) {
3493
+ return state;
3494
+ }
3495
+ state.counted = addCodexUsage(state.counted, increment2);
3496
+ state.snapshots.push({
3497
+ timestamp: parsed.timestamp,
3498
+ timestampMs: parseTimestamp(parsed.timestamp),
3499
+ usage: state.counted
3500
+ });
3501
+ return state;
3502
+ }
3503
+ if (total !== void 0 && state.previousTotal !== void 0 && sameCodexUsage(total, state.previousTotal)) {
3504
+ return state;
3505
+ }
3506
+ const increment = parseSnapshotIncrement(state, snapshot.last, total);
3507
+ if (total !== void 0) {
3508
+ state.previousTotal = total;
3509
+ }
3510
+ if (increment === void 0 || !positiveCodexUsage(increment)) {
3511
+ return state;
3512
+ }
3513
+ state.counted = addCodexUsage(state.counted, increment);
3514
+ state.snapshots.push({
3515
+ timestamp: parsed.timestamp,
3516
+ timestampMs: parseTimestamp(parsed.timestamp),
3517
+ usage: state.counted
3518
+ });
3519
+ return state;
3520
+ }
3521
+ function collectCodexSnapshots(path, containsMultipleSessionIds) {
3522
+ return lineStreamFromFile(path).pipe(
3523
+ Stream8.runFold(
3524
+ () => ({
3525
+ snapshots: [],
3526
+ containment: containsMultipleSessionIds ? makeCodexContainmentState() : void 0,
3527
+ counted: ZERO_CODEX_USAGE,
3528
+ previousTotal: void 0
3529
+ }),
3530
+ accumulateCodexSnapshot
3531
+ ),
3532
+ Effect15.map((state) => state.snapshots)
3533
+ );
3534
+ }
3535
+ function selectInheritedUsage(snapshots, forkedAt, forkedAtMs) {
3536
+ let inherited = ZERO_CODEX_USAGE;
3537
+ for (const snapshot of snapshots) {
3538
+ const atOrBefore = snapshot.timestampMs === void 0 ? snapshot.timestamp <= forkedAt : snapshot.timestampMs <= forkedAtMs;
3539
+ if (atOrBefore) {
3540
+ inherited = snapshot.usage;
3541
+ }
3542
+ }
3543
+ return inherited;
3544
+ }
3545
+ function makeCodexLineageResolver(index, logger) {
3546
+ const snapshotsBySessionId = /* @__PURE__ */ new Map();
3547
+ const warned = /* @__PURE__ */ new Set();
3548
+ const unresolved = (source, reason) => {
3549
+ const warningKey = `${source.sourceIdentity}:${reason}`;
3550
+ if (!warned.has(warningKey)) {
3551
+ warned.add(warningKey);
3552
+ logger.warn("codex fork baseline unresolved", { reason });
3553
+ }
3554
+ return { _tag: "Unresolved", reason };
3555
+ };
3556
+ const requestFor = (source) => {
3557
+ const parentId = source.parentSourceSessionId;
3558
+ if (parentId === void 0) {
3559
+ return { _tag: "Root" };
3560
+ }
3561
+ if (source.sourceSessionId !== void 0 && index.cycleSessionIds.has(source.sourceSessionId)) {
3562
+ return unresolved(source, "cycle");
3563
+ }
3564
+ const parent = index.bySessionId.get(parentId);
3565
+ if (parent === void 0) {
3566
+ return unresolved(source, "missing_parent");
3567
+ }
3568
+ const { forkedAt } = source;
3569
+ const forkedAtMs = forkedAt === void 0 ? void 0 : parseTimestamp(forkedAt);
3570
+ if (forkedAt === void 0 || forkedAtMs === void 0) {
3571
+ return unresolved(source, "invalid_fork_boundary");
3572
+ }
3573
+ return { _tag: "Ready", forkedAt, forkedAtMs, parent, parentId };
3574
+ };
3575
+ return {
3576
+ baselineFor: Effect15.fn("CodexLineage.baselineFor")(function* (source) {
3577
+ const request = requestFor(source);
3578
+ if (request._tag !== "Ready") {
3579
+ return request;
3580
+ }
3581
+ let snapshots = snapshotsBySessionId.get(request.parentId);
3582
+ if (snapshots === void 0) {
3583
+ snapshots = yield* collectCodexSnapshots(request.parent.path, request.parent.containsMultipleSessionIds);
3584
+ snapshotsBySessionId.set(request.parentId, snapshots);
3585
+ }
3586
+ return {
3587
+ _tag: "Resolved",
3588
+ usage: selectInheritedUsage(snapshots, request.forkedAt, request.forkedAtMs)
3589
+ };
3590
+ })
3591
+ };
3592
+ }
3593
+
3594
+ // ../../packages/parsers/src/codex/bucketize.ts
3595
+ function startOfUtcHour(d) {
3596
+ return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), 0, 0, 0));
3597
+ }
3598
+ function key2(hourUtc, model) {
3599
+ return `${hourUtc.toISOString()}|${model}`;
3600
+ }
3601
+ function shouldExcludePath(path, excludePath) {
3602
+ return path !== void 0 && excludePath?.(path) === true;
3603
+ }
3604
+ function makeCodexFileAccumulator(options) {
3605
+ const lineageBaseline = options.lineageBaseline ?? { _tag: "Root" };
3606
+ return {
3607
+ buckets: /* @__PURE__ */ new Map(),
3608
+ candidates: [],
3609
+ currentModel: void 0,
3610
+ currentTurnId: void 0,
3611
+ containment: options.containInterleavedTotals === true ? makeCodexContainmentState() : void 0,
3612
+ excluded: false,
3613
+ lineNo: 0,
3614
+ nextEventIndex: 0,
3615
+ lineageKind: lineageBaseline._tag,
3616
+ inheritedBaselineUsage: lineageBaseline._tag === "Resolved" ? lineageBaseline.usage : void 0,
3617
+ sawTurnContext: false,
3618
+ previousTotalUsage: lineageBaseline._tag === "Resolved" ? lineageBaseline.usage : void 0,
3619
+ remainingInheritedUsage: lineageBaseline._tag === "Resolved" ? lineageBaseline.usage : void 0,
3620
+ pendingUsage: [],
3621
+ sessionLastSeenAt: void 0,
3622
+ sessionStartedAt: void 0
3623
+ };
3624
+ }
3625
+ function subtractRemainingInheritedUsage(acc, rawIncrement) {
3626
+ const remaining = acc.remainingInheritedUsage;
3627
+ if (remaining === void 0) {
3628
+ return rawIncrement;
3629
+ }
3630
+ acc.remainingInheritedUsage = subtractCodexUsageFloorZero(remaining, rawIncrement);
3631
+ return subtractCodexUsageFloorZero(rawIncrement, remaining);
3632
+ }
3633
+ function positiveIncrement(increment) {
3634
+ return positiveCodexUsage(increment) ? increment : void 0;
3635
+ }
3636
+ function unresolvedUsageIncrement(acc, total) {
3637
+ if (total === void 0) {
3638
+ return void 0;
3639
+ }
3640
+ const previous = acc.previousTotalUsage;
3641
+ acc.previousTotalUsage = total;
3642
+ if (previous === void 0 || sameCodexUsage(previous, total)) {
3643
+ return void 0;
3644
+ }
3645
+ return positiveIncrement(cumulativeCodexDelta(total, previous));
3646
+ }
3647
+ function resolvedUsageIncrement(acc, last, total) {
3648
+ if (total !== void 0) {
3649
+ const previous = acc.previousTotalUsage;
3650
+ if (previous !== void 0 && sameCodexUsage(previous, total)) {
3651
+ return void 0;
3652
+ }
3653
+ acc.previousTotalUsage = total;
3654
+ acc.remainingInheritedUsage = void 0;
3655
+ return positiveIncrement(cumulativeCodexDelta(total, previous));
3656
+ }
3657
+ if (last === void 0) {
3658
+ return void 0;
3659
+ }
3660
+ const rawIncrement = normalizeCodexUsage(last);
3661
+ return positiveIncrement(subtractRemainingInheritedUsage(acc, rawIncrement));
3662
+ }
3663
+ function containedUsageIncrement(acc, last, total) {
3664
+ const { containment } = acc;
3665
+ if (containment === void 0) {
3666
+ return void 0;
3667
+ }
3668
+ const normalizedTotal = total === void 0 ? void 0 : normalizeCodexUsage(total);
3669
+ const normalizedLast = last === void 0 ? void 0 : normalizeCodexUsage(last);
3670
+ if (acc.lineageKind === "Unresolved") {
3671
+ return applyCodexUnresolvedSnapshot(containment, normalizedLast, normalizedTotal);
3672
+ }
3673
+ if (acc.lineageKind === "Resolved") {
3674
+ const adjustedTotal = normalizedTotal === void 0 || acc.inheritedBaselineUsage === void 0 ? normalizedTotal : subtractCodexUsageFloorZero(normalizedTotal, acc.inheritedBaselineUsage);
3675
+ const adjustedLast = normalizedTotal === void 0 && normalizedLast !== void 0 ? subtractRemainingInheritedUsage(acc, normalizedLast) : normalizedLast;
3676
+ const increment = applyCodexContainedSnapshot(containment, adjustedLast, adjustedTotal, {
3677
+ totalOnlyBeforeInterleaving: true
3678
+ });
3679
+ if (normalizedTotal !== void 0) {
3680
+ acc.remainingInheritedUsage = void 0;
3681
+ }
3682
+ return increment;
3683
+ }
3684
+ return applyCodexContainedSnapshot(containment, normalizedLast, normalizedTotal);
3685
+ }
3686
+ function rootUsageIncrement(acc, last, total) {
3687
+ if (total !== void 0) {
3688
+ const previous = acc.previousTotalUsage;
3689
+ if (previous !== void 0 && sameCodexUsage(previous, total)) {
3690
+ return void 0;
3691
+ }
3692
+ const increment = last === void 0 ? cumulativeCodexDelta(total, previous) : normalizeCodexUsage(last);
3693
+ acc.previousTotalUsage = total;
3694
+ return positiveIncrement(increment);
3695
+ }
3696
+ return last === void 0 ? void 0 : positiveIncrement(normalizeCodexUsage(last));
3697
+ }
3698
+ function usageIncrement(acc, last, total) {
3699
+ if (acc.containment !== void 0) {
3700
+ return containedUsageIncrement(acc, last, total);
3701
+ }
3702
+ const normalizedTotal = total === void 0 ? void 0 : normalizeCodexUsage(total);
3703
+ if (acc.lineageKind === "Resolved") {
3704
+ return resolvedUsageIncrement(acc, last, normalizedTotal);
3705
+ }
3706
+ if (acc.lineageKind === "Unresolved") {
3707
+ return unresolvedUsageIncrement(acc, normalizedTotal);
3708
+ }
3709
+ return rootUsageIncrement(acc, last, normalizedTotal);
3710
+ }
2697
3711
  function addToBucket(acc, hourUtc, model, delta) {
2698
- const inputTokens = Math.max(0, delta.input_tokens ?? 0);
2699
- const outputTokens = Math.max(0, delta.output_tokens ?? 0);
2700
- const cacheReadTokens = Math.max(0, delta.cached_input_tokens ?? delta.cache_read_input_tokens ?? 0);
3712
+ const { inputTokens, outputTokens, cacheReadTokens } = delta;
2701
3713
  if (inputTokens === 0 && outputTokens === 0 && cacheReadTokens === 0) {
2702
3714
  return false;
2703
3715
  }
@@ -2719,6 +3731,19 @@ function addToBucket(acc, hourUtc, model, delta) {
2719
3731
  existing.cacheReadTokens += cacheReadTokens;
2720
3732
  return true;
2721
3733
  }
3734
+ function recordUsageCandidate(acc, usage, model) {
3735
+ if (!addToBucket(acc, startOfUtcHour(usage.eventAt), model, usage.usage)) {
3736
+ return;
3737
+ }
3738
+ acc.candidates.push({ ...usage, model });
3739
+ markSessionSeen(acc, usage.eventAt);
3740
+ }
3741
+ function flushPendingUsage(acc, model) {
3742
+ for (const pending of acc.pendingUsage) {
3743
+ recordUsageCandidate(acc, pending, model);
3744
+ }
3745
+ acc.pendingUsage = [];
3746
+ }
2722
3747
  function markSessionSeen(acc, eventAt) {
2723
3748
  if (acc.sessionStartedAt === void 0 || eventAt.getTime() < acc.sessionStartedAt.getTime()) {
2724
3749
  acc.sessionStartedAt = eventAt;
@@ -2748,6 +3773,8 @@ function accumulateCodexLine(context, acc, raw) {
2748
3773
  if (shouldExcludePath(parsed.payload.cwd, options.excludePath)) {
2749
3774
  acc.excluded = true;
2750
3775
  acc.buckets.clear();
3776
+ acc.candidates = [];
3777
+ acc.pendingUsage = [];
2751
3778
  }
2752
3779
  return acc;
2753
3780
  }
@@ -2755,15 +3782,22 @@ function accumulateCodexLine(context, acc, raw) {
2755
3782
  if (shouldExcludePath(parsed.payload.cwd, options.excludePath)) {
2756
3783
  acc.excluded = true;
2757
3784
  acc.buckets.clear();
3785
+ acc.candidates = [];
3786
+ acc.pendingUsage = [];
2758
3787
  return acc;
2759
3788
  }
2760
3789
  const model2 = parsed.payload.model ?? parsed.payload.info?.model;
2761
3790
  if (model2 !== void 0) {
2762
3791
  acc.currentModel = model2;
2763
3792
  acc.sawTurnContext = true;
3793
+ flushPendingUsage(acc, model2);
2764
3794
  }
2765
3795
  return acc;
2766
3796
  }
3797
+ if (isCodexTaskStartedEvent(parsed)) {
3798
+ acc.currentTurnId = pickCodexTurnId(parsed);
3799
+ return acc;
3800
+ }
2767
3801
  if (!isCodexTokenCountEvent(parsed)) {
2768
3802
  return acc;
2769
3803
  }
@@ -2771,28 +3805,31 @@ function accumulateCodexLine(context, acc, raw) {
2771
3805
  if (!Number.isFinite(tsMs)) {
2772
3806
  return acc;
2773
3807
  }
2774
- const eventModel = pickCodexEventModel(parsed);
2775
- const model = acc.sawTurnContext ? acc.currentModel ?? eventModel : eventModel ?? acc.currentModel;
2776
- if (model === void 0) {
2777
- return acc;
2778
- }
2779
- const delta = pickCodexDelta(parsed);
3808
+ const snapshot = extractCodexUsageSnapshot(parsed);
3809
+ const delta = usageIncrement(acc, snapshot.last, snapshot.total);
2780
3810
  if (delta === void 0) {
2781
3811
  return acc;
2782
3812
  }
2783
3813
  const eventAt = new Date(tsMs);
2784
- if (addToBucket(acc, startOfUtcHour(eventAt), model, delta)) {
2785
- markSessionSeen(acc, eventAt);
3814
+ const eventIndex = acc.nextEventIndex;
3815
+ acc.nextEventIndex += 1;
3816
+ const turnId = pickCodexTurnId(parsed) ?? acc.currentTurnId;
3817
+ const candidate = { eventAt, eventIndex, turnId, usage: delta };
3818
+ const eventModel = pickCodexEventModel(parsed);
3819
+ const model = acc.sawTurnContext ? acc.currentModel ?? eventModel : eventModel ?? acc.currentModel;
3820
+ if (model === void 0) {
3821
+ acc.pendingUsage.push(candidate);
3822
+ return acc;
2786
3823
  }
3824
+ recordUsageCandidate(acc, candidate, model);
2787
3825
  return acc;
2788
3826
  }
2789
- function bucketsFromAccumulator(path, acc) {
3827
+ function bucketsFromAccumulator(path, options, acc) {
2790
3828
  if (acc.excluded || acc.sessionStartedAt === void 0 || acc.sessionLastSeenAt === void 0) {
2791
3829
  return [];
2792
3830
  }
2793
- const sessionStartedAt = acc.sessionStartedAt;
2794
- const sessionLastSeenAt = acc.sessionLastSeenAt;
2795
- const sessionIds = /* @__PURE__ */ new Set([path]);
3831
+ const { sessionLastSeenAt, sessionStartedAt } = acc;
3832
+ const sessionIds = /* @__PURE__ */ new Set([options.sessionIdentity ?? path]);
2796
3833
  return Array.from(acc.buckets.values(), (bucket) => ({
2797
3834
  harness: "codex",
2798
3835
  model: bucket.model,
@@ -2806,13 +3843,37 @@ function bucketsFromAccumulator(path, acc) {
2806
3843
  sessionLastSeenAt
2807
3844
  }));
2808
3845
  }
2809
- function bucketizeCodexFileStream(path, logger, options = {}) {
3846
+ function candidatesFromAccumulator(path, options, acc) {
3847
+ if (acc.excluded || acc.sessionStartedAt === void 0 || acc.sessionLastSeenAt === void 0) {
3848
+ return [];
3849
+ }
3850
+ const sessionIdentity = options.sessionIdentity ?? path;
3851
+ const canonicalSessionIdentity = options.canonicalSessionIdentity ?? sessionIdentity;
3852
+ return acc.candidates.map((candidate) => ({
3853
+ cacheReadTokens: candidate.usage.cacheReadTokens,
3854
+ canonicalSessionIdentity,
3855
+ eventIndex: candidate.eventIndex,
3856
+ hourUtc: startOfUtcHour(candidate.eventAt),
3857
+ inputTokens: candidate.usage.inputTokens,
3858
+ model: candidate.model,
3859
+ outputTokens: candidate.usage.outputTokens,
3860
+ sessionIdentity,
3861
+ sessionLastSeenAt: acc.sessionLastSeenAt,
3862
+ sessionStartedAt: acc.sessionStartedAt,
3863
+ turnId: candidate.turnId
3864
+ }));
3865
+ }
3866
+ function codexFileUsageEffect(path, logger, options = {}) {
2810
3867
  const context = { logger, options, path };
2811
- return Stream8.unwrap(
2812
- lineStreamFromFile(path).pipe(
2813
- Stream8.runFold(makeCodexFileAccumulator, (acc, raw) => accumulateCodexLine(context, acc, raw)),
2814
- Effect15.map((acc) => Stream8.fromIterable(bucketsFromAccumulator(path, acc)))
2815
- )
3868
+ return lineStreamFromFile(path).pipe(
3869
+ Stream9.runFold(
3870
+ () => makeCodexFileAccumulator(options),
3871
+ (acc, raw) => accumulateCodexLine(context, acc, raw)
3872
+ ),
3873
+ Effect16.map((acc) => ({
3874
+ buckets: bucketsFromAccumulator(path, options, acc),
3875
+ candidates: candidatesFromAccumulator(path, options, acc)
3876
+ }))
2816
3877
  );
2817
3878
  }
2818
3879
 
@@ -2823,6 +3884,12 @@ function isObject2(value) {
2823
3884
  function isFiniteNumber2(value) {
2824
3885
  return typeof value === "number" && Number.isFinite(value);
2825
3886
  }
3887
+ var MAX_CLAUDE_SOURCE_ID_LENGTH = 512;
3888
+ var MAX_CLAUDE_TIMESTAMP_LENGTH = 128;
3889
+ var MAX_CLAUDE_LOCAL_PATH_LENGTH = 4096;
3890
+ function isOptionalBoundedString2(value, maxLength) {
3891
+ return value === void 0 || typeof value === "string" && value.length <= maxLength;
3892
+ }
2826
3893
  function isClaudeUsage(value) {
2827
3894
  if (!isObject2(value)) {
2828
3895
  return false;
@@ -2843,20 +3910,23 @@ function isClaudeAssistantEvent(value) {
2843
3910
  if (value["type"] !== "assistant") {
2844
3911
  return false;
2845
3912
  }
2846
- if (typeof value["timestamp"] !== "string") {
3913
+ if (typeof value["timestamp"] !== "string" || value["timestamp"].length > MAX_CLAUDE_TIMESTAMP_LENGTH) {
2847
3914
  return false;
2848
3915
  }
2849
- if (value["cwd"] !== void 0 && typeof value["cwd"] !== "string") {
3916
+ if (!isOptionalBoundedString2(value["cwd"], MAX_CLAUDE_LOCAL_PATH_LENGTH)) {
3917
+ return false;
3918
+ }
3919
+ if (!isOptionalBoundedString2(value["requestId"], MAX_CLAUDE_SOURCE_ID_LENGTH)) {
2850
3920
  return false;
2851
3921
  }
2852
- if (value["requestId"] !== void 0 && typeof value["requestId"] !== "string") {
3922
+ if (value["isSidechain"] !== void 0 && typeof value["isSidechain"] !== "boolean") {
2853
3923
  return false;
2854
3924
  }
2855
3925
  const message = value["message"];
2856
3926
  if (!isObject2(message)) {
2857
3927
  return false;
2858
3928
  }
2859
- if (message["id"] !== void 0 && typeof message["id"] !== "string") {
3929
+ if (!isOptionalBoundedString2(message["id"], MAX_CLAUDE_SOURCE_ID_LENGTH)) {
2860
3930
  return false;
2861
3931
  }
2862
3932
  if (typeof message["model"] !== "string") {
@@ -2872,26 +3942,22 @@ function isClaudeAssistantEvent(value) {
2872
3942
  function key3(hourUtc, model) {
2873
3943
  return `${hourUtc.toISOString()}|${model}`;
2874
3944
  }
2875
- function shouldExcludePath2(path, excludePath) {
2876
- return path !== void 0 && excludePath?.(path) === true;
2877
- }
2878
- function makeClaudeFileAccumulator(options) {
2879
- return {
2880
- buckets: /* @__PURE__ */ new Map(),
2881
- excluded: false,
2882
- lineNo: 0,
2883
- seenTuples: options.seenTuples ?? /* @__PURE__ */ new Set(),
2884
- sessionLastSeenAt: void 0,
2885
- sessionStartedAt: void 0
2886
- };
2887
- }
3945
+ function shouldExcludePath2(path, excludePath) {
3946
+ return path !== void 0 && excludePath?.(path) === true;
3947
+ }
3948
+ function makeClaudeFileCandidateAccumulator() {
3949
+ return { excluded: false, identified: /* @__PURE__ */ new Map(), lineNo: 0, unidentified: [] };
3950
+ }
3951
+ function makeClaudeSessionAccumulator() {
3952
+ return { buckets: /* @__PURE__ */ new Map(), sessionLastSeenAt: void 0, sessionStartedAt: void 0 };
3953
+ }
2888
3954
  function lineStreamFromFile2(path) {
2889
- return Stream9.callback(
2890
- (queue) => Effect16.acquireRelease(
2891
- Effect16.sync(() => {
3955
+ return Stream10.callback(
3956
+ (queue) => Effect17.acquireRelease(
3957
+ Effect17.sync(() => {
2892
3958
  let done = false;
2893
3959
  const stream = createReadStream2(path, { encoding: "utf8" });
2894
- const rl = createInterface3({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
3960
+ const lines = createInterface3({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
2895
3961
  const offer = (line) => {
2896
3962
  if (!done) {
2897
3963
  Queue2.offerUnsafe(queue, line);
@@ -2910,61 +3976,32 @@ function lineStreamFromFile2(path) {
2910
3976
  }
2911
3977
  };
2912
3978
  stream.once("error", fail);
2913
- rl.once("error", fail);
2914
- rl.once("close", end);
2915
- rl.on("line", offer);
3979
+ lines.once("error", fail);
3980
+ lines.once("close", end);
3981
+ lines.on("line", offer);
2916
3982
  return {
2917
3983
  close: () => {
2918
3984
  done = true;
2919
- rl.off("close", end);
2920
- rl.off("error", fail);
2921
- rl.off("line", offer);
3985
+ lines.off("close", end);
3986
+ lines.off("error", fail);
3987
+ lines.off("line", offer);
2922
3988
  stream.off("error", fail);
2923
- rl.close();
3989
+ lines.close();
2924
3990
  stream.destroy();
2925
3991
  }
2926
3992
  };
2927
3993
  }),
2928
- (resource) => Effect16.sync(() => resource.close())
3994
+ (resource) => Effect17.sync(() => resource.close())
2929
3995
  )
2930
3996
  );
2931
3997
  }
2932
- function addToBucket2(acc, hourUtc, model, usage) {
2933
- const inputTokens = Math.max(0, usage.input_tokens ?? 0);
2934
- const outputTokens = Math.max(0, usage.output_tokens ?? 0);
2935
- const cacheReadTokens = Math.max(0, usage.cache_read_input_tokens ?? 0);
2936
- const cacheCreateTokens = Math.max(0, usage.cache_creation_input_tokens ?? 0);
2937
- if (inputTokens === 0 && outputTokens === 0 && cacheReadTokens === 0 && cacheCreateTokens === 0) {
3998
+ function eventIsEligible(timestampMs3, options) {
3999
+ if (options.cutoff !== void 0 && timestampMs3 < options.cutoff.getTime()) {
2938
4000
  return false;
2939
4001
  }
2940
- const k = key3(hourUtc, model);
2941
- const existing = acc.buckets.get(k);
2942
- if (existing === void 0) {
2943
- acc.buckets.set(k, {
2944
- hourUtc,
2945
- model,
2946
- inputTokens,
2947
- outputTokens,
2948
- cacheReadTokens,
2949
- cacheCreateTokens
2950
- });
2951
- return true;
2952
- }
2953
- existing.inputTokens += inputTokens;
2954
- existing.outputTokens += outputTokens;
2955
- existing.cacheReadTokens += cacheReadTokens;
2956
- existing.cacheCreateTokens += cacheCreateTokens;
2957
- return true;
2958
- }
2959
- function markSessionSeen2(acc, eventAt) {
2960
- if (acc.sessionStartedAt === void 0 || eventAt.getTime() < acc.sessionStartedAt.getTime()) {
2961
- acc.sessionStartedAt = eventAt;
2962
- }
2963
- if (acc.sessionLastSeenAt === void 0 || eventAt.getTime() > acc.sessionLastSeenAt.getTime()) {
2964
- acc.sessionLastSeenAt = eventAt;
2965
- }
4002
+ return options.endExclusive === void 0 || timestampMs3 < options.endExclusive.getTime();
2966
4003
  }
2967
- function accumulateClaudeLine(context, acc, raw) {
4004
+ function accumulateClaudeCandidate(context, acc, raw) {
2968
4005
  const { logger, options, path } = context;
2969
4006
  acc.lineNo += 1;
2970
4007
  if (acc.excluded) {
@@ -2986,37 +4023,129 @@ function accumulateClaudeLine(context, acc, raw) {
2986
4023
  }
2987
4024
  if (shouldExcludePath2(parsed.cwd, options.excludePath)) {
2988
4025
  acc.excluded = true;
2989
- acc.buckets.clear();
4026
+ acc.identified.clear();
4027
+ acc.unidentified = [];
2990
4028
  return acc;
2991
4029
  }
2992
- const tsMs = Date.parse(parsed.timestamp);
2993
- if (!Number.isFinite(tsMs)) {
4030
+ const timestampMs3 = Date.parse(parsed.timestamp);
4031
+ if (!(Number.isFinite(timestampMs3) && eventIsEligible(timestampMs3, options))) {
2994
4032
  return acc;
2995
4033
  }
2996
- if (options.cutoff !== void 0 && tsMs < options.cutoff.getTime()) {
2997
- return acc;
4034
+ const tupleKey = parsed.message.id === void 0 || parsed.requestId === void 0 ? void 0 : JSON.stringify([parsed.message.id, parsed.requestId]);
4035
+ const candidate = {
4036
+ eventAt: new Date(timestampMs3),
4037
+ eventOrder: acc.lineNo,
4038
+ isSidechain: parsed.isSidechain === true,
4039
+ model: parsed.message.model,
4040
+ pathRole: options.pathRole ?? "parent",
4041
+ sessionId: options.sessionId ?? path,
4042
+ sourceOrder: normalize3(path),
4043
+ tupleKey,
4044
+ usage: parsed.message.usage
4045
+ };
4046
+ if (tupleKey === void 0) {
4047
+ acc.unidentified.push(candidate);
4048
+ } else {
4049
+ acc.identified.set(tupleKey, candidate);
2998
4050
  }
2999
- const { message, requestId } = parsed;
3000
- if (message.id !== void 0 && requestId !== void 0) {
3001
- const tuple = `${message.id}::${requestId}`;
3002
- if (acc.seenTuples.has(tuple)) {
3003
- return acc;
4051
+ return acc;
4052
+ }
4053
+ function candidatesFromAccumulator2(acc) {
4054
+ return acc.excluded ? [] : [...acc.identified.values(), ...acc.unidentified];
4055
+ }
4056
+ function collectClaudeFileCandidates(path, logger, options = {}) {
4057
+ const context = { logger, options, path };
4058
+ return lineStreamFromFile2(path).pipe(
4059
+ Stream10.runFold(makeClaudeFileCandidateAccumulator, (acc, raw) => accumulateClaudeCandidate(context, acc, raw)),
4060
+ Effect17.map(candidatesFromAccumulator2)
4061
+ );
4062
+ }
4063
+ function normalizedUsage(usage) {
4064
+ return {
4065
+ inputTokens: Math.max(0, usage.input_tokens ?? 0),
4066
+ outputTokens: Math.max(0, usage.output_tokens ?? 0),
4067
+ cacheReadTokens: Math.max(0, usage.cache_read_input_tokens ?? 0),
4068
+ cacheCreateTokens: Math.max(0, usage.cache_creation_input_tokens ?? 0)
4069
+ };
4070
+ }
4071
+ function sameUsage(left, right) {
4072
+ const a = normalizedUsage(left);
4073
+ const b = normalizedUsage(right);
4074
+ return a.inputTokens === b.inputTokens && a.outputTokens === b.outputTokens && a.cacheReadTokens === b.cacheReadTokens && a.cacheCreateTokens === b.cacheCreateTokens;
4075
+ }
4076
+ function candidateWins(next, current) {
4077
+ if (next.model === current.model && sameUsage(next.usage, current.usage)) {
4078
+ if (next.isSidechain !== current.isSidechain) {
4079
+ return !next.isSidechain;
4080
+ }
4081
+ if (next.pathRole !== current.pathRole) {
4082
+ return next.pathRole === "parent";
3004
4083
  }
3005
- acc.seenTuples.add(tuple);
3006
4084
  }
3007
- const eventAt = new Date(tsMs);
3008
- if (addToBucket2(acc, startOfUtcHour(eventAt), message.model, message.usage)) {
3009
- markSessionSeen2(acc, eventAt);
4085
+ const timestampOrder = next.eventAt.getTime() - current.eventAt.getTime();
4086
+ if (timestampOrder !== 0) {
4087
+ return timestampOrder > 0;
3010
4088
  }
3011
- return acc;
4089
+ if (next.eventOrder !== current.eventOrder) {
4090
+ return next.eventOrder > current.eventOrder;
4091
+ }
4092
+ return next.sourceOrder.localeCompare(current.sourceOrder) < 0;
3012
4093
  }
3013
- function bucketsFromAccumulator2(path, options, acc) {
3014
- if (acc.excluded || acc.sessionStartedAt === void 0 || acc.sessionLastSeenAt === void 0) {
4094
+ function reconcileClaudeCandidates(candidates) {
4095
+ const identified = /* @__PURE__ */ new Map();
4096
+ const unidentified = [];
4097
+ for (const candidate of candidates) {
4098
+ if (candidate.tupleKey === void 0) {
4099
+ unidentified.push(candidate);
4100
+ continue;
4101
+ }
4102
+ const current = identified.get(candidate.tupleKey);
4103
+ if (current === void 0 || candidateWins(candidate, current)) {
4104
+ identified.set(candidate.tupleKey, candidate);
4105
+ }
4106
+ }
4107
+ return [...identified.values(), ...unidentified];
4108
+ }
4109
+ function addToBucket2(acc, candidate) {
4110
+ const usage = normalizedUsage(candidate.usage);
4111
+ const { cacheCreateTokens, cacheReadTokens, inputTokens, outputTokens } = usage;
4112
+ if (inputTokens === 0 && outputTokens === 0 && cacheReadTokens === 0 && cacheCreateTokens === 0) {
4113
+ return false;
4114
+ }
4115
+ const hourUtc = startOfUtcHour(candidate.eventAt);
4116
+ const bucketKey = key3(hourUtc, candidate.model);
4117
+ const existing = acc.buckets.get(bucketKey);
4118
+ if (existing === void 0) {
4119
+ acc.buckets.set(bucketKey, {
4120
+ hourUtc,
4121
+ model: candidate.model,
4122
+ inputTokens,
4123
+ outputTokens,
4124
+ cacheReadTokens,
4125
+ cacheCreateTokens
4126
+ });
4127
+ return true;
4128
+ }
4129
+ existing.inputTokens += inputTokens;
4130
+ existing.outputTokens += outputTokens;
4131
+ existing.cacheReadTokens += cacheReadTokens;
4132
+ existing.cacheCreateTokens += cacheCreateTokens;
4133
+ return true;
4134
+ }
4135
+ function markSessionSeen2(acc, eventAt) {
4136
+ if (acc.sessionStartedAt === void 0 || eventAt.getTime() < acc.sessionStartedAt.getTime()) {
4137
+ acc.sessionStartedAt = eventAt;
4138
+ }
4139
+ if (acc.sessionLastSeenAt === void 0 || eventAt.getTime() > acc.sessionLastSeenAt.getTime()) {
4140
+ acc.sessionLastSeenAt = eventAt;
4141
+ }
4142
+ }
4143
+ function bucketsForSession(sessionId, acc) {
4144
+ const { sessionLastSeenAt, sessionStartedAt } = acc;
4145
+ if (sessionStartedAt === void 0 || sessionLastSeenAt === void 0) {
3015
4146
  return [];
3016
4147
  }
3017
- const sessionStartedAt = acc.sessionStartedAt;
3018
- const sessionLastSeenAt = acc.sessionLastSeenAt;
3019
- const sessionIds = /* @__PURE__ */ new Set([options.sessionId ?? path]);
4148
+ const sessionIds = /* @__PURE__ */ new Set([sessionId]);
3020
4149
  return Array.from(acc.buckets.values(), (bucket) => ({
3021
4150
  harness: "claude_code",
3022
4151
  model: bucket.model,
@@ -3030,70 +4159,80 @@ function bucketsFromAccumulator2(path, options, acc) {
3030
4159
  sessionLastSeenAt
3031
4160
  }));
3032
4161
  }
4162
+ function bucketizeClaudeCandidates(candidates) {
4163
+ const sessions = /* @__PURE__ */ new Map();
4164
+ for (const candidate of reconcileClaudeCandidates(candidates)) {
4165
+ const acc = sessions.get(candidate.sessionId) ?? makeClaudeSessionAccumulator();
4166
+ if (addToBucket2(acc, candidate)) {
4167
+ markSessionSeen2(acc, candidate.eventAt);
4168
+ }
4169
+ sessions.set(candidate.sessionId, acc);
4170
+ }
4171
+ return [...sessions.entries()].flatMap(([sessionId, acc]) => bucketsForSession(sessionId, acc));
4172
+ }
3033
4173
  function bucketizeClaudeFileStream(path, logger, options = {}) {
3034
- const context = { logger, options, path };
3035
- return Stream9.unwrap(
3036
- lineStreamFromFile2(path).pipe(
3037
- Stream9.runFold(
3038
- () => makeClaudeFileAccumulator(options),
3039
- (acc, raw) => accumulateClaudeLine(context, acc, raw)
3040
- ),
3041
- Effect16.map((acc) => Stream9.fromIterable(bucketsFromAccumulator2(path, options, acc)))
4174
+ return Stream10.unwrap(
4175
+ collectClaudeFileCandidates(path, logger, options).pipe(
4176
+ Effect17.map((candidates) => Stream10.fromIterable(bucketizeClaudeCandidates(candidates)))
3042
4177
  )
3043
4178
  );
3044
4179
  }
3045
4180
 
3046
4181
  // ../../packages/parsers/src/claude/index.ts
3047
- function sourceSessionId(path) {
4182
+ function sourceContext(path) {
3048
4183
  const fileDir = dirname2(path);
3049
4184
  if (basename(fileDir) !== "subagents") {
3050
- return path;
4185
+ return { pathRole: "parent", sessionId: path };
3051
4186
  }
3052
4187
  const sessionDir = dirname2(fileDir);
3053
- return `${sessionDir}.jsonl`;
4188
+ return { pathRole: "subagent", sessionId: `${sessionDir}.jsonl` };
3054
4189
  }
3055
4190
  function bucketizeClaudeSourcesStream(files, logger, options) {
3056
- return Stream10.suspend(() => {
3057
- const seenTuples = /* @__PURE__ */ new Set();
3058
- return files.pipe(
3059
- Stream10.flatMap(
3060
- (file) => {
3061
- const sessionId = sourceSessionId(file);
3062
- return bucketizeClaudeFileStream(file, logger, {
3063
- cutoff: options.cutoff,
3064
- excludePath: options.excludePath,
3065
- sessionId,
3066
- seenTuples
4191
+ return Stream11.unwrap(
4192
+ files.pipe(
4193
+ Stream11.runCollect,
4194
+ Effect18.map((paths) => [...paths].sort()),
4195
+ Effect18.flatMap(
4196
+ (paths) => Effect18.forEach(paths, (path) => {
4197
+ const source = sourceContext(path);
4198
+ return collectClaudeFileCandidates(path, logger, {
4199
+ ...options,
4200
+ pathRole: source.pathRole,
4201
+ sessionId: source.sessionId
3067
4202
  });
3068
- },
3069
- { concurrency: 1 }
3070
- )
3071
- );
3072
- });
4203
+ })
4204
+ ),
4205
+ Effect18.map((filesCandidates) => Stream11.fromIterable(bucketizeClaudeCandidates(filesCandidates.flat())))
4206
+ )
4207
+ );
3073
4208
  }
3074
- var parseClaudeEffect = Effect17.fn("ClaudeParser.parseClaudeEffect")(
3075
- (opts) => Stream10.runCollect(parseClaudeStream(opts))
4209
+ var parseClaudeEffect = Effect18.fn("ClaudeParser.parseClaudeEffect")(
4210
+ (opts) => Stream11.runCollect(parseClaudeStream(opts))
3076
4211
  );
3077
- var parseClaudeStream = (opts) => Stream10.unwrap(
3078
- Effect17.gen(function* () {
4212
+ var parseClaudeStream = (opts) => Stream11.unwrap(
4213
+ Effect18.gen(function* () {
3079
4214
  const logger = opts.logger ?? silentLogger;
3080
4215
  const root = opts.root ?? resolveClaudeRoot();
3081
4216
  if ((yield* pathKindEffectStrict(root)) === "file") {
4217
+ const source = sourceContext(root);
3082
4218
  return bucketizeClaudeFileStream(root, logger, {
3083
4219
  cutoff: opts.cutoff,
4220
+ endExclusive: opts.endExclusive,
3084
4221
  excludePath: opts.excludePath,
3085
- sessionId: sourceSessionId(root)
4222
+ pathRole: source.pathRole,
4223
+ sessionId: source.sessionId
3086
4224
  });
3087
4225
  }
3088
4226
  return bucketizeClaudeSourcesStream(walkStreamStrict(root, opts.cutoff), logger, {
3089
4227
  cutoff: opts.cutoff,
4228
+ endExclusive: opts.endExclusive,
3090
4229
  excludePath: opts.excludePath
3091
4230
  });
3092
4231
  })
3093
4232
  );
3094
4233
 
3095
4234
  // ../../packages/parsers/src/codex/index.ts
3096
- import { Effect as Effect18, Stream as Stream11 } from "effect";
4235
+ import { Effect as Effect19, Stream as Stream12 } from "effect";
3097
4236
  var codexRoots = (opts, root) => {
3098
4237
  const roots = [root];
3099
4238
  if (opts.root === void 0 && opts.archivedRoot === void 0) {
@@ -3103,52 +4242,134 @@ var codexRoots = (opts, root) => {
3103
4242
  }
3104
4243
  return roots;
3105
4244
  };
3106
- var parseCodexEffect = Effect18.fn("CodexParser.parseCodexEffect")(
3107
- (opts) => Stream11.runCollect(parseCodexStream(opts))
4245
+ function candidateIdentity(candidate) {
4246
+ return [
4247
+ candidate.canonicalSessionIdentity,
4248
+ candidate.turnId ?? "",
4249
+ String(candidate.eventIndex),
4250
+ candidate.hourUtc.toISOString().slice(0, 10),
4251
+ candidate.model,
4252
+ String(candidate.inputTokens),
4253
+ String(candidate.cacheReadTokens),
4254
+ String(candidate.outputTokens)
4255
+ ].join("");
4256
+ }
4257
+ function reconcileCodexCandidates(candidates) {
4258
+ const seen = /* @__PURE__ */ new Set();
4259
+ const winners = [];
4260
+ for (const candidate of candidates) {
4261
+ const identity = candidateIdentity(candidate);
4262
+ if (seen.has(identity)) {
4263
+ continue;
4264
+ }
4265
+ seen.add(identity);
4266
+ winners.push(candidate);
4267
+ }
4268
+ return winners;
4269
+ }
4270
+ function bucketsFromCodexCandidates(candidates) {
4271
+ const buckets = /* @__PURE__ */ new Map();
4272
+ for (const candidate of candidates) {
4273
+ const identity = `${candidate.sessionIdentity}${candidate.hourUtc.toISOString()}${candidate.model}`;
4274
+ const existing = buckets.get(identity);
4275
+ if (existing === void 0) {
4276
+ buckets.set(identity, {
4277
+ cacheReadTokens: candidate.cacheReadTokens,
4278
+ hourUtc: candidate.hourUtc,
4279
+ inputTokens: candidate.inputTokens,
4280
+ model: candidate.model,
4281
+ outputTokens: candidate.outputTokens,
4282
+ sessionIdentity: candidate.sessionIdentity,
4283
+ sessionLastSeenAt: candidate.sessionLastSeenAt,
4284
+ sessionStartedAt: candidate.sessionStartedAt
4285
+ });
4286
+ continue;
4287
+ }
4288
+ existing.inputTokens += candidate.inputTokens;
4289
+ existing.outputTokens += candidate.outputTokens;
4290
+ existing.cacheReadTokens += candidate.cacheReadTokens;
4291
+ }
4292
+ return [...buckets.values()].map((bucket) => ({
4293
+ harness: "codex",
4294
+ model: bucket.model,
4295
+ hourUtc: bucket.hourUtc,
4296
+ inputTokens: bucket.inputTokens,
4297
+ outputTokens: bucket.outputTokens,
4298
+ cacheReadTokens: bucket.cacheReadTokens,
4299
+ cacheCreateTokens: 0,
4300
+ sessionIds: /* @__PURE__ */ new Set([bucket.sessionIdentity]),
4301
+ sessionStartedAt: bucket.sessionStartedAt,
4302
+ sessionLastSeenAt: bucket.sessionLastSeenAt
4303
+ }));
4304
+ }
4305
+ var parseCodexEffect = Effect19.fn("CodexParser.parseCodexEffect")(
4306
+ (opts) => Stream12.runCollect(parseCodexStream(opts))
3108
4307
  );
3109
- var parseCodexStream = (opts) => Stream11.unwrap(
3110
- Effect18.gen(function* () {
4308
+ var parseCodexStream = (opts) => Stream12.unwrap(
4309
+ Effect19.gen(function* () {
3111
4310
  const logger = opts.logger ?? silentLogger;
3112
4311
  const root = opts.root ?? resolveCodexSessionsRoot();
3113
4312
  if ((yield* pathKindEffectStrict(root)) === "file") {
3114
- return bucketizeCodexFileStream(root, logger, { excludePath: opts.excludePath });
4313
+ return bucketizeIndexedCodexFilesStream(Stream12.succeed(root), opts.cutoff, logger, {
4314
+ excludePath: opts.excludePath
4315
+ });
3115
4316
  }
3116
- return bucketizeCodexFilesStream(walkManyStreamStrict(codexRoots(opts, root), opts.cutoff), logger, {
3117
- excludePath: opts.excludePath
3118
- });
4317
+ return bucketizeIndexedCodexFilesStream(
4318
+ walkManyStreamStrict(codexRoots(opts, root), /* @__PURE__ */ new Date(0)),
4319
+ opts.cutoff,
4320
+ logger,
4321
+ { excludePath: opts.excludePath }
4322
+ );
3119
4323
  })
3120
4324
  );
3121
- function bucketizeCodexFilesStream(files, logger, options) {
3122
- return files.pipe(Stream11.flatMap((file) => bucketizeCodexFileStream(file, logger, options)));
4325
+ function bucketizeIndexedCodexFilesStream(files, cutoff, logger, options) {
4326
+ return Stream12.unwrap(
4327
+ files.pipe(
4328
+ Stream12.runCollect,
4329
+ Effect19.map((paths) => [...paths]),
4330
+ Effect19.flatMap((paths) => buildCodexSourceIndex(paths, cutoff, logger)),
4331
+ Effect19.flatMap(
4332
+ (index) => Effect19.gen(function* () {
4333
+ const resolver = makeCodexLineageResolver(index, logger);
4334
+ const usages = yield* Effect19.forEach(
4335
+ index.sources.filter((source) => source.eligible),
4336
+ (source) => resolver.baselineFor(source).pipe(
4337
+ Effect19.flatMap(
4338
+ (lineageBaseline) => codexFileUsageEffect(source.path, logger, {
4339
+ ...options,
4340
+ canonicalSessionIdentity: source.lineageSessionId ?? source.sourceIdentity,
4341
+ containInterleavedTotals: source.containsMultipleSessionIds,
4342
+ lineageBaseline,
4343
+ sessionIdentity: source.sourceIdentity
4344
+ })
4345
+ )
4346
+ )
4347
+ );
4348
+ const candidates = usages.flatMap((usage) => usage.candidates);
4349
+ return Stream12.fromIterable(bucketsFromCodexCandidates(reconcileCodexCandidates(candidates)));
4350
+ })
4351
+ )
4352
+ )
4353
+ );
3123
4354
  }
3124
4355
 
3125
4356
  // ../../packages/parsers/src/effect/hermes-sqlite.ts
3126
- import { Context as Context9, Effect as Effect19, Layer as Layer8, Schema as Schema22 } from "effect";
4357
+ import { Context as Context9, Effect as Effect20, Layer as Layer8, Schema as Schema22 } from "effect";
3127
4358
  var NODE_SQLITE_SPECIFIER = ["node", "sqlite"].join(":");
3128
4359
  var MILLISECONDS_PER_SECOND = 1e3;
3129
- var HERMES_SESSION_QUERY = `
3130
- select
3131
- id,
3132
- model,
3133
- billing_provider as "billingProvider",
3134
- started_at as "startedAt",
3135
- ended_at as "endedAt",
3136
- input_tokens as "inputTokens",
3137
- output_tokens as "outputTokens",
3138
- cache_read_tokens as "cacheReadTokens",
3139
- cache_write_tokens as "cacheWriteTokens",
3140
- cwd
3141
- from sessions
3142
- where coalesce(ended_at, started_at) >= ?
3143
- order by started_at asc, id asc
3144
- `;
3145
4360
  var HermesSqliteError = class extends Schema22.TaggedErrorClass()("HermesSqliteError", {
3146
4361
  message: Schema22.String,
3147
- operation: Schema22.Literals(["load", "open", "query", "close"]),
4362
+ operation: Schema22.Literals(["close", "load", "open", "query", "schema"]),
3148
4363
  path: Schema22.optionalKey(Schema22.String),
3149
4364
  cause: Schema22.optionalKey(Schema22.Defect())
3150
4365
  }) {
3151
4366
  };
4367
+ function unsupportedSchema(message) {
4368
+ return { _tag: "UnsupportedHermesSchema", message };
4369
+ }
4370
+ function isUnsupportedSchema(value) {
4371
+ return typeof value === "object" && value !== null && "_tag" in value && value._tag === "UnsupportedHermesSchema";
4372
+ }
3152
4373
  var HermesSqlite = class extends Context9.Service()("@oopst/parsers/HermesSqlite") {
3153
4374
  };
3154
4375
  var toHermesSqliteError = (operation, path) => (error) => new HermesSqliteError({
@@ -3157,34 +4378,124 @@ var toHermesSqliteError = (operation, path) => (error) => new HermesSqliteError(
3157
4378
  path,
3158
4379
  cause: error
3159
4380
  });
3160
- var loadDatabaseSync = Effect19.fn("HermesSqlite.loadDatabaseSync")(function* () {
3161
- const sqliteModule = yield* Effect19.tryPromise({
4381
+ var loadDatabaseSync = Effect20.fn("HermesSqlite.loadDatabaseSync")(function* () {
4382
+ const sqliteModule = yield* Effect20.tryPromise({
3162
4383
  try: () => import(NODE_SQLITE_SPECIFIER),
3163
4384
  catch: toHermesSqliteError("load")
3164
4385
  });
3165
4386
  return sqliteModule.DatabaseSync;
3166
4387
  });
3167
- var openDatabase = (path, DatabaseSync) => Effect19.try({
4388
+ var openDatabase = (path, DatabaseSync) => Effect20.try({
3168
4389
  try: () => new DatabaseSync(path, { readOnly: true, timeout: 1e3 }),
3169
4390
  catch: toHermesSqliteError("open", path)
3170
4391
  });
3171
- var closeDatabase = (path, db) => Effect19.try({
4392
+ var closeDatabase = (path, db) => Effect20.try({
3172
4393
  try: () => {
3173
4394
  db.close();
3174
4395
  },
3175
4396
  catch: toHermesSqliteError("close", path)
3176
4397
  });
3177
- var querySessionRows = (path, db, cutoff) => Effect19.try({
4398
+ function tableNames(db) {
4399
+ const rows = db.prepare("select name from sqlite_master where type = 'table'").all();
4400
+ return new Set(rows.flatMap((row) => typeof row.name === "string" ? [row.name] : []));
4401
+ }
4402
+ function tableColumns(db, table) {
4403
+ const rows = db.prepare(`pragma table_info(${table})`).all();
4404
+ return new Set(rows.flatMap((row) => typeof row.name === "string" ? [row.name] : []));
4405
+ }
4406
+ function requireColumns(table, columns, required) {
4407
+ const missing = required.filter((column) => !columns.has(column));
4408
+ if (missing.length > 0) {
4409
+ throw unsupportedSchema(`${table} is missing required accounting columns`);
4410
+ }
4411
+ }
4412
+ function selectedColumn(columns, column, alias, qualifier) {
4413
+ const source = qualifier === void 0 ? column : `${qualifier}.${column}`;
4414
+ return columns.has(column) ? `${source} as "${alias}"` : `null as "${alias}"`;
4415
+ }
4416
+ function sessionRows(db, cutoffSeconds) {
4417
+ const columns = tableColumns(db, "sessions");
4418
+ requireColumns("sessions", columns, ["id", "model", "started_at", "ended_at", "input_tokens", "output_tokens"]);
4419
+ const query = `
4420
+ select
4421
+ id,
4422
+ model,
4423
+ ${selectedColumn(columns, "billing_provider", "billingProvider")},
4424
+ started_at as "startedAt",
4425
+ ended_at as "endedAt",
4426
+ input_tokens as "inputTokens",
4427
+ output_tokens as "outputTokens",
4428
+ ${selectedColumn(columns, "cache_read_tokens", "cacheReadTokens")},
4429
+ ${selectedColumn(columns, "cache_write_tokens", "cacheWriteTokens")},
4430
+ ${selectedColumn(columns, "reasoning_tokens", "reasoningTokens")},
4431
+ ${selectedColumn(columns, "cwd", "cwd")}
4432
+ from sessions
4433
+ where ended_at is null or ended_at >= ?
4434
+ order by started_at asc, id asc
4435
+ `;
4436
+ return db.prepare(query).all(cutoffSeconds);
4437
+ }
4438
+ function modelUsageRows(db, cutoffSeconds) {
4439
+ const columns = tableColumns(db, "session_model_usage");
4440
+ requireColumns("session_model_usage", columns, ["session_id", "model", "input_tokens", "output_tokens"]);
4441
+ const query = `
4442
+ select
4443
+ usage.session_id as "sessionId",
4444
+ usage.model,
4445
+ ${selectedColumn(columns, "billing_provider", "billingProvider", "usage")},
4446
+ ${selectedColumn(columns, "first_seen", "firstSeen", "usage")},
4447
+ ${selectedColumn(columns, "last_seen", "lastSeen", "usage")},
4448
+ usage.input_tokens as "inputTokens",
4449
+ usage.output_tokens as "outputTokens",
4450
+ ${selectedColumn(columns, "cache_read_tokens", "cacheReadTokens", "usage")},
4451
+ ${selectedColumn(columns, "cache_write_tokens", "cacheWriteTokens", "usage")},
4452
+ ${selectedColumn(columns, "reasoning_tokens", "reasoningTokens", "usage")}
4453
+ from session_model_usage as usage
4454
+ inner join sessions on sessions.id = usage.session_id
4455
+ where sessions.ended_at is null or sessions.ended_at >= ?
4456
+ order by usage.session_id asc, usage.model asc
4457
+ `;
4458
+ return db.prepare(query).all(cutoffSeconds);
4459
+ }
4460
+ function attachModelUsageRows(sessions, usageRows) {
4461
+ const bySession = /* @__PURE__ */ new Map();
4462
+ for (const row of usageRows) {
4463
+ if (typeof row.sessionId !== "string") {
4464
+ continue;
4465
+ }
4466
+ const rows = bySession.get(row.sessionId) ?? [];
4467
+ rows.push(row);
4468
+ bySession.set(row.sessionId, rows);
4469
+ }
4470
+ return sessions.map((row) => ({
4471
+ ...row,
4472
+ modelUsageRows: typeof row.id === "string" ? bySession.get(row.id) ?? [] : []
4473
+ }));
4474
+ }
4475
+ var querySessionRows = (path, db, cutoff) => Effect20.try({
3178
4476
  try: () => {
3179
- const statement = db.prepare(HERMES_SESSION_QUERY);
3180
- return statement.all(cutoff.getTime() / MILLISECONDS_PER_SECOND);
4477
+ const tables = tableNames(db);
4478
+ if (!tables.has("sessions")) {
4479
+ throw unsupportedSchema("sessions table is unavailable");
4480
+ }
4481
+ const cutoffSeconds = cutoff.getTime() / MILLISECONDS_PER_SECOND;
4482
+ const sessions = sessionRows(db, cutoffSeconds);
4483
+ if (!tables.has("session_model_usage")) {
4484
+ return sessions;
4485
+ }
4486
+ return attachModelUsageRows(sessions, modelUsageRows(db, cutoffSeconds));
3181
4487
  },
3182
- catch: toHermesSqliteError("query", path)
4488
+ catch: (error) => isUnsupportedSchema(error) ? new HermesSqliteError({
4489
+ message: "Hermes SQLite schema does not expose supported accounting aggregates",
4490
+ operation: "schema",
4491
+ path,
4492
+ cause: error
4493
+ }) : toHermesSqliteError("query", path)(error)
3183
4494
  });
3184
4495
  var makeHermesSqlite = () => ({
3185
- readSessionRows: Effect19.fn("HermesSqlite.readSessionRows")(function* (path, cutoff) {
4496
+ readSessionRows: Effect20.fn("HermesSqlite.readSessionRows")(function* (path, cutoff) {
3186
4497
  const DatabaseSync = yield* loadDatabaseSync();
3187
- return yield* Effect19.acquireUseRelease(
4498
+ return yield* Effect20.acquireUseRelease(
3188
4499
  openDatabase(path, DatabaseSync),
3189
4500
  (db) => querySessionRows(path, db, cutoff),
3190
4501
  (db) => closeDatabase(path, db)
@@ -3193,13 +4504,13 @@ var makeHermesSqlite = () => ({
3193
4504
  });
3194
4505
  var HermesSqliteLive = Layer8.effect(
3195
4506
  HermesSqlite,
3196
- Effect19.sync(() => HermesSqlite.of(makeHermesSqlite()))
4507
+ Effect20.sync(() => HermesSqlite.of(makeHermesSqlite()))
3197
4508
  );
3198
4509
 
3199
4510
  // ../../packages/parsers/src/effect/opencode-io.ts
3200
4511
  import { basename as basename2, join as join6 } from "path";
3201
4512
  import { NodeFileSystem as NodeFileSystem5 } from "@effect/platform-node";
3202
- import { Context as Context10, Effect as Effect20, FileSystem as FileSystem6, Layer as Layer9, Option as Option3, Schema as Schema23, Stream as Stream12 } from "effect";
4513
+ import { Context as Context10, Effect as Effect21, FileSystem as FileSystem6, Layer as Layer9, Option as Option3, Schema as Schema23, Stream as Stream13 } from "effect";
3203
4514
  var NODE_SQLITE_SPECIFIER2 = ["node", "sqlite"].join(":");
3204
4515
  var OPENCODE_DB_FILE_PATTERN2 = /^opencode(?:-[^/]+)?\.db$/u;
3205
4516
  var SESSION_MESSAGE_QUERY = `
@@ -3231,13 +4542,13 @@ var toOpenCodeIoError = (operation, path) => (error) => new OpenCodeIoError({
3231
4542
  path,
3232
4543
  cause: error
3233
4544
  });
3234
- var readDir2 = (fs, path) => fs.readDirectory(path).pipe(Effect20.mapError(toOpenCodeIoError("readdir", path)));
3235
- var readDirBestEffort = (fs, path) => readDir2(fs, path).pipe(Effect20.catch(() => Effect20.succeed([])));
3236
- var readStat2 = (fs, path) => fs.stat(path).pipe(Effect20.mapError(toOpenCodeIoError("stat", path)));
3237
- var readStatBestEffort = (fs, path) => readStat2(fs, path).pipe(Effect20.catch(() => Effect20.succeed(void 0)));
3238
- var pathKind = Effect20.fn("OpenCodeIo.pathKind")(function* (fs, path) {
4545
+ var readDir2 = (fs, path) => fs.readDirectory(path).pipe(Effect21.mapError(toOpenCodeIoError("readdir", path)));
4546
+ var readDirBestEffort = (fs, path) => readDir2(fs, path).pipe(Effect21.catch(() => Effect21.succeed([])));
4547
+ var readStat2 = (fs, path) => fs.stat(path).pipe(Effect21.mapError(toOpenCodeIoError("stat", path)));
4548
+ var readStatBestEffort = (fs, path) => readStat2(fs, path).pipe(Effect21.catch(() => Effect21.succeed(void 0)));
4549
+ var pathKind = Effect21.fn("OpenCodeIo.pathKind")(function* (fs, path) {
3239
4550
  return yield* readStat2(fs, path).pipe(
3240
- Effect20.map((info) => {
4551
+ Effect21.map((info) => {
3241
4552
  if (info.type === "File") {
3242
4553
  return "file";
3243
4554
  }
@@ -3246,10 +4557,10 @@ var pathKind = Effect20.fn("OpenCodeIo.pathKind")(function* (fs, path) {
3246
4557
  }
3247
4558
  return "missing";
3248
4559
  }),
3249
- Effect20.catch(() => Effect20.succeed("missing"))
4560
+ Effect21.catch(() => Effect21.succeed("missing"))
3250
4561
  );
3251
4562
  });
3252
- var listOpenCodeDatabases = Effect20.fn("OpenCodeIo.listOpenCodeDatabases")(function* (fs, root) {
4563
+ var listOpenCodeDatabases = Effect21.fn("OpenCodeIo.listOpenCodeDatabases")(function* (fs, root) {
3253
4564
  const entries = yield* readDirBestEffort(fs, root);
3254
4565
  const databasePaths = [];
3255
4566
  for (const entry of entries) {
@@ -3263,7 +4574,7 @@ var listOpenCodeDatabases = Effect20.fn("OpenCodeIo.listOpenCodeDatabases")(func
3263
4574
  }
3264
4575
  return databasePaths;
3265
4576
  });
3266
- var listProjectLegacyRoots = Effect20.fn("OpenCodeIo.listProjectLegacyRoots")(function* (fs, root) {
4577
+ var listProjectLegacyRoots = Effect21.fn("OpenCodeIo.listProjectLegacyRoots")(function* (fs, root) {
3267
4578
  const projectRoot = join6(root, "project");
3268
4579
  const entries = yield* readDirBestEffort(fs, projectRoot);
3269
4580
  const roots = [];
@@ -3275,7 +4586,7 @@ var listProjectLegacyRoots = Effect20.fn("OpenCodeIo.listProjectLegacyRoots")(fu
3275
4586
  }
3276
4587
  return roots;
3277
4588
  });
3278
- var legacyRoots = Effect20.fn("OpenCodeIo.legacyRoots")(function* (fs, root) {
4589
+ var legacyRoots = Effect21.fn("OpenCodeIo.legacyRoots")(function* (fs, root) {
3279
4590
  return [
3280
4591
  join6(root, "storage", "message"),
3281
4592
  join6(root, "storage", "session", "message"),
@@ -3286,26 +4597,26 @@ var isRecent = (info, cutoff) => Option3.match(info.mtime, {
3286
4597
  onNone: () => false,
3287
4598
  onSome: (mtime) => mtime.getTime() >= cutoff.getTime()
3288
4599
  });
3289
- var streamJsonFiles = (fs, root, cutoff) => Stream12.suspend(
3290
- () => Stream12.fromIterableEffect(readDirBestEffort(fs, root)).pipe(
3291
- Stream12.flatMap((entry) => {
4600
+ var streamJsonFiles = (fs, root, cutoff) => Stream13.suspend(
4601
+ () => Stream13.fromIterableEffect(readDirBestEffort(fs, root)).pipe(
4602
+ Stream13.flatMap((entry) => {
3292
4603
  const full = join6(root, entry);
3293
- return Stream12.fromEffect(readStatBestEffort(fs, full)).pipe(
3294
- Stream12.flatMap((info) => {
4604
+ return Stream13.fromEffect(readStatBestEffort(fs, full)).pipe(
4605
+ Stream13.flatMap((info) => {
3295
4606
  if (info?.type === "Directory") {
3296
4607
  return streamJsonFiles(fs, full, cutoff);
3297
4608
  }
3298
4609
  if (info?.type === "File" && entry.endsWith(".json") && isRecent(info, cutoff)) {
3299
- return Stream12.succeed(full);
4610
+ return Stream13.succeed(full);
3300
4611
  }
3301
- return Stream12.empty;
4612
+ return Stream13.empty;
3302
4613
  })
3303
4614
  );
3304
4615
  })
3305
4616
  )
3306
4617
  );
3307
- var streamManyJsonFiles = (fs, roots, cutoff) => Stream12.fromIterable(roots).pipe(Stream12.flatMap((root) => streamJsonFiles(fs, root, cutoff)));
3308
- var discoverSources = Effect20.fn("OpenCodeIo.discoverSources")(function* (fs, root, cutoff) {
4618
+ var streamManyJsonFiles = (fs, roots, cutoff) => Stream13.fromIterable(roots).pipe(Stream13.flatMap((root) => streamJsonFiles(fs, root, cutoff)));
4619
+ var discoverSources = Effect21.fn("OpenCodeIo.discoverSources")(function* (fs, root, cutoff) {
3309
4620
  const kind = yield* pathKind(fs, root);
3310
4621
  if (kind === "missing") {
3311
4622
  return { _tag: "Missing" };
@@ -3324,26 +4635,26 @@ var discoverSources = Effect20.fn("OpenCodeIo.discoverSources")(function* (fs, r
3324
4635
  return { _tag: "LegacyJsonFiles", paths: streamManyJsonFiles(fs, roots, cutoff) };
3325
4636
  });
3326
4637
  var readJsonFile = (fs, path) => fs.readFileString(path, "utf8").pipe(
3327
- Effect20.mapError(toOpenCodeIoError("readFile", path)),
3328
- Effect20.flatMap(
3329
- (text) => Effect20.try({
4638
+ Effect21.mapError(toOpenCodeIoError("readFile", path)),
4639
+ Effect21.flatMap(
4640
+ (text) => Effect21.try({
3330
4641
  try: () => JSON.parse(text),
3331
4642
  catch: toOpenCodeIoError("parseJson", path)
3332
4643
  })
3333
4644
  )
3334
4645
  );
3335
- var loadDatabaseSync2 = Effect20.fn("OpenCodeIo.loadDatabaseSync")(function* () {
3336
- const sqliteModule = yield* Effect20.tryPromise({
4646
+ var loadDatabaseSync2 = Effect21.fn("OpenCodeIo.loadDatabaseSync")(function* () {
4647
+ const sqliteModule = yield* Effect21.tryPromise({
3337
4648
  try: () => import(NODE_SQLITE_SPECIFIER2),
3338
4649
  catch: toOpenCodeIoError("load")
3339
4650
  });
3340
4651
  return sqliteModule.DatabaseSync;
3341
4652
  });
3342
- var openDatabase2 = (path, DatabaseSync) => Effect20.try({
4653
+ var openDatabase2 = (path, DatabaseSync) => Effect21.try({
3343
4654
  try: () => new DatabaseSync(path, { readOnly: true, timeout: 1e3 }),
3344
4655
  catch: toOpenCodeIoError("open", path)
3345
4656
  });
3346
- var closeDatabase2 = (path, db) => Effect20.try({
4657
+ var closeDatabase2 = (path, db) => Effect21.try({
3347
4658
  try: () => {
3348
4659
  db.close();
3349
4660
  },
@@ -3368,7 +4679,7 @@ function readSessionPaths(db) {
3368
4679
  }
3369
4680
  return paths;
3370
4681
  }
3371
- var querySqliteRows = (path, db, cutoff) => Effect20.try({
4682
+ var querySqliteRows = (path, db, cutoff) => Effect21.try({
3372
4683
  try: () => {
3373
4684
  const statement = db.prepare(SESSION_MESSAGE_QUERY);
3374
4685
  return {
@@ -3378,9 +4689,9 @@ var querySqliteRows = (path, db, cutoff) => Effect20.try({
3378
4689
  },
3379
4690
  catch: toOpenCodeIoError("query", path)
3380
4691
  });
3381
- var readSqlite = Effect20.fn("OpenCodeIo.readSqlite")(function* (path, cutoff) {
4692
+ var readSqlite = Effect21.fn("OpenCodeIo.readSqlite")(function* (path, cutoff) {
3382
4693
  const DatabaseSync = yield* loadDatabaseSync2();
3383
- return yield* Effect20.acquireUseRelease(
4694
+ return yield* Effect21.acquireUseRelease(
3384
4695
  openDatabase2(path, DatabaseSync),
3385
4696
  (db) => querySqliteRows(path, db, cutoff),
3386
4697
  (db) => closeDatabase2(path, db)
@@ -3393,17 +4704,17 @@ var makeOpenCodeIo = (fs) => ({
3393
4704
  });
3394
4705
  var OpenCodeIoLive = Layer9.effect(
3395
4706
  OpenCodeIo,
3396
- Effect20.gen(function* () {
4707
+ Effect21.gen(function* () {
3397
4708
  const fs = yield* FileSystem6.FileSystem;
3398
4709
  return OpenCodeIo.of(makeOpenCodeIo(fs));
3399
4710
  })
3400
4711
  ).pipe(Layer9.provide(NodeFileSystem5.layer));
3401
4712
 
3402
4713
  // ../../packages/parsers/src/opencode/index.ts
3403
- import { Effect as Effect22, Stream as Stream13 } from "effect";
4714
+ import { Effect as Effect23, Stream as Stream14 } from "effect";
3404
4715
 
3405
4716
  // ../../packages/parsers/src/opencode/bucketize.ts
3406
- import { Effect as Effect21 } from "effect";
4717
+ import { Effect as Effect22 } from "effect";
3407
4718
 
3408
4719
  // ../../packages/parsers/src/opencode/events.ts
3409
4720
  function isObject3(value) {
@@ -3624,7 +4935,7 @@ function* finalizedBuckets(accumulators) {
3624
4935
  function parseJson(value) {
3625
4936
  return typeof value === "string" ? JSON.parse(value) : value;
3626
4937
  }
3627
- var bucketizeOpenCodeDatabaseEffect = Effect21.fn("bucketizeOpenCodeDatabaseEffect")(
4938
+ var bucketizeOpenCodeDatabaseEffect = Effect22.fn("bucketizeOpenCodeDatabaseEffect")(
3628
4939
  function* (path, cutoff, logger, excludePath) {
3629
4940
  const io = yield* OpenCodeIo;
3630
4941
  const rows = yield* io.readSqlite(path, cutoff);
@@ -3647,7 +4958,7 @@ var bucketizeOpenCodeDatabaseEffect = Effect21.fn("bucketizeOpenCodeDatabaseEffe
3647
4958
  return [...finalizedBuckets(accumulators)];
3648
4959
  }
3649
4960
  );
3650
- var bucketizeOpenCodeJsonFileEffect = Effect21.fn("bucketizeOpenCodeJsonFileEffect")(
4961
+ var bucketizeOpenCodeJsonFileEffect = Effect22.fn("bucketizeOpenCodeJsonFileEffect")(
3651
4962
  function* (path) {
3652
4963
  const io = yield* OpenCodeIo;
3653
4964
  const parsed = yield* io.readJsonFile(path);
@@ -3662,23 +4973,23 @@ var bucketizeOpenCodeJsonFileEffect = Effect21.fn("bucketizeOpenCodeJsonFileEffe
3662
4973
  );
3663
4974
 
3664
4975
  // ../../packages/parsers/src/opencode/index.ts
3665
- var discoverOpenCodeSourcesEffect = Effect22.fn("discoverOpenCodeSourcesEffect")(function* (root, cutoff) {
4976
+ var discoverOpenCodeSourcesEffect = Effect23.fn("discoverOpenCodeSourcesEffect")(function* (root, cutoff) {
3666
4977
  const io = yield* OpenCodeIo;
3667
4978
  return yield* io.discoverSources(root, cutoff);
3668
4979
  });
3669
- var parseOpenCodeEffect = Effect22.fn(
4980
+ var parseOpenCodeEffect = Effect23.fn(
3670
4981
  "OpenCodeParser.parseOpenCodeEffect"
3671
- )((opts) => Stream13.runCollect(parseOpenCodeStream(opts)));
3672
- var parseOpenCodeStream = (opts) => Stream13.unwrap(
3673
- Effect22.gen(function* () {
4982
+ )((opts) => Stream14.runCollect(parseOpenCodeStream(opts)));
4983
+ var parseOpenCodeStream = (opts) => Stream14.unwrap(
4984
+ Effect23.gen(function* () {
3674
4985
  const logger = opts.logger ?? silentLogger;
3675
4986
  const root = opts.root ?? resolveOpenCodeRoot();
3676
4987
  const sources = yield* discoverOpenCodeSourcesEffect(root, opts.cutoff);
3677
4988
  if (sources._tag === "Missing") {
3678
- return Stream13.empty;
4989
+ return Stream14.empty;
3679
4990
  }
3680
4991
  if (sources._tag === "JsonFile") {
3681
- return openCodeJsonFileStreams(Stream13.succeed(sources.path));
4992
+ return openCodeJsonFileStreams(Stream14.succeed(sources.path));
3682
4993
  }
3683
4994
  if (sources._tag === "DatabaseFiles") {
3684
4995
  return openCodeDatabaseStreams(sources.paths, opts, logger);
@@ -3687,28 +4998,28 @@ var parseOpenCodeStream = (opts) => Stream13.unwrap(
3687
4998
  })
3688
4999
  );
3689
5000
  function openCodeDatabaseStreams(paths, opts, logger) {
3690
- let stream = Stream13.empty;
5001
+ let stream = Stream14.empty;
3691
5002
  for (const dbPath of paths) {
3692
5003
  stream = stream.pipe(
3693
- Stream13.concat(
3694
- Stream13.fromIterableEffect(bucketizeOpenCodeDatabaseEffect(dbPath, opts.cutoff, logger, opts.excludePath))
5004
+ Stream14.concat(
5005
+ Stream14.fromIterableEffect(bucketizeOpenCodeDatabaseEffect(dbPath, opts.cutoff, logger, opts.excludePath))
3695
5006
  )
3696
5007
  );
3697
5008
  }
3698
5009
  return stream;
3699
5010
  }
3700
5011
  function openCodeJsonFileStreams(paths) {
3701
- return paths.pipe(Stream13.flatMap((file) => Stream13.fromIterableEffect(bucketizeOpenCodeJsonFileEffect(file))));
5012
+ return paths.pipe(Stream14.flatMap((file) => Stream14.fromIterableEffect(bucketizeOpenCodeJsonFileEffect(file))));
3702
5013
  }
3703
5014
 
3704
5015
  // ../../packages/parsers/src/pi/index.ts
3705
5016
  import { basename as basename3, join as join7 } from "path";
3706
- import { Effect as Effect24, Layer as Layer10, Stream as Stream15 } from "effect";
5017
+ import { Effect as Effect25, Layer as Layer10, Stream as Stream16 } from "effect";
3707
5018
 
3708
5019
  // ../../packages/parsers/src/pi/bucketize.ts
3709
5020
  import { createReadStream as createReadStream3 } from "fs";
3710
5021
  import { createInterface as createInterface4 } from "readline";
3711
- import { Cause as Cause3, Effect as Effect23, Queue as Queue3, Stream as Stream14 } from "effect";
5022
+ import { Cause as Cause3, Effect as Effect24, Queue as Queue3, Stream as Stream15 } from "effect";
3712
5023
 
3713
5024
  // ../../packages/parsers/src/pi/events.ts
3714
5025
  function isObject4(value) {
@@ -3787,9 +5098,9 @@ function key5(hourUtc, model) {
3787
5098
  return `${hourUtc.toISOString()}|${model}`;
3788
5099
  }
3789
5100
  function lineStreamFromFile3(path) {
3790
- return Stream14.callback(
3791
- (queue) => Effect23.acquireRelease(
3792
- Effect23.sync(() => {
5101
+ return Stream15.callback(
5102
+ (queue) => Effect24.acquireRelease(
5103
+ Effect24.sync(() => {
3793
5104
  let done = false;
3794
5105
  const stream = createReadStream3(path, { encoding: "utf8" });
3795
5106
  const rl = createInterface4({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
@@ -3826,7 +5137,7 @@ function lineStreamFromFile3(path) {
3826
5137
  }
3827
5138
  };
3828
5139
  }),
3829
- (resource) => Effect23.sync(() => resource.close())
5140
+ (resource) => Effect24.sync(() => resource.close())
3830
5141
  )
3831
5142
  );
3832
5143
  }
@@ -3844,6 +5155,33 @@ function modelId2(provider, model) {
3844
5155
  }
3845
5156
  return `${trimmedProvider}/${trimmedModel}`;
3846
5157
  }
5158
+ function hermesModelId(provider, model, fallbackProvider) {
5159
+ if (typeof model !== "string") {
5160
+ return void 0;
5161
+ }
5162
+ const trimmedModel = model.trim();
5163
+ if (trimmedModel.length === 0) {
5164
+ return void 0;
5165
+ }
5166
+ const directProvider = typeof provider === "string" ? provider.trim() : "";
5167
+ const fallback = typeof fallbackProvider === "string" ? fallbackProvider.trim() : "";
5168
+ const sourceProvider = directProvider.length > 0 ? directProvider : fallback;
5169
+ if (sourceProvider.length > 0) {
5170
+ const normalizedProvider = normalizeProvider(sourceProvider);
5171
+ if (trimmedModel.startsWith(`${sourceProvider}/`)) {
5172
+ return `${normalizedProvider}/${trimmedModel.slice(sourceProvider.length + 1)}`;
5173
+ }
5174
+ if (trimmedModel.startsWith(`${normalizedProvider}/`)) {
5175
+ return trimmedModel;
5176
+ }
5177
+ return `${normalizedProvider}/${trimmedModel}`;
5178
+ }
5179
+ const separator = trimmedModel.indexOf("/");
5180
+ if (separator <= 0) {
5181
+ return void 0;
5182
+ }
5183
+ return `${normalizeProvider(trimmedModel.slice(0, separator))}/${trimmedModel.slice(separator + 1)}`;
5184
+ }
3847
5185
  function normalizeProvider(provider) {
3848
5186
  return provider === "openai-codex" ? "openai" : provider;
3849
5187
  }
@@ -3860,53 +5198,134 @@ function timestampMs2(value) {
3860
5198
  }
3861
5199
  return Math.abs(n) < UNIX_MILLISECONDS_THRESHOLD ? n * MILLISECONDS_PER_SECOND2 : n;
3862
5200
  }
3863
- function hermesRowToBucket(row, excludePath) {
3864
- if (typeof row.cwd === "string" && excludePath?.(row.cwd) === true) {
3865
- return void 0;
3866
- }
3867
- const sessionId = typeof row.id === "string" ? row.id : void 0;
3868
- const startedAtMs = timestampMs2(row.startedAt);
3869
- const lastSeenAtMs = timestampMs2(row.endedAt) ?? startedAtMs;
3870
- const provider = typeof row.billingProvider === "string" ? row.billingProvider : void 0;
3871
- const sourceModel = typeof row.model === "string" ? row.model : void 0;
3872
- if (sessionId === void 0 || startedAtMs === void 0 || lastSeenAtMs === void 0 || provider === void 0 || sourceModel === void 0) {
3873
- return void 0;
3874
- }
3875
- const model = modelId2(provider, sourceModel);
3876
- if (model === void 0) {
3877
- return void 0;
5201
+ function normalizeHermesUsage(row) {
5202
+ return {
5203
+ inputTokens: positive2(row.inputTokens),
5204
+ outputTokens: positive2(row.outputTokens),
5205
+ cacheReadTokens: positive2(row.cacheReadTokens),
5206
+ cacheCreateTokens: positive2(row.cacheWriteTokens)
5207
+ };
5208
+ }
5209
+ function addHermesUsage(left, right) {
5210
+ return {
5211
+ inputTokens: left.inputTokens + right.inputTokens,
5212
+ outputTokens: left.outputTokens + right.outputTokens,
5213
+ cacheReadTokens: left.cacheReadTokens + right.cacheReadTokens,
5214
+ cacheCreateTokens: left.cacheCreateTokens + right.cacheCreateTokens
5215
+ };
5216
+ }
5217
+ function subtractHermesUsageFloorZero(current, baseline) {
5218
+ return {
5219
+ inputTokens: Math.max(0, current.inputTokens - baseline.inputTokens),
5220
+ outputTokens: Math.max(0, current.outputTokens - baseline.outputTokens),
5221
+ cacheReadTokens: Math.max(0, current.cacheReadTokens - baseline.cacheReadTokens),
5222
+ cacheCreateTokens: Math.max(0, current.cacheCreateTokens - baseline.cacheCreateTokens)
5223
+ };
5224
+ }
5225
+ function hasHermesUsage(usage) {
5226
+ return usage.inputTokens > 0 || usage.outputTokens > 0 || usage.cacheReadTokens > 0 || usage.cacheCreateTokens > 0;
5227
+ }
5228
+ function maxModelLastSeen(rows) {
5229
+ let latest;
5230
+ for (const row of rows) {
5231
+ const at = timestampMs2(row.lastSeen) ?? timestampMs2(row.firstSeen);
5232
+ if (at !== void 0 && (latest === void 0 || at > latest)) {
5233
+ latest = at;
5234
+ }
3878
5235
  }
3879
- const inputTokens = positive2(row.inputTokens);
3880
- const outputTokens = positive2(row.outputTokens);
3881
- const cacheReadTokens = positive2(row.cacheReadTokens);
3882
- const cacheCreateTokens = positive2(row.cacheWriteTokens);
3883
- if (inputTokens === 0 && outputTokens === 0 && cacheReadTokens === 0 && cacheCreateTokens === 0) {
5236
+ return latest;
5237
+ }
5238
+ function makeHermesBucket(options) {
5239
+ if (!hasHermesUsage(options.usage)) {
3884
5240
  return void 0;
3885
5241
  }
3886
- const startedAt = new Date(startedAtMs);
5242
+ const startedAt = new Date(options.startedAtMs);
3887
5243
  return {
3888
5244
  harness: "hermes",
3889
- model,
3890
- hourUtc: startOfUtcHour(startedAt),
3891
- inputTokens,
3892
- outputTokens,
3893
- cacheReadTokens,
3894
- cacheCreateTokens,
3895
- sessionIds: /* @__PURE__ */ new Set([`hermes:${sessionId}`]),
5245
+ model: options.model,
5246
+ hourUtc: startOfUtcHour(new Date(options.attributedAtMs)),
5247
+ inputTokens: options.usage.inputTokens,
5248
+ outputTokens: options.usage.outputTokens,
5249
+ cacheReadTokens: options.usage.cacheReadTokens,
5250
+ cacheCreateTokens: options.usage.cacheCreateTokens,
5251
+ sessionIds: /* @__PURE__ */ new Set([`hermes:${options.sessionId}`]),
3896
5252
  sessionStartedAt: startedAt,
3897
- sessionLastSeenAt: new Date(lastSeenAtMs)
5253
+ sessionLastSeenAt: new Date(options.lastSeenAtMs)
3898
5254
  };
3899
5255
  }
3900
- function bucketizeHermesRows(rows, excludePath) {
5256
+ function bucketizeHermesModelUsage(options) {
3901
5257
  const buckets = [];
3902
- for (const row of rows) {
3903
- const bucket = hermesRowToBucket(row, excludePath);
5258
+ let attributedUsage = {
5259
+ cacheCreateTokens: 0,
5260
+ cacheReadTokens: 0,
5261
+ inputTokens: 0,
5262
+ outputTokens: 0
5263
+ };
5264
+ for (const usageRow of options.rows) {
5265
+ const model = hermesModelId(usageRow.billingProvider, usageRow.model, options.fallbackProvider);
5266
+ if (model === void 0) {
5267
+ continue;
5268
+ }
5269
+ const usage = normalizeHermesUsage(usageRow);
5270
+ const bucket = makeHermesBucket({
5271
+ attributedAtMs: timestampMs2(usageRow.firstSeen) ?? options.startedAtMs,
5272
+ lastSeenAtMs: options.lastSeenAtMs,
5273
+ model,
5274
+ sessionId: options.sessionId,
5275
+ startedAtMs: options.startedAtMs,
5276
+ usage
5277
+ });
3904
5278
  if (bucket !== void 0) {
3905
5279
  buckets.push(bucket);
5280
+ attributedUsage = addHermesUsage(attributedUsage, usage);
5281
+ }
5282
+ }
5283
+ return { attributedUsage, buckets };
5284
+ }
5285
+ function hermesRowToBuckets(row, excludePath) {
5286
+ if (typeof row.cwd === "string" && excludePath?.(row.cwd) === true) {
5287
+ return [];
5288
+ }
5289
+ const sessionId = typeof row.id === "string" ? row.id : void 0;
5290
+ const startedAtMs = timestampMs2(row.startedAt);
5291
+ if (sessionId === void 0 || startedAtMs === void 0) {
5292
+ return [];
5293
+ }
5294
+ const modelUsageRows2 = row.modelUsageRows ?? [];
5295
+ const lastSeenAtMs = timestampMs2(row.endedAt) ?? maxModelLastSeen(modelUsageRows2) ?? startedAtMs;
5296
+ const modelUsage = bucketizeHermesModelUsage({
5297
+ fallbackProvider: row.billingProvider,
5298
+ lastSeenAtMs,
5299
+ rows: modelUsageRows2,
5300
+ sessionId,
5301
+ startedAtMs
5302
+ });
5303
+ const buckets = [...modelUsage.buckets];
5304
+ const aggregateUsage = normalizeHermesUsage(row);
5305
+ const residualUsage = subtractHermesUsageFloorZero(aggregateUsage, modelUsage.attributedUsage);
5306
+ const residualModel = hermesModelId(row.billingProvider, row.model);
5307
+ if (residualModel !== void 0) {
5308
+ const residual = makeHermesBucket({
5309
+ attributedAtMs: startedAtMs,
5310
+ lastSeenAtMs,
5311
+ model: residualModel,
5312
+ sessionId,
5313
+ startedAtMs,
5314
+ usage: residualUsage
5315
+ });
5316
+ if (residual !== void 0) {
5317
+ buckets.push(residual);
3906
5318
  }
3907
5319
  }
3908
5320
  return buckets;
3909
5321
  }
5322
+ function bucketizeHermesRows(rows, excludePath) {
5323
+ const buckets = [];
5324
+ for (const row of rows) {
5325
+ buckets.push(...hermesRowToBuckets(row, excludePath));
5326
+ }
5327
+ return buckets;
5328
+ }
3910
5329
  function makePiStyleFileAccumulator(options) {
3911
5330
  return {
3912
5331
  buckets: /* @__PURE__ */ new Map(),
@@ -3999,12 +5418,11 @@ function accumulatePiStyleLine(context, acc, raw) {
3999
5418
  existing.cacheCreateTokens += cacheCreateTokens;
4000
5419
  return acc;
4001
5420
  }
4002
- function bucketsFromAccumulator3(path, harness, acc) {
5421
+ function bucketsFromAccumulator2(path, harness, acc) {
4003
5422
  if (acc.excluded || acc.sessionStartedAt === void 0 || acc.sessionLastSeenAt === void 0) {
4004
5423
  return [];
4005
5424
  }
4006
- const sessionStartedAt = acc.sessionStartedAt;
4007
- const sessionLastSeenAt = acc.sessionLastSeenAt;
5425
+ const { sessionLastSeenAt, sessionStartedAt } = acc;
4008
5426
  const sessionIds = /* @__PURE__ */ new Set([path]);
4009
5427
  return Array.from(acc.buckets.values(), (bucket) => ({
4010
5428
  harness,
@@ -4021,17 +5439,17 @@ function bucketsFromAccumulator3(path, harness, acc) {
4021
5439
  }
4022
5440
  function bucketizePiStyleFileStream(path, harness, logger, options = {}) {
4023
5441
  const context = { logger, options, path };
4024
- return Stream14.unwrap(
5442
+ return Stream15.unwrap(
4025
5443
  lineStreamFromFile3(path).pipe(
4026
- Stream14.runFold(
5444
+ Stream15.runFold(
4027
5445
  () => makePiStyleFileAccumulator(options),
4028
5446
  (acc, raw) => accumulatePiStyleLine(context, acc, raw)
4029
5447
  ),
4030
- Effect23.map((acc) => Stream14.fromIterable(bucketsFromAccumulator3(path, harness, acc)))
5448
+ Effect24.map((acc) => Stream15.fromIterable(bucketsFromAccumulator2(path, harness, acc)))
4031
5449
  )
4032
5450
  );
4033
5451
  }
4034
- var bucketizeHermesDatabaseEffect = Effect23.fn(
5452
+ var bucketizeHermesDatabaseEffect = Effect24.fn(
4035
5453
  "bucketizeHermesDatabaseEffect"
4036
5454
  )(function* (path, cutoff, excludePath) {
4037
5455
  const sqlite = yield* HermesSqlite;
@@ -4040,14 +5458,14 @@ var bucketizeHermesDatabaseEffect = Effect23.fn(
4040
5458
  });
4041
5459
 
4042
5460
  // ../../packages/parsers/src/pi/index.ts
4043
- var isFileBestEffortEffect = (path) => pathKindEffect(path).pipe(Effect24.map((kind) => kind === "file"));
5461
+ var isFileBestEffortEffect = (path) => pathKindEffect(path).pipe(Effect25.map((kind) => kind === "file"));
4044
5462
  function bucketizePiStyleSourcesStream(files, harness, logger, options) {
4045
5463
  return files.pipe(
4046
- Stream15.flatMap((file) => bucketizePiStyleFileStream(file, harness, logger, options), { concurrency: 1 })
5464
+ Stream16.flatMap((file) => bucketizePiStyleFileStream(file, harness, logger, options), { concurrency: 1 })
4047
5465
  );
4048
5466
  }
4049
- var parsePiStyleStream = (opts, harness, defaultRoot) => Stream15.unwrap(
4050
- Effect24.gen(function* () {
5467
+ var parsePiStyleStream = (opts, harness, defaultRoot) => Stream16.unwrap(
5468
+ Effect25.gen(function* () {
4051
5469
  const logger = opts.logger ?? silentLogger;
4052
5470
  const root = opts.root ?? defaultRoot();
4053
5471
  const kind = yield* pathKindEffectStrict(root);
@@ -4065,25 +5483,23 @@ var parsePiStyleStream = (opts, harness, defaultRoot) => Stream15.unwrap(
4065
5483
  });
4066
5484
  })
4067
5485
  );
4068
- var parsePiEffect = Effect24.fn("PiParser.parsePiEffect")((opts) => Stream15.runCollect(parsePiStream(opts)));
5486
+ var parsePiEffect = Effect25.fn("PiParser.parsePiEffect")((opts) => Stream16.runCollect(parsePiStream(opts)));
4069
5487
  var parsePiStream = (opts) => parsePiStyleStream(opts, "pi", resolvePiRoot);
4070
5488
  var parserLive = Layer10.mergeAll(FileWalkerLive, HermesSqliteLive);
4071
- var parseHermesEffect = Effect24.fn(
5489
+ var parseHermesEffect = Effect25.fn(
4072
5490
  "HermesParser.parseHermesEffect"
4073
- )((opts) => Stream15.runCollect(parseHermesStream(opts)));
4074
- var parseHermesStream = (opts) => Stream15.unwrap(
4075
- Effect24.gen(function* () {
5491
+ )((opts) => Stream16.runCollect(parseHermesStream(opts)));
5492
+ var parseHermesStream = (opts) => Stream16.unwrap(
5493
+ Effect25.gen(function* () {
4076
5494
  const logger = opts.logger ?? silentLogger;
4077
5495
  const root = opts.root ?? resolveHermesRoot();
4078
5496
  const kind = yield* pathKindEffectStrict(root);
4079
5497
  if (kind === "missing") {
4080
- return asHermesStream(Stream15.empty);
5498
+ return asHermesStream(Stream16.empty);
4081
5499
  }
4082
5500
  if (kind === "file") {
4083
5501
  if (basename3(root).endsWith(".db")) {
4084
- return asHermesStream(
4085
- Stream15.fromIterableEffect(bucketizeHermesDatabaseEffect(root, opts.cutoff, opts.excludePath))
4086
- );
5502
+ return asHermesStream(collectHermesDatabaseStream(root, logger, opts));
4087
5503
  }
4088
5504
  return asHermesStream(
4089
5505
  bucketizePiStyleFileStream(root, "hermes", logger, { cutoff: opts.cutoff, excludePath: opts.excludePath })
@@ -4091,9 +5507,7 @@ var parseHermesStream = (opts) => Stream15.unwrap(
4091
5507
  }
4092
5508
  const stateDb = join7(root, "state.db");
4093
5509
  if (yield* isFileBestEffortEffect(stateDb)) {
4094
- return asHermesStream(
4095
- Stream15.fromIterableEffect(bucketizeHermesDatabaseEffect(stateDb, opts.cutoff, opts.excludePath))
4096
- );
5510
+ return asHermesStream(collectHermesDatabaseStream(stateDb, logger, opts));
4097
5511
  }
4098
5512
  const sessionsRoot2 = join7(root, "sessions");
4099
5513
  if ((yield* pathKindEffect(sessionsRoot2)) === "directory") {
@@ -4109,8 +5523,20 @@ var parseHermesStream = (opts) => Stream15.unwrap(
4109
5523
  function asHermesStream(stream) {
4110
5524
  return stream;
4111
5525
  }
4112
- var collectHermesPiStyleDirectoryStream = (root, logger, opts) => Stream15.unwrap(
4113
- Effect24.sync(() => {
5526
+ function collectHermesDatabaseStream(path, logger, opts) {
5527
+ return Stream16.fromIterableEffect(bucketizeHermesDatabaseEffect(path, opts.cutoff, opts.excludePath)).pipe(
5528
+ Stream16.catchTag("HermesSqliteError", (error) => {
5529
+ if (error.operation !== "schema") {
5530
+ return Stream16.fail(error);
5531
+ }
5532
+ return Stream16.fromEffectDrain(
5533
+ Effect25.sync(() => logger.warn("unsupported hermes sqlite schema", { reason: "unsupported_schema" }))
5534
+ );
5535
+ })
5536
+ );
5537
+ }
5538
+ var collectHermesPiStyleDirectoryStream = (root, logger, opts) => Stream16.unwrap(
5539
+ Effect25.sync(() => {
4114
5540
  const seenMessageIds = /* @__PURE__ */ new Set();
4115
5541
  return bucketizePiStyleSourcesStream(walkStreamStrict(root, opts.cutoff), "hermes", logger, {
4116
5542
  cutoff: opts.cutoff,
@@ -4121,7 +5547,7 @@ var collectHermesPiStyleDirectoryStream = (root, logger, opts) => Stream15.unwra
4121
5547
  );
4122
5548
 
4123
5549
  // src/sync/parser-factory.ts
4124
- import { Effect as Effect25, Layer as Layer11, Stream as Stream16 } from "effect";
5550
+ import { Effect as Effect26, Layer as Layer11, Stream as Stream17 } from "effect";
4125
5551
  var GLOB_PATTERN = /[*?[\]{}]/u;
4126
5552
  var HermesParserLive = Layer11.mergeAll(FileWalkerLive, HermesSqliteLive);
4127
5553
  function sessionsRoot(agentDir, sessionDir, fallbackAgentDir) {
@@ -4186,7 +5612,7 @@ function createExcludeMatcher(options) {
4186
5612
  };
4187
5613
  }
4188
5614
  function resolveParserSourcePlanEffect(options = {}) {
4189
- return Effect25.gen(function* () {
5615
+ return Effect26.gen(function* () {
4190
5616
  const sources = yield* ParserSources;
4191
5617
  const home = yield* sources.homeDir;
4192
5618
  const env = options.env ?? (yield* sources.cliEnv);
@@ -4225,49 +5651,43 @@ function resolveParserSourcePlanEffect(options = {}) {
4225
5651
  });
4226
5652
  }
4227
5653
  function provideFileWalker(stream) {
4228
- return stream.pipe(Stream16.provide(FileWalkerLive));
4229
- }
4230
- function concatParserStreams(streams) {
4231
- let stream = Stream16.empty;
4232
- for (const next of streams) {
4233
- stream = stream.pipe(Stream16.concat(next));
5654
+ return stream.pipe(Stream17.provide(FileWalkerLive));
5655
+ }
5656
+ function codexStreamForPlan(plan, cutoff, excludePath) {
5657
+ if (plan.codexSessionsSourceExisted) {
5658
+ return provideFileWalker(
5659
+ parseCodexStream({
5660
+ archivedRoot: plan.codexArchivedSourceExisted ? plan.codexArchivedRoot : void 0,
5661
+ cutoff,
5662
+ excludePath,
5663
+ root: plan.codexSessionsRoot
5664
+ })
5665
+ );
4234
5666
  }
4235
- return stream;
5667
+ if (plan.codexArchivedSourceExisted) {
5668
+ return provideFileWalker(
5669
+ parseCodexStream({ cutoff, excludePath, root: plan.codexArchivedRoot })
5670
+ );
5671
+ }
5672
+ return Stream17.empty;
4236
5673
  }
4237
5674
  function buildFactoryFromPlan(plan) {
4238
5675
  const excludePath = createExcludeMatcher({ exclude: plan.exclude, home: plan.home });
4239
- return (ctx) => {
4240
- const codexStreams = [];
4241
- if (plan.codexSessionsSourceExisted) {
4242
- codexStreams.push(
4243
- provideFileWalker(
4244
- parseCodexStream({ cutoff: ctx.cutoff, excludePath, root: plan.codexSessionsRoot })
4245
- )
4246
- );
4247
- }
4248
- if (plan.codexArchivedSourceExisted) {
4249
- codexStreams.push(
4250
- provideFileWalker(
4251
- parseCodexStream({ cutoff: ctx.cutoff, excludePath, root: plan.codexArchivedRoot })
4252
- )
4253
- );
4254
- }
4255
- return {
4256
- anySourceExisted: plan.anySourceExisted,
4257
- codex: concatParserStreams(codexStreams),
4258
- claudeCode: plan.claudeSourceExisted ? provideFileWalker(parseClaudeStream({ cutoff: ctx.cutoff, excludePath, root: plan.claudeRoot })) : Stream16.empty,
4259
- pi: plan.piSourceExisted ? provideFileWalker(parsePiStream({ cutoff: ctx.cutoff, excludePath, root: plan.piRoot })) : Stream16.empty,
4260
- hermes: plan.hermesSourceExisted ? parseHermesStream({ cutoff: ctx.cutoff, excludePath, root: plan.hermesRoot }).pipe(
4261
- Stream16.provide(HermesParserLive)
4262
- ) : Stream16.empty,
4263
- opencode: plan.opencodeSourceExisted ? parseOpenCodeStream({ cutoff: ctx.cutoff, excludePath, root: plan.opencodeRoot }).pipe(
4264
- Stream16.provide(OpenCodeIoLive)
4265
- ) : Stream16.empty
4266
- };
4267
- };
5676
+ return (ctx) => ({
5677
+ anySourceExisted: plan.anySourceExisted,
5678
+ codex: codexStreamForPlan(plan, ctx.cutoff, excludePath),
5679
+ claudeCode: plan.claudeSourceExisted ? provideFileWalker(parseClaudeStream({ cutoff: ctx.cutoff, excludePath, root: plan.claudeRoot })) : Stream17.empty,
5680
+ pi: plan.piSourceExisted ? provideFileWalker(parsePiStream({ cutoff: ctx.cutoff, excludePath, root: plan.piRoot })) : Stream17.empty,
5681
+ hermes: plan.hermesSourceExisted ? parseHermesStream({ cutoff: ctx.cutoff, excludePath, root: plan.hermesRoot }).pipe(
5682
+ Stream17.provide(HermesParserLive)
5683
+ ) : Stream17.empty,
5684
+ opencode: plan.opencodeSourceExisted ? parseOpenCodeStream({ cutoff: ctx.cutoff, excludePath, root: plan.opencodeRoot }).pipe(
5685
+ Stream17.provide(OpenCodeIoLive)
5686
+ ) : Stream17.empty
5687
+ });
4268
5688
  }
4269
5689
  function buildRealParserFactoryEffect(options = {}) {
4270
- return Effect25.map(resolveParserSourcePlanEffect(options), buildFactoryFromPlan);
5690
+ return Effect26.map(resolveParserSourcePlanEffect(options), buildFactoryFromPlan);
4271
5691
  }
4272
5692
 
4273
5693
  // src/index.ts
@@ -4301,8 +5721,40 @@ function parseHarnessFilter(value) {
4301
5721
  }
4302
5722
  throw new SyncError(EXIT.invalidArguments, `error: --harness must be one of ${SUPPORTED_HARNESSES.join("|")}`);
4303
5723
  }
5724
+ function parseReplacementFlags(raw) {
5725
+ const startHourUtc = optionalValue(raw.replaceStart);
5726
+ const endHourUtc = optionalValue(raw.replaceEnd);
5727
+ const harnessList = optionalValue(raw.replaceHarnesses);
5728
+ if (startHourUtc === void 0 && endHourUtc === void 0 && harnessList === void 0) {
5729
+ if (raw.confirmReplacement) {
5730
+ throw new SyncError(EXIT.invalidArguments, "error: --confirm-replacement requires an atomic replacement scope");
5731
+ }
5732
+ return void 0;
5733
+ }
5734
+ if (!(startHourUtc && endHourUtc && harnessList)) {
5735
+ throw new SyncError(
5736
+ EXIT.invalidArguments,
5737
+ "error: --replace-start, --replace-end, and --replace-harnesses must be provided together"
5738
+ );
5739
+ }
5740
+ const harnesses = harnessList.split(",").map((value) => parseHarnessFilter(value.trim()));
5741
+ if (harnesses.some((harness) => harness === void 0)) {
5742
+ throw new SyncError(EXIT.invalidArguments, "error: replacement harness list cannot be empty");
5743
+ }
5744
+ if (!(raw.dryRun || raw.confirmReplacement)) {
5745
+ throw new SyncError(
5746
+ EXIT.invalidArguments,
5747
+ "error: atomic replacement uploads require --confirm-replacement (use --dry-run to inspect first)"
5748
+ );
5749
+ }
5750
+ return {
5751
+ startHourUtc,
5752
+ endHourUtc,
5753
+ harnesses
5754
+ };
5755
+ }
4304
5756
  function syncCommandErrorExitCode(err) {
4305
- return Effect26.gen(function* () {
5757
+ return Effect27.gen(function* () {
4306
5758
  const console = yield* CliConsole;
4307
5759
  if (err instanceof CliConfigError) {
4308
5760
  yield* console.writeStderrLine(err.message);
@@ -4317,7 +5769,7 @@ function syncCommandErrorExitCode(err) {
4317
5769
  });
4318
5770
  }
4319
5771
  function setupCommandErrorExitCode(err) {
4320
- return Effect26.gen(function* () {
5772
+ return Effect27.gen(function* () {
4321
5773
  const console = yield* CliConsole;
4322
5774
  if (err instanceof CliConfigError || err instanceof SetupError) {
4323
5775
  yield* console.writeStderrLine(err.message);
@@ -4329,26 +5781,26 @@ function setupCommandErrorExitCode(err) {
4329
5781
  }
4330
5782
  function recoverCommandExitCode(effect) {
4331
5783
  return effect.pipe(
4332
- Effect26.catch((err) => syncCommandErrorExitCode(err)),
4333
- Effect26.catchDefect((err) => syncCommandErrorExitCode(err))
5784
+ Effect27.catch((err) => syncCommandErrorExitCode(err)),
5785
+ Effect27.catchDefect((err) => syncCommandErrorExitCode(err))
4334
5786
  );
4335
5787
  }
4336
5788
  function recoverSetupExitCode(effect) {
4337
5789
  return effect.pipe(
4338
- Effect26.catch((err) => setupCommandErrorExitCode(err)),
4339
- Effect26.catchDefect((err) => setupCommandErrorExitCode(err))
5790
+ Effect27.catch((err) => setupCommandErrorExitCode(err)),
5791
+ Effect27.catchDefect((err) => setupCommandErrorExitCode(err))
4340
5792
  );
4341
5793
  }
4342
5794
  function exitCodeToCommandEffect(exitCode) {
4343
- return exitCode === EXIT.success ? Effect26.void : Effect26.fail(new CliExit(exitCode));
5795
+ return exitCode === EXIT.success ? Effect27.void : Effect27.fail(new CliExit(exitCode));
4344
5796
  }
4345
5797
  function runSyncCommand(raw) {
4346
5798
  return recoverCommandExitCode(
4347
- Effect26.gen(function* () {
5799
+ Effect27.gen(function* () {
4348
5800
  const configReader = yield* CliConfigReader;
4349
5801
  const console = yield* CliConsole;
4350
5802
  const env = yield* configReader.read();
4351
- const config = yield* Effect26.try({
5803
+ const config = yield* Effect27.try({
4352
5804
  try: () => resolveSyncConfig(
4353
5805
  {
4354
5806
  api: optionalValue(raw.api),
@@ -4362,10 +5814,14 @@ function runSyncCommand(raw) {
4362
5814
  catch: (err) => err
4363
5815
  });
4364
5816
  const { device, token } = config;
4365
- const harness = yield* Effect26.try({
5817
+ const harness = yield* Effect27.try({
4366
5818
  try: () => parseHarnessFilter(optionalValue(raw.harness)),
4367
5819
  catch: (err) => err
4368
5820
  });
5821
+ const replacement = yield* Effect27.try({
5822
+ try: () => parseReplacementFlags(raw),
5823
+ catch: (err) => err
5824
+ });
4369
5825
  if (!(token && device)) {
4370
5826
  yield* console.writeStderrLine(
4371
5827
  "error: --token and --device are required (or set ~/.oopst.yml / OOPST_TOKEN / OOPST_DEVICE)"
@@ -4373,7 +5829,7 @@ function runSyncCommand(raw) {
4373
5829
  return EXIT.invalidArguments;
4374
5830
  }
4375
5831
  const factory = yield* buildRealParserFactoryEffect({ env, exclude: config.exclude }).pipe(
4376
- Effect26.provide(ParserSourcesLive)
5832
+ Effect27.provide(ParserSourcesLive)
4377
5833
  );
4378
5834
  let sourcesExistedHint = false;
4379
5835
  const parserFactory = (ctx) => {
@@ -4387,6 +5843,7 @@ function runSyncCommand(raw) {
4387
5843
  serverUrl: config.serverUrl,
4388
5844
  lookbackDays: config.lookbackDays,
4389
5845
  harness,
5846
+ replacement,
4390
5847
  dryRun: raw.dryRun,
4391
5848
  verbose: raw.verbose,
4392
5849
  color: yield* console.stdoutIsTTY,
@@ -4398,16 +5855,16 @@ function runSyncCommand(raw) {
4398
5855
  });
4399
5856
  return result.exitCode;
4400
5857
  })
4401
- ).pipe(Effect26.flatMap(exitCodeToCommandEffect));
5858
+ ).pipe(Effect27.flatMap(exitCodeToCommandEffect));
4402
5859
  }
4403
5860
  function runSetupCommand(raw) {
4404
5861
  return recoverSetupExitCode(
4405
- Effect26.gen(function* () {
5862
+ Effect27.gen(function* () {
4406
5863
  const console = yield* CliConsole;
4407
5864
  const metadata = yield* CliPackageMetadata;
4408
5865
  const cliProcess = yield* CliProcess;
4409
5866
  const nodePath = yield* cliProcess.execPath;
4410
- const result = yield* Effect26.tryPromise({
5867
+ const result = yield* Effect27.tryPromise({
4411
5868
  try: () => runSetup({
4412
5869
  api: optionalValue(raw.api),
4413
5870
  server: optionalValue(raw.server),
@@ -4420,7 +5877,7 @@ function runSetupCommand(raw) {
4420
5877
  });
4421
5878
  return result.exitCode;
4422
5879
  })
4423
- ).pipe(Effect26.flatMap(exitCodeToCommandEffect));
5880
+ ).pipe(Effect27.flatMap(exitCodeToCommandEffect));
4424
5881
  }
4425
5882
  var apiFlag = Flag.string("api").pipe(
4426
5883
  Flag.withDescription("API base URL (or server.api / OOPST_SERVER)"),
@@ -4443,6 +5900,22 @@ var harnessFlag = Flag.string("harness").pipe(
4443
5900
  Flag.withDescription("Restrict to codex|claude_code|pi|hermes|opencode"),
4444
5901
  Flag.optional
4445
5902
  );
5903
+ var replaceStartFlag = Flag.string("replace-start").pipe(
5904
+ Flag.withDescription("Atomic replacement start as an inclusive UTC hour"),
5905
+ Flag.optional
5906
+ );
5907
+ var replaceEndFlag = Flag.string("replace-end").pipe(
5908
+ Flag.withDescription("Atomic replacement end as an exclusive UTC hour"),
5909
+ Flag.optional
5910
+ );
5911
+ var replaceHarnessesFlag = Flag.string("replace-harnesses").pipe(
5912
+ Flag.withDescription("Comma-separated harness set for an atomic replacement"),
5913
+ Flag.optional
5914
+ );
5915
+ var confirmReplacementFlag = Flag.boolean("confirm-replacement").pipe(
5916
+ Flag.withDescription("Confirm that the declared replacement scope may overwrite live facts"),
5917
+ Flag.withDefault(false)
5918
+ );
4446
5919
  var dryRunFlag = Flag.boolean("dry-run").pipe(
4447
5920
  Flag.withDescription("Parse and print without uploading"),
4448
5921
  Flag.withDefault(false)
@@ -4458,6 +5931,10 @@ function buildCli() {
4458
5931
  server: serverFlag,
4459
5932
  lookbackDays: lookbackDaysFlag,
4460
5933
  harness: harnessFlag,
5934
+ replaceStart: replaceStartFlag,
5935
+ replaceEnd: replaceEndFlag,
5936
+ replaceHarnesses: replaceHarnessesFlag,
5937
+ confirmReplacement: confirmReplacementFlag,
4461
5938
  dryRun: dryRunFlag,
4462
5939
  verbose: verboseFlag
4463
5940
  },
@@ -4477,10 +5954,10 @@ function buildCli() {
4477
5954
  );
4478
5955
  }
4479
5956
  function runCli() {
4480
- return Effect26.gen(function* () {
5957
+ return Effect27.gen(function* () {
4481
5958
  const metadata = yield* CliPackageMetadata;
4482
5959
  yield* buildCli().pipe(Command.run({ version: metadata.version }));
4483
- }).pipe(Effect26.provide(CliRuntimeLayer));
5960
+ }).pipe(Effect27.provide(CliRuntimeLayer));
4484
5961
  }
4485
5962
  if (isEntrypoint()) {
4486
5963
  runCli().pipe(NodeRuntime.runMain);