@cookielab.io/oopst 0.6.2 → 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,6 +2999,12 @@ function isObject(value) {
2519
2999
  function isFiniteNumber(value) {
2520
3000
  return typeof value === "number" && Number.isFinite(value);
2521
3001
  }
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
+ }
2522
3008
  function isCodexUsageCounters(value) {
2523
3009
  if (!isObject(value)) {
2524
3010
  return false;
@@ -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,6 +3048,11 @@ function isCodexTokenCountEvent(value) {
2557
3048
  if (info["model"] !== void 0 && typeof info["model"] !== "string") {
2558
3049
  return false;
2559
3050
  }
3051
+ for (const key6 of ["id", "turn_id", "turnId"]) {
3052
+ if (!isOptionalBoundedString(info[key6], MAX_CODEX_SOURCE_ID_LENGTH)) {
3053
+ return false;
3054
+ }
3055
+ }
2560
3056
  if (info["last_token_usage"] !== void 0 && !isCodexUsageCounters(info["last_token_usage"])) {
2561
3057
  return false;
2562
3058
  }
@@ -2572,6 +3068,28 @@ function isCodexTokenCountEvent(value) {
2572
3068
  }
2573
3069
  return true;
2574
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)) {
3081
+ return false;
3082
+ }
3083
+ }
3084
+ const info = payload["info"];
3085
+ if (info === void 0) {
3086
+ return true;
3087
+ }
3088
+ if (!isObject(info)) {
3089
+ return false;
3090
+ }
3091
+ return ["id", "turn_id", "turnId"].every((key6) => isOptionalBoundedString(info[key6], MAX_CODEX_SOURCE_ID_LENGTH));
3092
+ }
2575
3093
  function isCodexTurnContextEvent(value) {
2576
3094
  if (!isObject(value)) {
2577
3095
  return false;
@@ -2610,18 +3128,66 @@ 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
  }
3165
+ function nonEmpty(value) {
3166
+ const trimmed = value?.trim();
3167
+ return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
3168
+ }
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
+ };
3190
+ }
2625
3191
  function extractCodexUsageSnapshot(event) {
2626
3192
  return {
2627
3193
  last: event.payload.info?.last_token_usage,
@@ -2631,30 +3197,36 @@ function extractCodexUsageSnapshot(event) {
2631
3197
  function pickCodexEventModel(event) {
2632
3198
  return event.payload.info?.model ?? event.payload.model ?? event.model;
2633
3199
  }
2634
-
2635
- // ../../packages/parsers/src/codex/bucketize.ts
2636
- function startOfUtcHour(d) {
2637
- return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), 0, 0, 0));
2638
- }
2639
- function key2(hourUtc, model) {
2640
- return `${hourUtc.toISOString()}|${model}`;
2641
- }
2642
- function shouldExcludePath(path, excludePath) {
2643
- return path !== void 0 && excludePath?.(path) === true;
2644
- }
2645
- function makeCodexFileAccumulator() {
2646
- return {
2647
- buckets: /* @__PURE__ */ new Map(),
2648
- currentModel: void 0,
2649
- excluded: false,
2650
- lineNo: 0,
2651
- sawTurnContext: false,
2652
- previousTotalUsage: void 0,
2653
- pendingUsage: [],
2654
- sessionLastSeenAt: void 0,
2655
- sessionStartedAt: void 0
2656
- };
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
+ );
2657
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;
2658
3230
  function lineStreamFromFile(path) {
2659
3231
  return Stream8.callback(
2660
3232
  (queue) => Effect15.acquireRelease(
@@ -2699,44 +3271,442 @@ function lineStreamFromFile(path) {
2699
3271
  )
2700
3272
  );
2701
3273
  }
2702
- function normalizeUsage(usage) {
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;
2703
3420
  return {
2704
- inputTokens: Math.max(0, usage.input_tokens ?? 0),
2705
- outputTokens: Math.max(0, usage.output_tokens ?? 0),
2706
- cacheReadTokens: Math.max(0, usage.cached_input_tokens ?? usage.cache_read_input_tokens ?? 0)
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)
2707
3430
  };
2708
3431
  }
2709
- function sameUsage(a, b) {
2710
- return a.inputTokens === b.inputTokens && a.outputTokens === b.outputTokens && a.cacheReadTokens === b.cacheReadTokens;
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;
2711
3445
  }
2712
- function positiveUsage(usage) {
2713
- return usage.inputTokens > 0 || usage.outputTokens > 0 || usage.cacheReadTokens > 0;
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;
2714
3470
  }
2715
- function cumulativeDelta(current, previous) {
2716
- if (previous === void 0) {
2717
- return current;
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
+ }
2718
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
+ };
2719
3575
  return {
2720
- inputTokens: current.inputTokens < previous.inputTokens ? current.inputTokens : current.inputTokens - previous.inputTokens,
2721
- outputTokens: current.outputTokens < previous.outputTokens ? current.outputTokens : current.outputTokens - previous.outputTokens,
2722
- cacheReadTokens: current.cacheReadTokens < previous.cacheReadTokens ? current.cacheReadTokens : current.cacheReadTokens - previous.cacheReadTokens
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
+ })
2723
3591
  };
2724
3592
  }
2725
- function usageIncrement(acc, last, total) {
2726
- const normalizedTotal = total === void 0 ? void 0 : normalizeUsage(total);
2727
- if (normalizedTotal !== void 0) {
2728
- if (acc.previousTotalUsage !== void 0 && sameUsage(acc.previousTotalUsage, normalizedTotal)) {
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)) {
2729
3651
  return void 0;
2730
3652
  }
2731
- const increment2 = last === void 0 ? cumulativeDelta(normalizedTotal, acc.previousTotalUsage) : normalizeUsage(last);
2732
- acc.previousTotalUsage = normalizedTotal;
2733
- return positiveUsage(increment2) ? increment2 : void 0;
3653
+ acc.previousTotalUsage = total;
3654
+ acc.remainingInheritedUsage = void 0;
3655
+ return positiveIncrement(cumulativeCodexDelta(total, previous));
2734
3656
  }
2735
3657
  if (last === void 0) {
2736
3658
  return void 0;
2737
3659
  }
2738
- const increment = normalizeUsage(last);
2739
- return positiveUsage(increment) ? increment : void 0;
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);
2740
3710
  }
2741
3711
  function addToBucket(acc, hourUtc, model, delta) {
2742
3712
  const { inputTokens, outputTokens, cacheReadTokens } = delta;
@@ -2761,11 +3731,16 @@ function addToBucket(acc, hourUtc, model, delta) {
2761
3731
  existing.cacheReadTokens += cacheReadTokens;
2762
3732
  return true;
2763
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
+ }
2764
3741
  function flushPendingUsage(acc, model) {
2765
3742
  for (const pending of acc.pendingUsage) {
2766
- if (addToBucket(acc, startOfUtcHour(pending.eventAt), model, pending.usage)) {
2767
- markSessionSeen(acc, pending.eventAt);
2768
- }
3743
+ recordUsageCandidate(acc, pending, model);
2769
3744
  }
2770
3745
  acc.pendingUsage = [];
2771
3746
  }
@@ -2798,6 +3773,7 @@ function accumulateCodexLine(context, acc, raw) {
2798
3773
  if (shouldExcludePath(parsed.payload.cwd, options.excludePath)) {
2799
3774
  acc.excluded = true;
2800
3775
  acc.buckets.clear();
3776
+ acc.candidates = [];
2801
3777
  acc.pendingUsage = [];
2802
3778
  }
2803
3779
  return acc;
@@ -2806,6 +3782,7 @@ function accumulateCodexLine(context, acc, raw) {
2806
3782
  if (shouldExcludePath(parsed.payload.cwd, options.excludePath)) {
2807
3783
  acc.excluded = true;
2808
3784
  acc.buckets.clear();
3785
+ acc.candidates = [];
2809
3786
  acc.pendingUsage = [];
2810
3787
  return acc;
2811
3788
  }
@@ -2817,6 +3794,10 @@ function accumulateCodexLine(context, acc, raw) {
2817
3794
  }
2818
3795
  return acc;
2819
3796
  }
3797
+ if (isCodexTaskStartedEvent(parsed)) {
3798
+ acc.currentTurnId = pickCodexTurnId(parsed);
3799
+ return acc;
3800
+ }
2820
3801
  if (!isCodexTokenCountEvent(parsed)) {
2821
3802
  return acc;
2822
3803
  }
@@ -2830,24 +3811,25 @@ function accumulateCodexLine(context, acc, raw) {
2830
3811
  return acc;
2831
3812
  }
2832
3813
  const eventAt = new Date(tsMs);
3814
+ const eventIndex = acc.nextEventIndex;
3815
+ acc.nextEventIndex += 1;
3816
+ const turnId = pickCodexTurnId(parsed) ?? acc.currentTurnId;
3817
+ const candidate = { eventAt, eventIndex, turnId, usage: delta };
2833
3818
  const eventModel = pickCodexEventModel(parsed);
2834
3819
  const model = acc.sawTurnContext ? acc.currentModel ?? eventModel : eventModel ?? acc.currentModel;
2835
3820
  if (model === void 0) {
2836
- acc.pendingUsage.push({ eventAt, usage: delta });
3821
+ acc.pendingUsage.push(candidate);
2837
3822
  return acc;
2838
3823
  }
2839
- if (addToBucket(acc, startOfUtcHour(eventAt), model, delta)) {
2840
- markSessionSeen(acc, eventAt);
2841
- }
3824
+ recordUsageCandidate(acc, candidate, model);
2842
3825
  return acc;
2843
3826
  }
2844
- function bucketsFromAccumulator(path, acc) {
3827
+ function bucketsFromAccumulator(path, options, acc) {
2845
3828
  if (acc.excluded || acc.sessionStartedAt === void 0 || acc.sessionLastSeenAt === void 0) {
2846
3829
  return [];
2847
3830
  }
2848
- const sessionStartedAt = acc.sessionStartedAt;
2849
- const sessionLastSeenAt = acc.sessionLastSeenAt;
2850
- const sessionIds = /* @__PURE__ */ new Set([path]);
3831
+ const { sessionLastSeenAt, sessionStartedAt } = acc;
3832
+ const sessionIds = /* @__PURE__ */ new Set([options.sessionIdentity ?? path]);
2851
3833
  return Array.from(acc.buckets.values(), (bucket) => ({
2852
3834
  harness: "codex",
2853
3835
  model: bucket.model,
@@ -2861,13 +3843,37 @@ function bucketsFromAccumulator(path, acc) {
2861
3843
  sessionLastSeenAt
2862
3844
  }));
2863
3845
  }
2864
- 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 = {}) {
2865
3867
  const context = { logger, options, path };
2866
- return Stream8.unwrap(
2867
- lineStreamFromFile(path).pipe(
2868
- Stream8.runFold(makeCodexFileAccumulator, (acc, raw) => accumulateCodexLine(context, acc, raw)),
2869
- Effect15.map((acc) => Stream8.fromIterable(bucketsFromAccumulator(path, acc)))
2870
- )
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
+ }))
2871
3877
  );
2872
3878
  }
2873
3879
 
@@ -2878,6 +3884,12 @@ function isObject2(value) {
2878
3884
  function isFiniteNumber2(value) {
2879
3885
  return typeof value === "number" && Number.isFinite(value);
2880
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
+ }
2881
3893
  function isClaudeUsage(value) {
2882
3894
  if (!isObject2(value)) {
2883
3895
  return false;
@@ -2898,20 +3910,23 @@ function isClaudeAssistantEvent(value) {
2898
3910
  if (value["type"] !== "assistant") {
2899
3911
  return false;
2900
3912
  }
2901
- if (typeof value["timestamp"] !== "string") {
3913
+ if (typeof value["timestamp"] !== "string" || value["timestamp"].length > MAX_CLAUDE_TIMESTAMP_LENGTH) {
2902
3914
  return false;
2903
3915
  }
2904
- 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)) {
2905
3920
  return false;
2906
3921
  }
2907
- if (value["requestId"] !== void 0 && typeof value["requestId"] !== "string") {
3922
+ if (value["isSidechain"] !== void 0 && typeof value["isSidechain"] !== "boolean") {
2908
3923
  return false;
2909
3924
  }
2910
3925
  const message = value["message"];
2911
3926
  if (!isObject2(message)) {
2912
3927
  return false;
2913
3928
  }
2914
- if (message["id"] !== void 0 && typeof message["id"] !== "string") {
3929
+ if (!isOptionalBoundedString2(message["id"], MAX_CLAUDE_SOURCE_ID_LENGTH)) {
2915
3930
  return false;
2916
3931
  }
2917
3932
  if (typeof message["model"] !== "string") {
@@ -2928,25 +3943,21 @@ function key3(hourUtc, model) {
2928
3943
  return `${hourUtc.toISOString()}|${model}`;
2929
3944
  }
2930
3945
  function shouldExcludePath2(path, excludePath) {
2931
- return path !== void 0 && excludePath?.(path) === true;
2932
- }
2933
- function makeClaudeFileAccumulator(options) {
2934
- return {
2935
- buckets: /* @__PURE__ */ new Map(),
2936
- excluded: false,
2937
- lineNo: 0,
2938
- seenTuples: options.seenTuples ?? /* @__PURE__ */ new Set(),
2939
- sessionLastSeenAt: void 0,
2940
- sessionStartedAt: void 0
2941
- };
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 };
2942
3953
  }
2943
3954
  function lineStreamFromFile2(path) {
2944
- return Stream9.callback(
2945
- (queue) => Effect16.acquireRelease(
2946
- Effect16.sync(() => {
3955
+ return Stream10.callback(
3956
+ (queue) => Effect17.acquireRelease(
3957
+ Effect17.sync(() => {
2947
3958
  let done = false;
2948
3959
  const stream = createReadStream2(path, { encoding: "utf8" });
2949
- const rl = createInterface3({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
3960
+ const lines = createInterface3({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
2950
3961
  const offer = (line) => {
2951
3962
  if (!done) {
2952
3963
  Queue2.offerUnsafe(queue, line);
@@ -2965,61 +3976,32 @@ function lineStreamFromFile2(path) {
2965
3976
  }
2966
3977
  };
2967
3978
  stream.once("error", fail);
2968
- rl.once("error", fail);
2969
- rl.once("close", end);
2970
- rl.on("line", offer);
3979
+ lines.once("error", fail);
3980
+ lines.once("close", end);
3981
+ lines.on("line", offer);
2971
3982
  return {
2972
3983
  close: () => {
2973
3984
  done = true;
2974
- rl.off("close", end);
2975
- rl.off("error", fail);
2976
- rl.off("line", offer);
3985
+ lines.off("close", end);
3986
+ lines.off("error", fail);
3987
+ lines.off("line", offer);
2977
3988
  stream.off("error", fail);
2978
- rl.close();
3989
+ lines.close();
2979
3990
  stream.destroy();
2980
3991
  }
2981
3992
  };
2982
3993
  }),
2983
- (resource) => Effect16.sync(() => resource.close())
3994
+ (resource) => Effect17.sync(() => resource.close())
2984
3995
  )
2985
3996
  );
2986
3997
  }
2987
- function addToBucket2(acc, hourUtc, model, usage) {
2988
- const inputTokens = Math.max(0, usage.input_tokens ?? 0);
2989
- const outputTokens = Math.max(0, usage.output_tokens ?? 0);
2990
- const cacheReadTokens = Math.max(0, usage.cache_read_input_tokens ?? 0);
2991
- const cacheCreateTokens = Math.max(0, usage.cache_creation_input_tokens ?? 0);
2992
- if (inputTokens === 0 && outputTokens === 0 && cacheReadTokens === 0 && cacheCreateTokens === 0) {
3998
+ function eventIsEligible(timestampMs3, options) {
3999
+ if (options.cutoff !== void 0 && timestampMs3 < options.cutoff.getTime()) {
2993
4000
  return false;
2994
4001
  }
2995
- const k = key3(hourUtc, model);
2996
- const existing = acc.buckets.get(k);
2997
- if (existing === void 0) {
2998
- acc.buckets.set(k, {
2999
- hourUtc,
3000
- model,
3001
- inputTokens,
3002
- outputTokens,
3003
- cacheReadTokens,
3004
- cacheCreateTokens
3005
- });
3006
- return true;
3007
- }
3008
- existing.inputTokens += inputTokens;
3009
- existing.outputTokens += outputTokens;
3010
- existing.cacheReadTokens += cacheReadTokens;
3011
- existing.cacheCreateTokens += cacheCreateTokens;
3012
- return true;
3013
- }
3014
- function markSessionSeen2(acc, eventAt) {
3015
- if (acc.sessionStartedAt === void 0 || eventAt.getTime() < acc.sessionStartedAt.getTime()) {
3016
- acc.sessionStartedAt = eventAt;
3017
- }
3018
- if (acc.sessionLastSeenAt === void 0 || eventAt.getTime() > acc.sessionLastSeenAt.getTime()) {
3019
- acc.sessionLastSeenAt = eventAt;
3020
- }
4002
+ return options.endExclusive === void 0 || timestampMs3 < options.endExclusive.getTime();
3021
4003
  }
3022
- function accumulateClaudeLine(context, acc, raw) {
4004
+ function accumulateClaudeCandidate(context, acc, raw) {
3023
4005
  const { logger, options, path } = context;
3024
4006
  acc.lineNo += 1;
3025
4007
  if (acc.excluded) {
@@ -3041,37 +4023,129 @@ function accumulateClaudeLine(context, acc, raw) {
3041
4023
  }
3042
4024
  if (shouldExcludePath2(parsed.cwd, options.excludePath)) {
3043
4025
  acc.excluded = true;
3044
- acc.buckets.clear();
4026
+ acc.identified.clear();
4027
+ acc.unidentified = [];
3045
4028
  return acc;
3046
4029
  }
3047
- const tsMs = Date.parse(parsed.timestamp);
3048
- if (!Number.isFinite(tsMs)) {
4030
+ const timestampMs3 = Date.parse(parsed.timestamp);
4031
+ if (!(Number.isFinite(timestampMs3) && eventIsEligible(timestampMs3, options))) {
3049
4032
  return acc;
3050
4033
  }
3051
- if (options.cutoff !== void 0 && tsMs < options.cutoff.getTime()) {
3052
- 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);
3053
4050
  }
3054
- const { message, requestId } = parsed;
3055
- if (message.id !== void 0 && requestId !== void 0) {
3056
- const tuple = `${message.id}::${requestId}`;
3057
- if (acc.seenTuples.has(tuple)) {
3058
- 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";
3059
4083
  }
3060
- acc.seenTuples.add(tuple);
3061
4084
  }
3062
- const eventAt = new Date(tsMs);
3063
- if (addToBucket2(acc, startOfUtcHour(eventAt), message.model, message.usage)) {
3064
- markSessionSeen2(acc, eventAt);
4085
+ const timestampOrder = next.eventAt.getTime() - current.eventAt.getTime();
4086
+ if (timestampOrder !== 0) {
4087
+ return timestampOrder > 0;
3065
4088
  }
3066
- return acc;
4089
+ if (next.eventOrder !== current.eventOrder) {
4090
+ return next.eventOrder > current.eventOrder;
4091
+ }
4092
+ return next.sourceOrder.localeCompare(current.sourceOrder) < 0;
3067
4093
  }
3068
- function bucketsFromAccumulator2(path, options, acc) {
3069
- 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) {
3070
4146
  return [];
3071
4147
  }
3072
- const sessionStartedAt = acc.sessionStartedAt;
3073
- const sessionLastSeenAt = acc.sessionLastSeenAt;
3074
- const sessionIds = /* @__PURE__ */ new Set([options.sessionId ?? path]);
4148
+ const sessionIds = /* @__PURE__ */ new Set([sessionId]);
3075
4149
  return Array.from(acc.buckets.values(), (bucket) => ({
3076
4150
  harness: "claude_code",
3077
4151
  model: bucket.model,
@@ -3085,70 +4159,80 @@ function bucketsFromAccumulator2(path, options, acc) {
3085
4159
  sessionLastSeenAt
3086
4160
  }));
3087
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
+ }
3088
4173
  function bucketizeClaudeFileStream(path, logger, options = {}) {
3089
- const context = { logger, options, path };
3090
- return Stream9.unwrap(
3091
- lineStreamFromFile2(path).pipe(
3092
- Stream9.runFold(
3093
- () => makeClaudeFileAccumulator(options),
3094
- (acc, raw) => accumulateClaudeLine(context, acc, raw)
3095
- ),
3096
- 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)))
3097
4177
  )
3098
4178
  );
3099
4179
  }
3100
4180
 
3101
4181
  // ../../packages/parsers/src/claude/index.ts
3102
- function sourceSessionId(path) {
4182
+ function sourceContext(path) {
3103
4183
  const fileDir = dirname2(path);
3104
4184
  if (basename(fileDir) !== "subagents") {
3105
- return path;
4185
+ return { pathRole: "parent", sessionId: path };
3106
4186
  }
3107
4187
  const sessionDir = dirname2(fileDir);
3108
- return `${sessionDir}.jsonl`;
4188
+ return { pathRole: "subagent", sessionId: `${sessionDir}.jsonl` };
3109
4189
  }
3110
4190
  function bucketizeClaudeSourcesStream(files, logger, options) {
3111
- return Stream10.suspend(() => {
3112
- const seenTuples = /* @__PURE__ */ new Set();
3113
- return files.pipe(
3114
- Stream10.flatMap(
3115
- (file) => {
3116
- const sessionId = sourceSessionId(file);
3117
- return bucketizeClaudeFileStream(file, logger, {
3118
- cutoff: options.cutoff,
3119
- excludePath: options.excludePath,
3120
- sessionId,
3121
- 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
3122
4202
  });
3123
- },
3124
- { concurrency: 1 }
3125
- )
3126
- );
3127
- });
4203
+ })
4204
+ ),
4205
+ Effect18.map((filesCandidates) => Stream11.fromIterable(bucketizeClaudeCandidates(filesCandidates.flat())))
4206
+ )
4207
+ );
3128
4208
  }
3129
- var parseClaudeEffect = Effect17.fn("ClaudeParser.parseClaudeEffect")(
3130
- (opts) => Stream10.runCollect(parseClaudeStream(opts))
4209
+ var parseClaudeEffect = Effect18.fn("ClaudeParser.parseClaudeEffect")(
4210
+ (opts) => Stream11.runCollect(parseClaudeStream(opts))
3131
4211
  );
3132
- var parseClaudeStream = (opts) => Stream10.unwrap(
3133
- Effect17.gen(function* () {
4212
+ var parseClaudeStream = (opts) => Stream11.unwrap(
4213
+ Effect18.gen(function* () {
3134
4214
  const logger = opts.logger ?? silentLogger;
3135
4215
  const root = opts.root ?? resolveClaudeRoot();
3136
4216
  if ((yield* pathKindEffectStrict(root)) === "file") {
4217
+ const source = sourceContext(root);
3137
4218
  return bucketizeClaudeFileStream(root, logger, {
3138
4219
  cutoff: opts.cutoff,
4220
+ endExclusive: opts.endExclusive,
3139
4221
  excludePath: opts.excludePath,
3140
- sessionId: sourceSessionId(root)
4222
+ pathRole: source.pathRole,
4223
+ sessionId: source.sessionId
3141
4224
  });
3142
4225
  }
3143
4226
  return bucketizeClaudeSourcesStream(walkStreamStrict(root, opts.cutoff), logger, {
3144
4227
  cutoff: opts.cutoff,
4228
+ endExclusive: opts.endExclusive,
3145
4229
  excludePath: opts.excludePath
3146
4230
  });
3147
4231
  })
3148
4232
  );
3149
4233
 
3150
4234
  // ../../packages/parsers/src/codex/index.ts
3151
- import { Effect as Effect18, Stream as Stream11 } from "effect";
4235
+ import { Effect as Effect19, Stream as Stream12 } from "effect";
3152
4236
  var codexRoots = (opts, root) => {
3153
4237
  const roots = [root];
3154
4238
  if (opts.root === void 0 && opts.archivedRoot === void 0) {
@@ -3158,52 +4242,134 @@ var codexRoots = (opts, root) => {
3158
4242
  }
3159
4243
  return roots;
3160
4244
  };
3161
- var parseCodexEffect = Effect18.fn("CodexParser.parseCodexEffect")(
3162
- (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))
3163
4307
  );
3164
- var parseCodexStream = (opts) => Stream11.unwrap(
3165
- Effect18.gen(function* () {
4308
+ var parseCodexStream = (opts) => Stream12.unwrap(
4309
+ Effect19.gen(function* () {
3166
4310
  const logger = opts.logger ?? silentLogger;
3167
4311
  const root = opts.root ?? resolveCodexSessionsRoot();
3168
4312
  if ((yield* pathKindEffectStrict(root)) === "file") {
3169
- return bucketizeCodexFileStream(root, logger, { excludePath: opts.excludePath });
4313
+ return bucketizeIndexedCodexFilesStream(Stream12.succeed(root), opts.cutoff, logger, {
4314
+ excludePath: opts.excludePath
4315
+ });
3170
4316
  }
3171
- return bucketizeCodexFilesStream(walkManyStreamStrict(codexRoots(opts, root), opts.cutoff), logger, {
3172
- excludePath: opts.excludePath
3173
- });
4317
+ return bucketizeIndexedCodexFilesStream(
4318
+ walkManyStreamStrict(codexRoots(opts, root), /* @__PURE__ */ new Date(0)),
4319
+ opts.cutoff,
4320
+ logger,
4321
+ { excludePath: opts.excludePath }
4322
+ );
3174
4323
  })
3175
4324
  );
3176
- function bucketizeCodexFilesStream(files, logger, options) {
3177
- 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
+ );
3178
4354
  }
3179
4355
 
3180
4356
  // ../../packages/parsers/src/effect/hermes-sqlite.ts
3181
- 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";
3182
4358
  var NODE_SQLITE_SPECIFIER = ["node", "sqlite"].join(":");
3183
4359
  var MILLISECONDS_PER_SECOND = 1e3;
3184
- var HERMES_SESSION_QUERY = `
3185
- select
3186
- id,
3187
- model,
3188
- billing_provider as "billingProvider",
3189
- started_at as "startedAt",
3190
- ended_at as "endedAt",
3191
- input_tokens as "inputTokens",
3192
- output_tokens as "outputTokens",
3193
- cache_read_tokens as "cacheReadTokens",
3194
- cache_write_tokens as "cacheWriteTokens",
3195
- cwd
3196
- from sessions
3197
- where coalesce(ended_at, started_at) >= ?
3198
- order by started_at asc, id asc
3199
- `;
3200
4360
  var HermesSqliteError = class extends Schema22.TaggedErrorClass()("HermesSqliteError", {
3201
4361
  message: Schema22.String,
3202
- operation: Schema22.Literals(["load", "open", "query", "close"]),
4362
+ operation: Schema22.Literals(["close", "load", "open", "query", "schema"]),
3203
4363
  path: Schema22.optionalKey(Schema22.String),
3204
4364
  cause: Schema22.optionalKey(Schema22.Defect())
3205
4365
  }) {
3206
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
+ }
3207
4373
  var HermesSqlite = class extends Context9.Service()("@oopst/parsers/HermesSqlite") {
3208
4374
  };
3209
4375
  var toHermesSqliteError = (operation, path) => (error) => new HermesSqliteError({
@@ -3212,34 +4378,124 @@ var toHermesSqliteError = (operation, path) => (error) => new HermesSqliteError(
3212
4378
  path,
3213
4379
  cause: error
3214
4380
  });
3215
- var loadDatabaseSync = Effect19.fn("HermesSqlite.loadDatabaseSync")(function* () {
3216
- const sqliteModule = yield* Effect19.tryPromise({
4381
+ var loadDatabaseSync = Effect20.fn("HermesSqlite.loadDatabaseSync")(function* () {
4382
+ const sqliteModule = yield* Effect20.tryPromise({
3217
4383
  try: () => import(NODE_SQLITE_SPECIFIER),
3218
4384
  catch: toHermesSqliteError("load")
3219
4385
  });
3220
4386
  return sqliteModule.DatabaseSync;
3221
4387
  });
3222
- var openDatabase = (path, DatabaseSync) => Effect19.try({
4388
+ var openDatabase = (path, DatabaseSync) => Effect20.try({
3223
4389
  try: () => new DatabaseSync(path, { readOnly: true, timeout: 1e3 }),
3224
4390
  catch: toHermesSqliteError("open", path)
3225
4391
  });
3226
- var closeDatabase = (path, db) => Effect19.try({
4392
+ var closeDatabase = (path, db) => Effect20.try({
3227
4393
  try: () => {
3228
4394
  db.close();
3229
4395
  },
3230
4396
  catch: toHermesSqliteError("close", path)
3231
4397
  });
3232
- 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({
3233
4476
  try: () => {
3234
- const statement = db.prepare(HERMES_SESSION_QUERY);
3235
- 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));
3236
4487
  },
3237
- 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)
3238
4494
  });
3239
4495
  var makeHermesSqlite = () => ({
3240
- readSessionRows: Effect19.fn("HermesSqlite.readSessionRows")(function* (path, cutoff) {
4496
+ readSessionRows: Effect20.fn("HermesSqlite.readSessionRows")(function* (path, cutoff) {
3241
4497
  const DatabaseSync = yield* loadDatabaseSync();
3242
- return yield* Effect19.acquireUseRelease(
4498
+ return yield* Effect20.acquireUseRelease(
3243
4499
  openDatabase(path, DatabaseSync),
3244
4500
  (db) => querySessionRows(path, db, cutoff),
3245
4501
  (db) => closeDatabase(path, db)
@@ -3248,13 +4504,13 @@ var makeHermesSqlite = () => ({
3248
4504
  });
3249
4505
  var HermesSqliteLive = Layer8.effect(
3250
4506
  HermesSqlite,
3251
- Effect19.sync(() => HermesSqlite.of(makeHermesSqlite()))
4507
+ Effect20.sync(() => HermesSqlite.of(makeHermesSqlite()))
3252
4508
  );
3253
4509
 
3254
4510
  // ../../packages/parsers/src/effect/opencode-io.ts
3255
4511
  import { basename as basename2, join as join6 } from "path";
3256
4512
  import { NodeFileSystem as NodeFileSystem5 } from "@effect/platform-node";
3257
- 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";
3258
4514
  var NODE_SQLITE_SPECIFIER2 = ["node", "sqlite"].join(":");
3259
4515
  var OPENCODE_DB_FILE_PATTERN2 = /^opencode(?:-[^/]+)?\.db$/u;
3260
4516
  var SESSION_MESSAGE_QUERY = `
@@ -3286,13 +4542,13 @@ var toOpenCodeIoError = (operation, path) => (error) => new OpenCodeIoError({
3286
4542
  path,
3287
4543
  cause: error
3288
4544
  });
3289
- var readDir2 = (fs, path) => fs.readDirectory(path).pipe(Effect20.mapError(toOpenCodeIoError("readdir", path)));
3290
- var readDirBestEffort = (fs, path) => readDir2(fs, path).pipe(Effect20.catch(() => Effect20.succeed([])));
3291
- var readStat2 = (fs, path) => fs.stat(path).pipe(Effect20.mapError(toOpenCodeIoError("stat", path)));
3292
- var readStatBestEffort = (fs, path) => readStat2(fs, path).pipe(Effect20.catch(() => Effect20.succeed(void 0)));
3293
- 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) {
3294
4550
  return yield* readStat2(fs, path).pipe(
3295
- Effect20.map((info) => {
4551
+ Effect21.map((info) => {
3296
4552
  if (info.type === "File") {
3297
4553
  return "file";
3298
4554
  }
@@ -3301,10 +4557,10 @@ var pathKind = Effect20.fn("OpenCodeIo.pathKind")(function* (fs, path) {
3301
4557
  }
3302
4558
  return "missing";
3303
4559
  }),
3304
- Effect20.catch(() => Effect20.succeed("missing"))
4560
+ Effect21.catch(() => Effect21.succeed("missing"))
3305
4561
  );
3306
4562
  });
3307
- var listOpenCodeDatabases = Effect20.fn("OpenCodeIo.listOpenCodeDatabases")(function* (fs, root) {
4563
+ var listOpenCodeDatabases = Effect21.fn("OpenCodeIo.listOpenCodeDatabases")(function* (fs, root) {
3308
4564
  const entries = yield* readDirBestEffort(fs, root);
3309
4565
  const databasePaths = [];
3310
4566
  for (const entry of entries) {
@@ -3318,7 +4574,7 @@ var listOpenCodeDatabases = Effect20.fn("OpenCodeIo.listOpenCodeDatabases")(func
3318
4574
  }
3319
4575
  return databasePaths;
3320
4576
  });
3321
- var listProjectLegacyRoots = Effect20.fn("OpenCodeIo.listProjectLegacyRoots")(function* (fs, root) {
4577
+ var listProjectLegacyRoots = Effect21.fn("OpenCodeIo.listProjectLegacyRoots")(function* (fs, root) {
3322
4578
  const projectRoot = join6(root, "project");
3323
4579
  const entries = yield* readDirBestEffort(fs, projectRoot);
3324
4580
  const roots = [];
@@ -3330,7 +4586,7 @@ var listProjectLegacyRoots = Effect20.fn("OpenCodeIo.listProjectLegacyRoots")(fu
3330
4586
  }
3331
4587
  return roots;
3332
4588
  });
3333
- var legacyRoots = Effect20.fn("OpenCodeIo.legacyRoots")(function* (fs, root) {
4589
+ var legacyRoots = Effect21.fn("OpenCodeIo.legacyRoots")(function* (fs, root) {
3334
4590
  return [
3335
4591
  join6(root, "storage", "message"),
3336
4592
  join6(root, "storage", "session", "message"),
@@ -3341,26 +4597,26 @@ var isRecent = (info, cutoff) => Option3.match(info.mtime, {
3341
4597
  onNone: () => false,
3342
4598
  onSome: (mtime) => mtime.getTime() >= cutoff.getTime()
3343
4599
  });
3344
- var streamJsonFiles = (fs, root, cutoff) => Stream12.suspend(
3345
- () => Stream12.fromIterableEffect(readDirBestEffort(fs, root)).pipe(
3346
- Stream12.flatMap((entry) => {
4600
+ var streamJsonFiles = (fs, root, cutoff) => Stream13.suspend(
4601
+ () => Stream13.fromIterableEffect(readDirBestEffort(fs, root)).pipe(
4602
+ Stream13.flatMap((entry) => {
3347
4603
  const full = join6(root, entry);
3348
- return Stream12.fromEffect(readStatBestEffort(fs, full)).pipe(
3349
- Stream12.flatMap((info) => {
4604
+ return Stream13.fromEffect(readStatBestEffort(fs, full)).pipe(
4605
+ Stream13.flatMap((info) => {
3350
4606
  if (info?.type === "Directory") {
3351
4607
  return streamJsonFiles(fs, full, cutoff);
3352
4608
  }
3353
4609
  if (info?.type === "File" && entry.endsWith(".json") && isRecent(info, cutoff)) {
3354
- return Stream12.succeed(full);
4610
+ return Stream13.succeed(full);
3355
4611
  }
3356
- return Stream12.empty;
4612
+ return Stream13.empty;
3357
4613
  })
3358
4614
  );
3359
4615
  })
3360
4616
  )
3361
4617
  );
3362
- var streamManyJsonFiles = (fs, roots, cutoff) => Stream12.fromIterable(roots).pipe(Stream12.flatMap((root) => streamJsonFiles(fs, root, cutoff)));
3363
- 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) {
3364
4620
  const kind = yield* pathKind(fs, root);
3365
4621
  if (kind === "missing") {
3366
4622
  return { _tag: "Missing" };
@@ -3379,26 +4635,26 @@ var discoverSources = Effect20.fn("OpenCodeIo.discoverSources")(function* (fs, r
3379
4635
  return { _tag: "LegacyJsonFiles", paths: streamManyJsonFiles(fs, roots, cutoff) };
3380
4636
  });
3381
4637
  var readJsonFile = (fs, path) => fs.readFileString(path, "utf8").pipe(
3382
- Effect20.mapError(toOpenCodeIoError("readFile", path)),
3383
- Effect20.flatMap(
3384
- (text) => Effect20.try({
4638
+ Effect21.mapError(toOpenCodeIoError("readFile", path)),
4639
+ Effect21.flatMap(
4640
+ (text) => Effect21.try({
3385
4641
  try: () => JSON.parse(text),
3386
4642
  catch: toOpenCodeIoError("parseJson", path)
3387
4643
  })
3388
4644
  )
3389
4645
  );
3390
- var loadDatabaseSync2 = Effect20.fn("OpenCodeIo.loadDatabaseSync")(function* () {
3391
- const sqliteModule = yield* Effect20.tryPromise({
4646
+ var loadDatabaseSync2 = Effect21.fn("OpenCodeIo.loadDatabaseSync")(function* () {
4647
+ const sqliteModule = yield* Effect21.tryPromise({
3392
4648
  try: () => import(NODE_SQLITE_SPECIFIER2),
3393
4649
  catch: toOpenCodeIoError("load")
3394
4650
  });
3395
4651
  return sqliteModule.DatabaseSync;
3396
4652
  });
3397
- var openDatabase2 = (path, DatabaseSync) => Effect20.try({
4653
+ var openDatabase2 = (path, DatabaseSync) => Effect21.try({
3398
4654
  try: () => new DatabaseSync(path, { readOnly: true, timeout: 1e3 }),
3399
4655
  catch: toOpenCodeIoError("open", path)
3400
4656
  });
3401
- var closeDatabase2 = (path, db) => Effect20.try({
4657
+ var closeDatabase2 = (path, db) => Effect21.try({
3402
4658
  try: () => {
3403
4659
  db.close();
3404
4660
  },
@@ -3423,7 +4679,7 @@ function readSessionPaths(db) {
3423
4679
  }
3424
4680
  return paths;
3425
4681
  }
3426
- var querySqliteRows = (path, db, cutoff) => Effect20.try({
4682
+ var querySqliteRows = (path, db, cutoff) => Effect21.try({
3427
4683
  try: () => {
3428
4684
  const statement = db.prepare(SESSION_MESSAGE_QUERY);
3429
4685
  return {
@@ -3433,9 +4689,9 @@ var querySqliteRows = (path, db, cutoff) => Effect20.try({
3433
4689
  },
3434
4690
  catch: toOpenCodeIoError("query", path)
3435
4691
  });
3436
- var readSqlite = Effect20.fn("OpenCodeIo.readSqlite")(function* (path, cutoff) {
4692
+ var readSqlite = Effect21.fn("OpenCodeIo.readSqlite")(function* (path, cutoff) {
3437
4693
  const DatabaseSync = yield* loadDatabaseSync2();
3438
- return yield* Effect20.acquireUseRelease(
4694
+ return yield* Effect21.acquireUseRelease(
3439
4695
  openDatabase2(path, DatabaseSync),
3440
4696
  (db) => querySqliteRows(path, db, cutoff),
3441
4697
  (db) => closeDatabase2(path, db)
@@ -3448,17 +4704,17 @@ var makeOpenCodeIo = (fs) => ({
3448
4704
  });
3449
4705
  var OpenCodeIoLive = Layer9.effect(
3450
4706
  OpenCodeIo,
3451
- Effect20.gen(function* () {
4707
+ Effect21.gen(function* () {
3452
4708
  const fs = yield* FileSystem6.FileSystem;
3453
4709
  return OpenCodeIo.of(makeOpenCodeIo(fs));
3454
4710
  })
3455
4711
  ).pipe(Layer9.provide(NodeFileSystem5.layer));
3456
4712
 
3457
4713
  // ../../packages/parsers/src/opencode/index.ts
3458
- import { Effect as Effect22, Stream as Stream13 } from "effect";
4714
+ import { Effect as Effect23, Stream as Stream14 } from "effect";
3459
4715
 
3460
4716
  // ../../packages/parsers/src/opencode/bucketize.ts
3461
- import { Effect as Effect21 } from "effect";
4717
+ import { Effect as Effect22 } from "effect";
3462
4718
 
3463
4719
  // ../../packages/parsers/src/opencode/events.ts
3464
4720
  function isObject3(value) {
@@ -3679,7 +4935,7 @@ function* finalizedBuckets(accumulators) {
3679
4935
  function parseJson(value) {
3680
4936
  return typeof value === "string" ? JSON.parse(value) : value;
3681
4937
  }
3682
- var bucketizeOpenCodeDatabaseEffect = Effect21.fn("bucketizeOpenCodeDatabaseEffect")(
4938
+ var bucketizeOpenCodeDatabaseEffect = Effect22.fn("bucketizeOpenCodeDatabaseEffect")(
3683
4939
  function* (path, cutoff, logger, excludePath) {
3684
4940
  const io = yield* OpenCodeIo;
3685
4941
  const rows = yield* io.readSqlite(path, cutoff);
@@ -3702,7 +4958,7 @@ var bucketizeOpenCodeDatabaseEffect = Effect21.fn("bucketizeOpenCodeDatabaseEffe
3702
4958
  return [...finalizedBuckets(accumulators)];
3703
4959
  }
3704
4960
  );
3705
- var bucketizeOpenCodeJsonFileEffect = Effect21.fn("bucketizeOpenCodeJsonFileEffect")(
4961
+ var bucketizeOpenCodeJsonFileEffect = Effect22.fn("bucketizeOpenCodeJsonFileEffect")(
3706
4962
  function* (path) {
3707
4963
  const io = yield* OpenCodeIo;
3708
4964
  const parsed = yield* io.readJsonFile(path);
@@ -3717,23 +4973,23 @@ var bucketizeOpenCodeJsonFileEffect = Effect21.fn("bucketizeOpenCodeJsonFileEffe
3717
4973
  );
3718
4974
 
3719
4975
  // ../../packages/parsers/src/opencode/index.ts
3720
- var discoverOpenCodeSourcesEffect = Effect22.fn("discoverOpenCodeSourcesEffect")(function* (root, cutoff) {
4976
+ var discoverOpenCodeSourcesEffect = Effect23.fn("discoverOpenCodeSourcesEffect")(function* (root, cutoff) {
3721
4977
  const io = yield* OpenCodeIo;
3722
4978
  return yield* io.discoverSources(root, cutoff);
3723
4979
  });
3724
- var parseOpenCodeEffect = Effect22.fn(
4980
+ var parseOpenCodeEffect = Effect23.fn(
3725
4981
  "OpenCodeParser.parseOpenCodeEffect"
3726
- )((opts) => Stream13.runCollect(parseOpenCodeStream(opts)));
3727
- var parseOpenCodeStream = (opts) => Stream13.unwrap(
3728
- Effect22.gen(function* () {
4982
+ )((opts) => Stream14.runCollect(parseOpenCodeStream(opts)));
4983
+ var parseOpenCodeStream = (opts) => Stream14.unwrap(
4984
+ Effect23.gen(function* () {
3729
4985
  const logger = opts.logger ?? silentLogger;
3730
4986
  const root = opts.root ?? resolveOpenCodeRoot();
3731
4987
  const sources = yield* discoverOpenCodeSourcesEffect(root, opts.cutoff);
3732
4988
  if (sources._tag === "Missing") {
3733
- return Stream13.empty;
4989
+ return Stream14.empty;
3734
4990
  }
3735
4991
  if (sources._tag === "JsonFile") {
3736
- return openCodeJsonFileStreams(Stream13.succeed(sources.path));
4992
+ return openCodeJsonFileStreams(Stream14.succeed(sources.path));
3737
4993
  }
3738
4994
  if (sources._tag === "DatabaseFiles") {
3739
4995
  return openCodeDatabaseStreams(sources.paths, opts, logger);
@@ -3742,28 +4998,28 @@ var parseOpenCodeStream = (opts) => Stream13.unwrap(
3742
4998
  })
3743
4999
  );
3744
5000
  function openCodeDatabaseStreams(paths, opts, logger) {
3745
- let stream = Stream13.empty;
5001
+ let stream = Stream14.empty;
3746
5002
  for (const dbPath of paths) {
3747
5003
  stream = stream.pipe(
3748
- Stream13.concat(
3749
- Stream13.fromIterableEffect(bucketizeOpenCodeDatabaseEffect(dbPath, opts.cutoff, logger, opts.excludePath))
5004
+ Stream14.concat(
5005
+ Stream14.fromIterableEffect(bucketizeOpenCodeDatabaseEffect(dbPath, opts.cutoff, logger, opts.excludePath))
3750
5006
  )
3751
5007
  );
3752
5008
  }
3753
5009
  return stream;
3754
5010
  }
3755
5011
  function openCodeJsonFileStreams(paths) {
3756
- return paths.pipe(Stream13.flatMap((file) => Stream13.fromIterableEffect(bucketizeOpenCodeJsonFileEffect(file))));
5012
+ return paths.pipe(Stream14.flatMap((file) => Stream14.fromIterableEffect(bucketizeOpenCodeJsonFileEffect(file))));
3757
5013
  }
3758
5014
 
3759
5015
  // ../../packages/parsers/src/pi/index.ts
3760
5016
  import { basename as basename3, join as join7 } from "path";
3761
- 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";
3762
5018
 
3763
5019
  // ../../packages/parsers/src/pi/bucketize.ts
3764
5020
  import { createReadStream as createReadStream3 } from "fs";
3765
5021
  import { createInterface as createInterface4 } from "readline";
3766
- 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";
3767
5023
 
3768
5024
  // ../../packages/parsers/src/pi/events.ts
3769
5025
  function isObject4(value) {
@@ -3842,9 +5098,9 @@ function key5(hourUtc, model) {
3842
5098
  return `${hourUtc.toISOString()}|${model}`;
3843
5099
  }
3844
5100
  function lineStreamFromFile3(path) {
3845
- return Stream14.callback(
3846
- (queue) => Effect23.acquireRelease(
3847
- Effect23.sync(() => {
5101
+ return Stream15.callback(
5102
+ (queue) => Effect24.acquireRelease(
5103
+ Effect24.sync(() => {
3848
5104
  let done = false;
3849
5105
  const stream = createReadStream3(path, { encoding: "utf8" });
3850
5106
  const rl = createInterface4({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
@@ -3881,7 +5137,7 @@ function lineStreamFromFile3(path) {
3881
5137
  }
3882
5138
  };
3883
5139
  }),
3884
- (resource) => Effect23.sync(() => resource.close())
5140
+ (resource) => Effect24.sync(() => resource.close())
3885
5141
  )
3886
5142
  );
3887
5143
  }
@@ -3899,6 +5155,33 @@ function modelId2(provider, model) {
3899
5155
  }
3900
5156
  return `${trimmedProvider}/${trimmedModel}`;
3901
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
+ }
3902
5185
  function normalizeProvider(provider) {
3903
5186
  return provider === "openai-codex" ? "openai" : provider;
3904
5187
  }
@@ -3915,53 +5198,134 @@ function timestampMs2(value) {
3915
5198
  }
3916
5199
  return Math.abs(n) < UNIX_MILLISECONDS_THRESHOLD ? n * MILLISECONDS_PER_SECOND2 : n;
3917
5200
  }
3918
- function hermesRowToBucket(row, excludePath) {
3919
- if (typeof row.cwd === "string" && excludePath?.(row.cwd) === true) {
3920
- return void 0;
3921
- }
3922
- const sessionId = typeof row.id === "string" ? row.id : void 0;
3923
- const startedAtMs = timestampMs2(row.startedAt);
3924
- const lastSeenAtMs = timestampMs2(row.endedAt) ?? startedAtMs;
3925
- const provider = typeof row.billingProvider === "string" ? row.billingProvider : void 0;
3926
- const sourceModel = typeof row.model === "string" ? row.model : void 0;
3927
- if (sessionId === void 0 || startedAtMs === void 0 || lastSeenAtMs === void 0 || provider === void 0 || sourceModel === void 0) {
3928
- return void 0;
3929
- }
3930
- const model = modelId2(provider, sourceModel);
3931
- if (model === void 0) {
3932
- 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
+ }
3933
5235
  }
3934
- const inputTokens = positive2(row.inputTokens);
3935
- const outputTokens = positive2(row.outputTokens);
3936
- const cacheReadTokens = positive2(row.cacheReadTokens);
3937
- const cacheCreateTokens = positive2(row.cacheWriteTokens);
3938
- if (inputTokens === 0 && outputTokens === 0 && cacheReadTokens === 0 && cacheCreateTokens === 0) {
5236
+ return latest;
5237
+ }
5238
+ function makeHermesBucket(options) {
5239
+ if (!hasHermesUsage(options.usage)) {
3939
5240
  return void 0;
3940
5241
  }
3941
- const startedAt = new Date(startedAtMs);
5242
+ const startedAt = new Date(options.startedAtMs);
3942
5243
  return {
3943
5244
  harness: "hermes",
3944
- model,
3945
- hourUtc: startOfUtcHour(startedAt),
3946
- inputTokens,
3947
- outputTokens,
3948
- cacheReadTokens,
3949
- cacheCreateTokens,
3950
- 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}`]),
3951
5252
  sessionStartedAt: startedAt,
3952
- sessionLastSeenAt: new Date(lastSeenAtMs)
5253
+ sessionLastSeenAt: new Date(options.lastSeenAtMs)
3953
5254
  };
3954
5255
  }
3955
- function bucketizeHermesRows(rows, excludePath) {
5256
+ function bucketizeHermesModelUsage(options) {
3956
5257
  const buckets = [];
3957
- for (const row of rows) {
3958
- 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
+ });
3959
5278
  if (bucket !== void 0) {
3960
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);
3961
5318
  }
3962
5319
  }
3963
5320
  return buckets;
3964
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
+ }
3965
5329
  function makePiStyleFileAccumulator(options) {
3966
5330
  return {
3967
5331
  buckets: /* @__PURE__ */ new Map(),
@@ -4054,12 +5418,11 @@ function accumulatePiStyleLine(context, acc, raw) {
4054
5418
  existing.cacheCreateTokens += cacheCreateTokens;
4055
5419
  return acc;
4056
5420
  }
4057
- function bucketsFromAccumulator3(path, harness, acc) {
5421
+ function bucketsFromAccumulator2(path, harness, acc) {
4058
5422
  if (acc.excluded || acc.sessionStartedAt === void 0 || acc.sessionLastSeenAt === void 0) {
4059
5423
  return [];
4060
5424
  }
4061
- const sessionStartedAt = acc.sessionStartedAt;
4062
- const sessionLastSeenAt = acc.sessionLastSeenAt;
5425
+ const { sessionLastSeenAt, sessionStartedAt } = acc;
4063
5426
  const sessionIds = /* @__PURE__ */ new Set([path]);
4064
5427
  return Array.from(acc.buckets.values(), (bucket) => ({
4065
5428
  harness,
@@ -4076,17 +5439,17 @@ function bucketsFromAccumulator3(path, harness, acc) {
4076
5439
  }
4077
5440
  function bucketizePiStyleFileStream(path, harness, logger, options = {}) {
4078
5441
  const context = { logger, options, path };
4079
- return Stream14.unwrap(
5442
+ return Stream15.unwrap(
4080
5443
  lineStreamFromFile3(path).pipe(
4081
- Stream14.runFold(
5444
+ Stream15.runFold(
4082
5445
  () => makePiStyleFileAccumulator(options),
4083
5446
  (acc, raw) => accumulatePiStyleLine(context, acc, raw)
4084
5447
  ),
4085
- Effect23.map((acc) => Stream14.fromIterable(bucketsFromAccumulator3(path, harness, acc)))
5448
+ Effect24.map((acc) => Stream15.fromIterable(bucketsFromAccumulator2(path, harness, acc)))
4086
5449
  )
4087
5450
  );
4088
5451
  }
4089
- var bucketizeHermesDatabaseEffect = Effect23.fn(
5452
+ var bucketizeHermesDatabaseEffect = Effect24.fn(
4090
5453
  "bucketizeHermesDatabaseEffect"
4091
5454
  )(function* (path, cutoff, excludePath) {
4092
5455
  const sqlite = yield* HermesSqlite;
@@ -4095,14 +5458,14 @@ var bucketizeHermesDatabaseEffect = Effect23.fn(
4095
5458
  });
4096
5459
 
4097
5460
  // ../../packages/parsers/src/pi/index.ts
4098
- var isFileBestEffortEffect = (path) => pathKindEffect(path).pipe(Effect24.map((kind) => kind === "file"));
5461
+ var isFileBestEffortEffect = (path) => pathKindEffect(path).pipe(Effect25.map((kind) => kind === "file"));
4099
5462
  function bucketizePiStyleSourcesStream(files, harness, logger, options) {
4100
5463
  return files.pipe(
4101
- Stream15.flatMap((file) => bucketizePiStyleFileStream(file, harness, logger, options), { concurrency: 1 })
5464
+ Stream16.flatMap((file) => bucketizePiStyleFileStream(file, harness, logger, options), { concurrency: 1 })
4102
5465
  );
4103
5466
  }
4104
- var parsePiStyleStream = (opts, harness, defaultRoot) => Stream15.unwrap(
4105
- Effect24.gen(function* () {
5467
+ var parsePiStyleStream = (opts, harness, defaultRoot) => Stream16.unwrap(
5468
+ Effect25.gen(function* () {
4106
5469
  const logger = opts.logger ?? silentLogger;
4107
5470
  const root = opts.root ?? defaultRoot();
4108
5471
  const kind = yield* pathKindEffectStrict(root);
@@ -4120,25 +5483,23 @@ var parsePiStyleStream = (opts, harness, defaultRoot) => Stream15.unwrap(
4120
5483
  });
4121
5484
  })
4122
5485
  );
4123
- var parsePiEffect = Effect24.fn("PiParser.parsePiEffect")((opts) => Stream15.runCollect(parsePiStream(opts)));
5486
+ var parsePiEffect = Effect25.fn("PiParser.parsePiEffect")((opts) => Stream16.runCollect(parsePiStream(opts)));
4124
5487
  var parsePiStream = (opts) => parsePiStyleStream(opts, "pi", resolvePiRoot);
4125
5488
  var parserLive = Layer10.mergeAll(FileWalkerLive, HermesSqliteLive);
4126
- var parseHermesEffect = Effect24.fn(
5489
+ var parseHermesEffect = Effect25.fn(
4127
5490
  "HermesParser.parseHermesEffect"
4128
- )((opts) => Stream15.runCollect(parseHermesStream(opts)));
4129
- var parseHermesStream = (opts) => Stream15.unwrap(
4130
- Effect24.gen(function* () {
5491
+ )((opts) => Stream16.runCollect(parseHermesStream(opts)));
5492
+ var parseHermesStream = (opts) => Stream16.unwrap(
5493
+ Effect25.gen(function* () {
4131
5494
  const logger = opts.logger ?? silentLogger;
4132
5495
  const root = opts.root ?? resolveHermesRoot();
4133
5496
  const kind = yield* pathKindEffectStrict(root);
4134
5497
  if (kind === "missing") {
4135
- return asHermesStream(Stream15.empty);
5498
+ return asHermesStream(Stream16.empty);
4136
5499
  }
4137
5500
  if (kind === "file") {
4138
5501
  if (basename3(root).endsWith(".db")) {
4139
- return asHermesStream(
4140
- Stream15.fromIterableEffect(bucketizeHermesDatabaseEffect(root, opts.cutoff, opts.excludePath))
4141
- );
5502
+ return asHermesStream(collectHermesDatabaseStream(root, logger, opts));
4142
5503
  }
4143
5504
  return asHermesStream(
4144
5505
  bucketizePiStyleFileStream(root, "hermes", logger, { cutoff: opts.cutoff, excludePath: opts.excludePath })
@@ -4146,9 +5507,7 @@ var parseHermesStream = (opts) => Stream15.unwrap(
4146
5507
  }
4147
5508
  const stateDb = join7(root, "state.db");
4148
5509
  if (yield* isFileBestEffortEffect(stateDb)) {
4149
- return asHermesStream(
4150
- Stream15.fromIterableEffect(bucketizeHermesDatabaseEffect(stateDb, opts.cutoff, opts.excludePath))
4151
- );
5510
+ return asHermesStream(collectHermesDatabaseStream(stateDb, logger, opts));
4152
5511
  }
4153
5512
  const sessionsRoot2 = join7(root, "sessions");
4154
5513
  if ((yield* pathKindEffect(sessionsRoot2)) === "directory") {
@@ -4164,8 +5523,20 @@ var parseHermesStream = (opts) => Stream15.unwrap(
4164
5523
  function asHermesStream(stream) {
4165
5524
  return stream;
4166
5525
  }
4167
- var collectHermesPiStyleDirectoryStream = (root, logger, opts) => Stream15.unwrap(
4168
- 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(() => {
4169
5540
  const seenMessageIds = /* @__PURE__ */ new Set();
4170
5541
  return bucketizePiStyleSourcesStream(walkStreamStrict(root, opts.cutoff), "hermes", logger, {
4171
5542
  cutoff: opts.cutoff,
@@ -4176,7 +5547,7 @@ var collectHermesPiStyleDirectoryStream = (root, logger, opts) => Stream15.unwra
4176
5547
  );
4177
5548
 
4178
5549
  // src/sync/parser-factory.ts
4179
- 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";
4180
5551
  var GLOB_PATTERN = /[*?[\]{}]/u;
4181
5552
  var HermesParserLive = Layer11.mergeAll(FileWalkerLive, HermesSqliteLive);
4182
5553
  function sessionsRoot(agentDir, sessionDir, fallbackAgentDir) {
@@ -4241,7 +5612,7 @@ function createExcludeMatcher(options) {
4241
5612
  };
4242
5613
  }
4243
5614
  function resolveParserSourcePlanEffect(options = {}) {
4244
- return Effect25.gen(function* () {
5615
+ return Effect26.gen(function* () {
4245
5616
  const sources = yield* ParserSources;
4246
5617
  const home = yield* sources.homeDir;
4247
5618
  const env = options.env ?? (yield* sources.cliEnv);
@@ -4280,49 +5651,43 @@ function resolveParserSourcePlanEffect(options = {}) {
4280
5651
  });
4281
5652
  }
4282
5653
  function provideFileWalker(stream) {
4283
- return stream.pipe(Stream16.provide(FileWalkerLive));
4284
- }
4285
- function concatParserStreams(streams) {
4286
- let stream = Stream16.empty;
4287
- for (const next of streams) {
4288
- 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
+ );
4289
5666
  }
4290
- return stream;
5667
+ if (plan.codexArchivedSourceExisted) {
5668
+ return provideFileWalker(
5669
+ parseCodexStream({ cutoff, excludePath, root: plan.codexArchivedRoot })
5670
+ );
5671
+ }
5672
+ return Stream17.empty;
4291
5673
  }
4292
5674
  function buildFactoryFromPlan(plan) {
4293
5675
  const excludePath = createExcludeMatcher({ exclude: plan.exclude, home: plan.home });
4294
- return (ctx) => {
4295
- const codexStreams = [];
4296
- if (plan.codexSessionsSourceExisted) {
4297
- codexStreams.push(
4298
- provideFileWalker(
4299
- parseCodexStream({ cutoff: ctx.cutoff, excludePath, root: plan.codexSessionsRoot })
4300
- )
4301
- );
4302
- }
4303
- if (plan.codexArchivedSourceExisted) {
4304
- codexStreams.push(
4305
- provideFileWalker(
4306
- parseCodexStream({ cutoff: ctx.cutoff, excludePath, root: plan.codexArchivedRoot })
4307
- )
4308
- );
4309
- }
4310
- return {
4311
- anySourceExisted: plan.anySourceExisted,
4312
- codex: concatParserStreams(codexStreams),
4313
- claudeCode: plan.claudeSourceExisted ? provideFileWalker(parseClaudeStream({ cutoff: ctx.cutoff, excludePath, root: plan.claudeRoot })) : Stream16.empty,
4314
- pi: plan.piSourceExisted ? provideFileWalker(parsePiStream({ cutoff: ctx.cutoff, excludePath, root: plan.piRoot })) : Stream16.empty,
4315
- hermes: plan.hermesSourceExisted ? parseHermesStream({ cutoff: ctx.cutoff, excludePath, root: plan.hermesRoot }).pipe(
4316
- Stream16.provide(HermesParserLive)
4317
- ) : Stream16.empty,
4318
- opencode: plan.opencodeSourceExisted ? parseOpenCodeStream({ cutoff: ctx.cutoff, excludePath, root: plan.opencodeRoot }).pipe(
4319
- Stream16.provide(OpenCodeIoLive)
4320
- ) : Stream16.empty
4321
- };
4322
- };
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
+ });
4323
5688
  }
4324
5689
  function buildRealParserFactoryEffect(options = {}) {
4325
- return Effect25.map(resolveParserSourcePlanEffect(options), buildFactoryFromPlan);
5690
+ return Effect26.map(resolveParserSourcePlanEffect(options), buildFactoryFromPlan);
4326
5691
  }
4327
5692
 
4328
5693
  // src/index.ts
@@ -4356,8 +5721,40 @@ function parseHarnessFilter(value) {
4356
5721
  }
4357
5722
  throw new SyncError(EXIT.invalidArguments, `error: --harness must be one of ${SUPPORTED_HARNESSES.join("|")}`);
4358
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
+ }
4359
5756
  function syncCommandErrorExitCode(err) {
4360
- return Effect26.gen(function* () {
5757
+ return Effect27.gen(function* () {
4361
5758
  const console = yield* CliConsole;
4362
5759
  if (err instanceof CliConfigError) {
4363
5760
  yield* console.writeStderrLine(err.message);
@@ -4372,7 +5769,7 @@ function syncCommandErrorExitCode(err) {
4372
5769
  });
4373
5770
  }
4374
5771
  function setupCommandErrorExitCode(err) {
4375
- return Effect26.gen(function* () {
5772
+ return Effect27.gen(function* () {
4376
5773
  const console = yield* CliConsole;
4377
5774
  if (err instanceof CliConfigError || err instanceof SetupError) {
4378
5775
  yield* console.writeStderrLine(err.message);
@@ -4384,26 +5781,26 @@ function setupCommandErrorExitCode(err) {
4384
5781
  }
4385
5782
  function recoverCommandExitCode(effect) {
4386
5783
  return effect.pipe(
4387
- Effect26.catch((err) => syncCommandErrorExitCode(err)),
4388
- Effect26.catchDefect((err) => syncCommandErrorExitCode(err))
5784
+ Effect27.catch((err) => syncCommandErrorExitCode(err)),
5785
+ Effect27.catchDefect((err) => syncCommandErrorExitCode(err))
4389
5786
  );
4390
5787
  }
4391
5788
  function recoverSetupExitCode(effect) {
4392
5789
  return effect.pipe(
4393
- Effect26.catch((err) => setupCommandErrorExitCode(err)),
4394
- Effect26.catchDefect((err) => setupCommandErrorExitCode(err))
5790
+ Effect27.catch((err) => setupCommandErrorExitCode(err)),
5791
+ Effect27.catchDefect((err) => setupCommandErrorExitCode(err))
4395
5792
  );
4396
5793
  }
4397
5794
  function exitCodeToCommandEffect(exitCode) {
4398
- return exitCode === EXIT.success ? Effect26.void : Effect26.fail(new CliExit(exitCode));
5795
+ return exitCode === EXIT.success ? Effect27.void : Effect27.fail(new CliExit(exitCode));
4399
5796
  }
4400
5797
  function runSyncCommand(raw) {
4401
5798
  return recoverCommandExitCode(
4402
- Effect26.gen(function* () {
5799
+ Effect27.gen(function* () {
4403
5800
  const configReader = yield* CliConfigReader;
4404
5801
  const console = yield* CliConsole;
4405
5802
  const env = yield* configReader.read();
4406
- const config = yield* Effect26.try({
5803
+ const config = yield* Effect27.try({
4407
5804
  try: () => resolveSyncConfig(
4408
5805
  {
4409
5806
  api: optionalValue(raw.api),
@@ -4417,10 +5814,14 @@ function runSyncCommand(raw) {
4417
5814
  catch: (err) => err
4418
5815
  });
4419
5816
  const { device, token } = config;
4420
- const harness = yield* Effect26.try({
5817
+ const harness = yield* Effect27.try({
4421
5818
  try: () => parseHarnessFilter(optionalValue(raw.harness)),
4422
5819
  catch: (err) => err
4423
5820
  });
5821
+ const replacement = yield* Effect27.try({
5822
+ try: () => parseReplacementFlags(raw),
5823
+ catch: (err) => err
5824
+ });
4424
5825
  if (!(token && device)) {
4425
5826
  yield* console.writeStderrLine(
4426
5827
  "error: --token and --device are required (or set ~/.oopst.yml / OOPST_TOKEN / OOPST_DEVICE)"
@@ -4428,7 +5829,7 @@ function runSyncCommand(raw) {
4428
5829
  return EXIT.invalidArguments;
4429
5830
  }
4430
5831
  const factory = yield* buildRealParserFactoryEffect({ env, exclude: config.exclude }).pipe(
4431
- Effect26.provide(ParserSourcesLive)
5832
+ Effect27.provide(ParserSourcesLive)
4432
5833
  );
4433
5834
  let sourcesExistedHint = false;
4434
5835
  const parserFactory = (ctx) => {
@@ -4442,6 +5843,7 @@ function runSyncCommand(raw) {
4442
5843
  serverUrl: config.serverUrl,
4443
5844
  lookbackDays: config.lookbackDays,
4444
5845
  harness,
5846
+ replacement,
4445
5847
  dryRun: raw.dryRun,
4446
5848
  verbose: raw.verbose,
4447
5849
  color: yield* console.stdoutIsTTY,
@@ -4453,16 +5855,16 @@ function runSyncCommand(raw) {
4453
5855
  });
4454
5856
  return result.exitCode;
4455
5857
  })
4456
- ).pipe(Effect26.flatMap(exitCodeToCommandEffect));
5858
+ ).pipe(Effect27.flatMap(exitCodeToCommandEffect));
4457
5859
  }
4458
5860
  function runSetupCommand(raw) {
4459
5861
  return recoverSetupExitCode(
4460
- Effect26.gen(function* () {
5862
+ Effect27.gen(function* () {
4461
5863
  const console = yield* CliConsole;
4462
5864
  const metadata = yield* CliPackageMetadata;
4463
5865
  const cliProcess = yield* CliProcess;
4464
5866
  const nodePath = yield* cliProcess.execPath;
4465
- const result = yield* Effect26.tryPromise({
5867
+ const result = yield* Effect27.tryPromise({
4466
5868
  try: () => runSetup({
4467
5869
  api: optionalValue(raw.api),
4468
5870
  server: optionalValue(raw.server),
@@ -4475,7 +5877,7 @@ function runSetupCommand(raw) {
4475
5877
  });
4476
5878
  return result.exitCode;
4477
5879
  })
4478
- ).pipe(Effect26.flatMap(exitCodeToCommandEffect));
5880
+ ).pipe(Effect27.flatMap(exitCodeToCommandEffect));
4479
5881
  }
4480
5882
  var apiFlag = Flag.string("api").pipe(
4481
5883
  Flag.withDescription("API base URL (or server.api / OOPST_SERVER)"),
@@ -4498,6 +5900,22 @@ var harnessFlag = Flag.string("harness").pipe(
4498
5900
  Flag.withDescription("Restrict to codex|claude_code|pi|hermes|opencode"),
4499
5901
  Flag.optional
4500
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
+ );
4501
5919
  var dryRunFlag = Flag.boolean("dry-run").pipe(
4502
5920
  Flag.withDescription("Parse and print without uploading"),
4503
5921
  Flag.withDefault(false)
@@ -4513,6 +5931,10 @@ function buildCli() {
4513
5931
  server: serverFlag,
4514
5932
  lookbackDays: lookbackDaysFlag,
4515
5933
  harness: harnessFlag,
5934
+ replaceStart: replaceStartFlag,
5935
+ replaceEnd: replaceEndFlag,
5936
+ replaceHarnesses: replaceHarnessesFlag,
5937
+ confirmReplacement: confirmReplacementFlag,
4516
5938
  dryRun: dryRunFlag,
4517
5939
  verbose: verboseFlag
4518
5940
  },
@@ -4532,10 +5954,10 @@ function buildCli() {
4532
5954
  );
4533
5955
  }
4534
5956
  function runCli() {
4535
- return Effect26.gen(function* () {
5957
+ return Effect27.gen(function* () {
4536
5958
  const metadata = yield* CliPackageMetadata;
4537
5959
  yield* buildCli().pipe(Command.run({ version: metadata.version }));
4538
- }).pipe(Effect26.provide(CliRuntimeLayer));
5960
+ }).pipe(Effect27.provide(CliRuntimeLayer));
4539
5961
  }
4540
5962
  if (isEntrypoint()) {
4541
5963
  runCli().pipe(NodeRuntime.runMain);