@cookielab.io/oopst 0.6.2 → 0.6.4

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
@@ -4,23 +4,23 @@ const require = createRequire(import.meta.url);
4
4
 
5
5
  // src/index.ts
6
6
  import { realpathSync } from "fs";
7
- import process4 from "process";
7
+ import process2 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
14
14
  import { readFileSync } from "fs";
15
15
  import { homedir as readHomedir, hostname as readHostname } from "os";
16
16
  import { isAbsolute, join, normalize, resolve } from "path";
17
- import process from "process";
17
+ import { env as processEnv } from "process";
18
18
  import { Config, ConfigProvider, Effect, FileSystem, Option, Schema } from "effect";
19
19
  import { parse as parseYaml } from "yaml";
20
20
  var DEFAULT_SERVER_URL = "http://localhost:15433";
21
21
  var DEFAULT_LOOKBACK_DAYS = 2;
22
22
  var USER_CONFIG_FILE = ".oopst.yml";
23
- var runtimeEnv = process["env"];
23
+ var runtimeEnv = processEnv;
24
24
  var NonEmptyString = Schema.String.check(Schema.isNonEmpty());
25
25
  var NonNegativeInteger = Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0));
26
26
  var CliConfigSourceSchema = Schema.Struct({
@@ -261,31 +261,42 @@ function resolveSyncConfig(flags, env) {
261
261
 
262
262
  // src/services/command-platform.ts
263
263
  import { homedir as readHomedir2, hostname as readHostname2 } from "os";
264
- import process2 from "process";
264
+ import process from "process";
265
265
  import { fileURLToPath } from "url";
266
266
  import { NodeFileSystem, NodeServices } from "@effect/platform-node";
267
- import { Context, Effect as Effect2, FileSystem as FileSystem2, Layer, Stream } from "effect";
267
+ import { Effect as Effect2, FileSystem as FileSystem2, Layer, Stream } from "effect";
268
268
  import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
269
- var PACKAGE_JSON_PATHS = [
270
- fileURLToPath(new URL("../package.json", import.meta.url)),
271
- fileURLToPath(new URL("../../package.json", import.meta.url))
272
- ];
269
+
270
+ // src/services/command-console-services.ts
271
+ import { Context } from "effect";
273
272
  var CliConsole = class extends Context.Service()("@oopst/cli/CliConsole") {
274
273
  };
275
274
  var CliProcess = class extends Context.Service()("@oopst/cli/CliProcess") {
276
275
  };
277
- var CliHost = class extends Context.Service()("@oopst/cli/CliHost") {
276
+
277
+ // src/services/command-host-services.ts
278
+ import { Context as Context2 } from "effect";
279
+ var CliHost = class extends Context2.Service()("@oopst/cli/CliHost") {
278
280
  };
279
- var CliCommandRunner = class extends Context.Service()(
281
+ var CliCommandRunner = class extends Context2.Service()(
280
282
  "@oopst/cli/CliCommandRunner"
281
283
  ) {
282
284
  };
283
- var CliPackageMetadata = class extends Context.Service()(
285
+
286
+ // src/services/command-metadata-services.ts
287
+ import { Context as Context3 } from "effect";
288
+ var CliPackageMetadata = class extends Context3.Service()(
284
289
  "@oopst/cli/CliPackageMetadata"
285
290
  ) {
286
291
  };
287
- var CliConfigReader = class extends Context.Service()("@oopst/cli/CliConfigReader") {
292
+ var CliConfigReader = class extends Context3.Service()("@oopst/cli/CliConfigReader") {
288
293
  };
294
+
295
+ // src/services/command-platform.ts
296
+ var PACKAGE_JSON_PATHS = [
297
+ fileURLToPath(new URL("../package.json", import.meta.url)),
298
+ fileURLToPath(new URL("../../package.json", import.meta.url))
299
+ ];
289
300
  function writeLine(output, message) {
290
301
  output.write(`${message}
291
302
  `);
@@ -340,9 +351,9 @@ var CliConsoleLive = Layer.effect(
340
351
  Effect2.sync(
341
352
  () => CliConsole.of(
342
353
  makeCliConsole({
343
- stdin: process2.stdin,
344
- stdout: process2.stdout,
345
- stderr: process2.stderr
354
+ stdin: process.stdin,
355
+ stdout: process.stdout,
356
+ stderr: process.stderr
346
357
  })
347
358
  )
348
359
  )
@@ -353,7 +364,7 @@ var CliProcessLive = Layer.effect(
353
364
  () => CliProcess.of({
354
365
  execPath: Effect2.fn("CliProcess.execPath")(
355
366
  () => Effect2.try({
356
- try: () => process2.execPath,
367
+ try: () => process.execPath,
357
368
  catch: (err) => err
358
369
  })
359
370
  )()
@@ -443,8 +454,8 @@ var CliCommandPlatformLive = Layer.mergeAll(
443
454
  import { homedir } from "os";
444
455
  import { join as join2 } from "path";
445
456
  import { NodeFileSystem as NodeFileSystem2 } from "@effect/platform-node";
446
- import { Context as Context2, Effect as Effect3, FileSystem as FileSystem3, Layer as Layer2, Schema as Schema2 } from "effect";
447
- var ParserSources = class extends Context2.Service()("@oopst/cli/ParserSources") {
457
+ import { Context as Context4, Effect as Effect3, FileSystem as FileSystem3, Layer as Layer2, Schema as Schema2 } from "effect";
458
+ var ParserSources = class extends Context4.Service()("@oopst/cli/ParserSources") {
448
459
  };
449
460
  var ParserSourceError = class extends Schema2.TaggedErrorClass()("ParserSourceError", {
450
461
  cause: Schema2.optionalKey(Schema2.Defect()),
@@ -521,223 +532,106 @@ var ParserSourcesLive = Layer2.effect(
521
532
  ).pipe(Layer2.provide(NodeFileSystem2.layer));
522
533
 
523
534
  // src/services/sync-program.ts
524
- import { Context as Context6, Effect as Effect10, Layer as Layer5 } from "effect";
535
+ import { Context as Context8, Effect as Effect10, Layer as Layer5 } from "effect";
525
536
 
526
537
  // src/sync/run.ts
527
- import { Effect as Effect9, Stream as Stream4 } from "effect";
538
+ import { createHash } from "crypto";
528
539
 
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())
540
+ // ../../packages/api/src/errors/auth-errors.ts
541
+ import { Schema as Schema3 } from "effect";
542
+ var ErrorFields = {
543
+ message: Schema3.String
535
544
  };
536
- var UsageAggregationError = class extends Schema3.TaggedErrorClass()(
537
- "UsageAggregationError",
538
- UsageAggregationErrorFields
539
- ) {
545
+ var UnauthorizedError = class extends Schema3.TaggedErrorClass()("UnauthorizedError", ErrorFields) {
546
+ };
547
+ var ForbiddenError = class extends Schema3.TaggedErrorClass()("ForbiddenError", ErrorFields) {
540
548
  };
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
- }
674
549
 
675
- // ../../packages/api/src/errors.ts
550
+ // ../../packages/api/src/errors/config-errors.ts
676
551
  import { Schema as Schema4 } from "effect";
677
- var ErrorFields = {
678
- message: Schema4.String
679
- };
680
552
  var InfrastructureErrorFields = {
681
- ...ErrorFields,
553
+ message: Schema4.String,
682
554
  cause: Schema4.optionalKey(Schema4.Defect())
683
555
  };
684
- var UnauthorizedError = class extends Schema4.TaggedErrorClass()("UnauthorizedError", ErrorFields) {
556
+ var AppConfigError = class extends Schema4.TaggedErrorClass()("AppConfigError", InfrastructureErrorFields) {
557
+ };
558
+ var PricingRefreshError = class extends Schema4.TaggedErrorClass()(
559
+ "PricingRefreshError",
560
+ InfrastructureErrorFields
561
+ ) {
685
562
  };
686
- var ForbiddenError = class extends Schema4.TaggedErrorClass()("ForbiddenError", ErrorFields) {
563
+
564
+ // ../../packages/api/src/errors/infrastructure-errors.ts
565
+ import { Schema as Schema5 } from "effect";
566
+ var InfrastructureErrorFields2 = {
567
+ message: Schema5.String,
568
+ cause: Schema5.optionalKey(Schema5.Defect())
569
+ };
570
+ var ExternalHttpError = class extends Schema5.TaggedErrorClass()(
571
+ "ExternalHttpError",
572
+ InfrastructureErrorFields2
573
+ ) {
687
574
  };
688
- var NotFoundError = class extends Schema4.TaggedErrorClass()("NotFoundError", ErrorFields) {
575
+ var DatabaseError = class extends Schema5.TaggedErrorClass()("DatabaseError", InfrastructureErrorFields2) {
689
576
  };
690
- var ConflictError = class extends Schema4.TaggedErrorClass()("ConflictError", ErrorFields) {
577
+
578
+ // ../../packages/api/src/errors/request-errors.ts
579
+ import { Schema as Schema6 } from "effect";
580
+ var ErrorFields2 = {
581
+ message: Schema6.String
691
582
  };
692
- var ValidationError = class extends Schema4.TaggedErrorClass()("ValidationError", ErrorFields) {
583
+ var ValidationError = class extends Schema6.TaggedErrorClass()("ValidationError", ErrorFields2) {
693
584
  };
694
- var RateLimitedError = class extends Schema4.TaggedErrorClass()("RateLimitedError", ErrorFields) {
585
+ var RateLimitedError = class extends Schema6.TaggedErrorClass()("RateLimitedError", ErrorFields2) {
695
586
  };
696
- var ExternalHttpError = class extends Schema4.TaggedErrorClass()(
697
- "ExternalHttpError",
698
- InfrastructureErrorFields
699
- ) {
587
+
588
+ // ../../packages/api/src/errors/resource-errors.ts
589
+ import { Schema as Schema7 } from "effect";
590
+ var ErrorFields3 = {
591
+ message: Schema7.String
700
592
  };
701
- var DatabaseError = class extends Schema4.TaggedErrorClass()(
702
- "DatabaseError",
703
- InfrastructureErrorFields
704
- ) {
593
+ var NotFoundError = class extends Schema7.TaggedErrorClass()("NotFoundError", ErrorFields3) {
705
594
  };
706
- var AppConfigError = class extends Schema4.TaggedErrorClass()(
707
- "AppConfigError",
708
- InfrastructureErrorFields
709
- ) {
595
+ var ConflictError = class extends Schema7.TaggedErrorClass()("ConflictError", ErrorFields3) {
710
596
  };
711
- var PricingRefreshError = class extends Schema4.TaggedErrorClass()(
712
- "PricingRefreshError",
713
- InfrastructureErrorFields
714
- ) {
597
+
598
+ // ../../packages/api/src/errors/source-errors.ts
599
+ import { Schema as Schema8 } from "effect";
600
+ var ErrorFields4 = {
601
+ message: Schema8.String
715
602
  };
716
- var ParserSourceError2 = class extends Schema4.TaggedErrorClass()(
603
+ var InfrastructureErrorFields3 = {
604
+ ...ErrorFields4,
605
+ cause: Schema8.optionalKey(Schema8.Defect())
606
+ };
607
+ var ParserSourceError2 = class extends Schema8.TaggedErrorClass()(
717
608
  "ParserSourceError",
718
- InfrastructureErrorFields
609
+ InfrastructureErrorFields3
719
610
  ) {
720
611
  };
721
- var SyncQuotaExceededError = class extends Schema4.TaggedErrorClass()(
612
+ var SyncQuotaExceededError = class extends Schema8.TaggedErrorClass()(
722
613
  "SyncQuotaExceededError",
723
- ErrorFields
614
+ ErrorFields4
724
615
  ) {
725
616
  };
726
- var SyncTimestampRejectedError = class extends Schema4.TaggedErrorClass()(
617
+
618
+ // ../../packages/api/src/errors/sync-errors.ts
619
+ import { Schema as Schema9 } from "effect";
620
+ var SyncTimestampRejectedError = class extends Schema9.TaggedErrorClass()(
727
621
  "SyncTimestampRejectedError",
728
- ErrorFields
622
+ { message: Schema9.String }
729
623
  ) {
730
624
  };
731
625
 
732
626
  // ../../packages/api/src/rpc/auth.ts
733
- import { Context as Context3, Schema as Schema7 } from "effect";
627
+ import { Context as Context5, Schema as Schema12 } from "effect";
734
628
  import { Rpc, RpcGroup } from "effect/unstable/rpc";
735
629
 
736
630
  // ../../packages/api/src/schemas/auth.ts
737
- import { Schema as Schema5, SchemaGetter } from "effect";
631
+ import { Schema as Schema10, SchemaGetter } from "effect";
738
632
  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), {
633
+ var displayNameInput = Schema10.String.check(Schema10.isMaxLength(MAX_DISPLAY_NAME_LENGTH)).pipe(
634
+ Schema10.decodeTo(Schema10.NullOr(Schema10.String), {
741
635
  decode: SchemaGetter.transform((value) => {
742
636
  const trimmed = value.trim();
743
637
  return trimmed.length === 0 ? null : trimmed;
@@ -745,12 +639,12 @@ var displayNameInput = Schema5.String.check(Schema5.isMaxLength(MAX_DISPLAY_NAME
745
639
  encode: SchemaGetter.transform((value) => value ?? "")
746
640
  })
747
641
  );
748
- var updateProfileInput = Schema5.Struct({
642
+ var updateProfileInput = Schema10.Struct({
749
643
  displayName: displayNameInput
750
644
  });
751
645
 
752
646
  // ../../packages/api/src/schemas/common.ts
753
- import { Schema as Schema6 } from "effect";
647
+ import { Schema as Schema11 } from "effect";
754
648
  var ISO_UTC_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u;
755
649
  var CIVIL_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/u;
756
650
  var HEX64_PATTERN = /^[0-9a-f]{64}$/u;
@@ -795,49 +689,49 @@ var isApiUrl = (value) => {
795
689
  return false;
796
690
  }
797
691
  };
798
- var nonEmptyTrimmedString = Schema6.String.check(
799
- Schema6.isTrimmed(),
800
- Schema6.isNonEmpty({ message: "Expected a non-empty string" })
692
+ var nonEmptyTrimmedString = Schema11.String.check(
693
+ Schema11.isTrimmed(),
694
+ Schema11.isNonEmpty({ message: "Expected a non-empty string" })
801
695
  );
802
- var safeInteger = Schema6.Number.check(
803
- Schema6.isInt(),
804
- Schema6.isBetween({ minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER })
696
+ var safeInteger = Schema11.Number.check(
697
+ Schema11.isInt(),
698
+ Schema11.isBetween({ minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER })
805
699
  );
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" })
700
+ var nonNegativeSafeInteger = safeInteger.check(Schema11.isGreaterThanOrEqualTo(0));
701
+ var isoUtcString = Schema11.String.check(
702
+ Schema11.makeFilter(isExactUtcInstant, { expected: "an ISO-8601 UTC timestamp with millisecond precision" })
809
703
  );
810
- var civilDate = Schema6.String.check(
811
- Schema6.makeFilter(isCivilDate, { expected: "a valid calendar date formatted as YYYY-MM-DD" })
704
+ var civilDate = Schema11.String.check(
705
+ Schema11.makeFilter(isCivilDate, { expected: "a valid calendar date formatted as YYYY-MM-DD" })
812
706
  );
813
- var ianaTimezone = Schema6.String.check(
814
- Schema6.makeFilter(isIanaTimezone, { expected: "an IANA timezone identifier" })
707
+ var ianaTimezone = Schema11.String.check(
708
+ Schema11.makeFilter(isIanaTimezone, { expected: "an IANA timezone identifier" })
815
709
  );
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" })
710
+ var uuidV7 = Schema11.String.check(Schema11.isUUID(UUID_V7_VERSION)).pipe(Schema11.brand("UuidV7"));
711
+ var generalUuid = Schema11.String.check(Schema11.isUUID()).pipe(Schema11.brand("GeneralUuid"));
712
+ var hex64 = Schema11.String.check(
713
+ Schema11.isPattern(HEX64_PATTERN, { expected: "a 64-character lowercase hex string" })
820
714
  );
821
- var apiUrl = Schema6.String.check(Schema6.makeFilter(isApiUrl, { expected: "an HTTP(S) URL" }));
715
+ var apiUrl = Schema11.String.check(Schema11.makeFilter(isApiUrl, { expected: "an HTTP(S) URL" }));
822
716
 
823
717
  // ../../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),
718
+ var AuthUserResponse = Schema12.Struct({
719
+ user: Schema12.Struct({
720
+ id: Schema12.String,
721
+ email: Schema12.String,
722
+ displayName: Schema12.NullOr(Schema12.String),
829
723
  createdAt: isoUtcString
830
724
  })
831
725
  });
832
- var LogoutResponse = Schema7.Struct({
833
- ok: Schema7.Boolean
726
+ var LogoutResponse = Schema12.Struct({
727
+ ok: Schema12.Boolean
834
728
  });
835
- var WebRpcContext = class extends Context3.Service()("@oopst/api/WebRpcContext") {
729
+ var WebRpcContext = class extends Context5.Service()("@oopst/api/WebRpcContext") {
836
730
  };
837
731
  var AuthRpcs = RpcGroup.make(
838
732
  Rpc.make("auth.logout", {
839
733
  success: LogoutResponse,
840
- error: Schema7.Union([DatabaseError, UnauthorizedError])
734
+ error: Schema12.Union([DatabaseError, UnauthorizedError])
841
735
  }),
842
736
  Rpc.make("auth.me", {
843
737
  success: AuthUserResponse,
@@ -846,42 +740,42 @@ var AuthRpcs = RpcGroup.make(
846
740
  Rpc.make("auth.updateProfile", {
847
741
  payload: updateProfileInput,
848
742
  success: AuthUserResponse,
849
- error: Schema7.Union([DatabaseError, UnauthorizedError])
743
+ error: Schema12.Union([DatabaseError, UnauthorizedError])
850
744
  })
851
745
  );
852
746
 
853
747
  // ../../packages/api/src/rpc/devices.ts
854
- import { Schema as Schema9 } from "effect";
748
+ import { Schema as Schema14 } from "effect";
855
749
  import { Rpc as Rpc2, RpcGroup as RpcGroup2 } from "effect/unstable/rpc";
856
750
 
857
751
  // ../../packages/api/src/schemas/devices.ts
858
- import { Schema as Schema8 } from "effect";
752
+ import { Schema as Schema13 } from "effect";
859
753
  var MIN_DEVICE_NAME_LENGTH = 1;
860
754
  var MAX_DEVICE_NAME_LENGTH = 80;
861
755
  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)
756
+ var deviceNameSchema = Schema13.String.check(
757
+ Schema13.isMinLength(MIN_DEVICE_NAME_LENGTH),
758
+ Schema13.isMaxLength(MAX_DEVICE_NAME_LENGTH)
865
759
  );
866
- var editableDeviceNameSchema = Schema8.Trim.check(
867
- Schema8.isMinLength(MIN_DEVICE_NAME_LENGTH),
868
- Schema8.isMaxLength(MAX_DEVICE_NAME_LENGTH)
760
+ var editableDeviceNameSchema = Schema13.Trim.check(
761
+ Schema13.isMinLength(MIN_DEVICE_NAME_LENGTH),
762
+ Schema13.isMaxLength(MAX_DEVICE_NAME_LENGTH)
869
763
  );
870
- var deviceIdSchema = Schema8.String.check(Schema8.isUUID(UUID_V7_VERSION2));
871
- var renameDeviceInput = Schema8.Struct({
764
+ var deviceIdSchema = Schema13.String.check(Schema13.isUUID(UUID_V7_VERSION2));
765
+ var renameDeviceInput = Schema13.Struct({
872
766
  id: deviceIdSchema,
873
767
  name: editableDeviceNameSchema
874
768
  });
875
- var deleteDeviceInput = Schema8.Struct({
769
+ var deleteDeviceInput = Schema13.Struct({
876
770
  id: deviceIdSchema
877
771
  });
878
- var renameDeviceOutput = Schema8.Struct({
772
+ var renameDeviceOutput = Schema13.Struct({
879
773
  id: deviceIdSchema,
880
- name: Schema8.String,
774
+ name: Schema13.String,
881
775
  lastSeen: isoUtcString
882
776
  });
883
- var deleteDeviceOutput = Schema8.Struct({
884
- ok: Schema8.Boolean
777
+ var deleteDeviceOutput = Schema13.Struct({
778
+ ok: Schema13.Boolean
885
779
  });
886
780
 
887
781
  // ../../packages/api/src/rpc/devices.ts
@@ -889,27 +783,27 @@ var DevicesRpcs = RpcGroup2.make(
889
783
  Rpc2.make("devices.rename", {
890
784
  payload: renameDeviceInput,
891
785
  success: renameDeviceOutput,
892
- error: Schema9.Union([ConflictError, DatabaseError, NotFoundError, UnauthorizedError])
786
+ error: Schema14.Union([ConflictError, DatabaseError, NotFoundError, UnauthorizedError])
893
787
  }),
894
788
  Rpc2.make("devices.delete", {
895
789
  payload: deleteDeviceInput,
896
790
  success: deleteDeviceOutput,
897
- error: Schema9.Union([DatabaseError, NotFoundError, UnauthorizedError])
791
+ error: Schema14.Union([DatabaseError, NotFoundError, UnauthorizedError])
898
792
  })
899
793
  );
900
794
 
901
795
  // ../../packages/api/src/rpc/stats.ts
902
- import { Schema as Schema11 } from "effect";
796
+ import { Schema as Schema16 } from "effect";
903
797
  import { Rpc as Rpc3, RpcGroup as RpcGroup3 } from "effect/unstable/rpc";
904
798
 
905
799
  // ../../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);
800
+ import { Effect as Effect4, Schema as Schema15 } from "effect";
801
+ var scopeEnum = Schema15.Literals(["all", "me"]);
802
+ var costStackByValueEnum = Schema15.Literals(["device", "harness", "model"]);
803
+ var costStackByEnum = Schema15.optional(costStackByValueEnum);
804
+ var usageDetailStackByEnum = Schema15.Literals(["device", "harness", "model"]);
805
+ var stackByValueEnum = Schema15.Literals(["device", "harness"]);
806
+ var stackByEnum = Schema15.optional(stackByValueEnum);
913
807
  var MAX_TIME_ZONE_LENGTH = 64;
914
808
  var DEFAULT_DAYS = 90;
915
809
  var MAX_DAYS = 365;
@@ -927,16 +821,16 @@ var MAX_LEADERBOARD_LIMIT = 50;
927
821
  var DEFAULT_LEADERBOARD_LIMIT = 10;
928
822
  var UUID_V7_VERSION3 = 7;
929
823
  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));
824
+ var timeZoneSchema = ianaTimezone.check(Schema15.isMaxLength(MAX_TIME_ZONE_LENGTH));
931
825
  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));
826
+ var daysSchema = safeInteger.check(Schema15.isBetween({ minimum: MIN_LEADERBOARD_LIMIT, maximum: MAX_DAYS })).pipe(Schema15.withDecodingDefaultTypeKey(Effect4.succeed(DEFAULT_DAYS)));
827
+ var leaderboardLimitSchema = safeInteger.check(Schema15.isBetween({ minimum: MIN_LEADERBOARD_LIMIT, maximum: MAX_LEADERBOARD_LIMIT })).pipe(Schema15.withDecodingDefaultTypeKey(Effect4.succeed(DEFAULT_LEADERBOARD_LIMIT)));
828
+ var defaultTimeZoneSchema = timeZoneSchema.pipe(Schema15.withDecodingDefaultTypeKey(Effect4.succeed("UTC")));
829
+ var uuidSchema = Schema15.String.check(Schema15.isUUID(UUID_V7_VERSION3));
936
830
  var rangeFields = {
937
831
  days: daysSchema,
938
- endDate: Schema10.optional(civilDateSchema),
939
- startDate: Schema10.optional(civilDateSchema)
832
+ endDate: Schema15.optional(civilDateSchema),
833
+ startDate: Schema15.optional(civilDateSchema)
940
834
  };
941
835
  function validateDateRange(input) {
942
836
  if (!(input.startDate || input.endDate)) {
@@ -976,216 +870,216 @@ function parseCivilDateUtcMs(date) {
976
870
  }
977
871
  return parsed.getTime();
978
872
  }
979
- var dailyCostInput = Schema10.Struct({
873
+ var dailyCostInput = Schema15.Struct({
980
874
  scope: scopeEnum,
981
875
  stackBy: costStackByEnum,
982
876
  ...rangeFields,
983
877
  timeZone: defaultTimeZoneSchema
984
- }).check(Schema10.makeFilter(validateDateRange));
985
- var dailyPointSchema = Schema10.Struct({
878
+ }).check(Schema15.makeFilter(validateDateRange));
879
+ var dailyPointSchema = Schema15.Struct({
986
880
  /** Local civil date in YYYY-MM-DD for the requested stats timezone. */
987
881
  date: civilDateSchema,
988
882
  /** null when stackBy is undefined. */
989
- seriesName: Schema10.NullOr(Schema10.String),
883
+ seriesName: Schema15.NullOr(Schema15.String),
990
884
  /** USD; 0 or positive. */
991
- usd: Schema10.Number
885
+ usd: Schema15.Number
992
886
  });
993
- var dailyCostOutput = Schema10.Array(dailyPointSchema);
994
- var dailySessionsInput = Schema10.Struct({
887
+ var dailyCostOutput = Schema15.Array(dailyPointSchema);
888
+ var dailySessionsInput = Schema15.Struct({
995
889
  scope: scopeEnum,
996
890
  stackBy: stackByEnum,
997
891
  ...rangeFields,
998
892
  timeZone: defaultTimeZoneSchema
999
- }).check(Schema10.makeFilter(validateDateRange));
1000
- var dailySessionsPointSchema = Schema10.Struct({
893
+ }).check(Schema15.makeFilter(validateDateRange));
894
+ var dailySessionsPointSchema = Schema15.Struct({
1001
895
  date: civilDateSchema,
1002
- seriesName: Schema10.NullOr(Schema10.String),
896
+ seriesName: Schema15.NullOr(Schema15.String),
1003
897
  sessions: nonNegativeSafeInteger
1004
898
  });
1005
- var dailySessionsOutput = Schema10.Array(dailySessionsPointSchema);
899
+ var dailySessionsOutput = Schema15.Array(dailySessionsPointSchema);
1006
900
  var weekdaySessionsInput = dailySessionsInput;
1007
- var weekdaySessionsPointSchema = Schema10.Struct({
901
+ var weekdaySessionsPointSchema = Schema15.Struct({
1008
902
  /** 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),
903
+ weekday: safeInteger.check(Schema15.isBetween({ minimum: MIN_ISO_WEEKDAY, maximum: MAX_ISO_WEEKDAY })),
904
+ seriesName: Schema15.NullOr(Schema15.String),
1011
905
  sessions: nonNegativeSafeInteger
1012
906
  });
1013
- var weekdaySessionsOutput = Schema10.Array(weekdaySessionsPointSchema);
907
+ var weekdaySessionsOutput = Schema15.Array(weekdaySessionsPointSchema);
1014
908
  var hourlySessionsInput = dailySessionsInput;
1015
- var hourlySessionsPointSchema = Schema10.Struct({
909
+ var hourlySessionsPointSchema = Schema15.Struct({
1016
910
  /** 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),
911
+ hour: safeInteger.check(Schema15.isBetween({ minimum: MIN_HOUR_OF_DAY, maximum: MAX_HOUR_OF_DAY })),
912
+ seriesName: Schema15.NullOr(Schema15.String),
1019
913
  sessions: nonNegativeSafeInteger
1020
914
  });
1021
- var hourlySessionsOutput = Schema10.Array(hourlySessionsPointSchema);
1022
- var summaryInput = Schema10.Struct({
915
+ var hourlySessionsOutput = Schema15.Array(hourlySessionsPointSchema);
916
+ var summaryInput = Schema15.Struct({
1023
917
  ...rangeFields,
1024
918
  scope: scopeEnum,
1025
919
  timeZone: defaultTimeZoneSchema
1026
- }).check(Schema10.makeFilter(validateDateRange));
1027
- var summaryOutput = Schema10.Struct({
1028
- rangeUsd: Schema10.Number.check(Schema10.isGreaterThanOrEqualTo(0)),
920
+ }).check(Schema15.makeFilter(validateDateRange));
921
+ var summaryOutput = Schema15.Struct({
922
+ rangeUsd: Schema15.Number.check(Schema15.isGreaterThanOrEqualTo(0)),
1029
923
  rangeSessions: nonNegativeSafeInteger,
1030
924
  rangeSubjectCount: nonNegativeSafeInteger,
1031
- totalUsd: Schema10.Number.check(Schema10.isGreaterThanOrEqualTo(0)),
925
+ totalUsd: Schema15.Number.check(Schema15.isGreaterThanOrEqualTo(0)),
1032
926
  totalSessions: nonNegativeSafeInteger
1033
927
  });
1034
- var deviceSummarySchema = Schema10.Struct({
928
+ var deviceSummarySchema = Schema15.Struct({
1035
929
  id: uuidSchema,
1036
- name: Schema10.String,
930
+ name: Schema15.String,
1037
931
  /** ISO-8601 UTC. */
1038
932
  lastSeen: isoUtcString
1039
933
  });
1040
- var devicesOutput = Schema10.Array(deviceSummarySchema);
1041
- var participantSyncSchema = Schema10.Struct({
934
+ var devicesOutput = Schema15.Array(deviceSummarySchema);
935
+ var participantSyncSchema = Schema15.Struct({
1042
936
  userId: uuidSchema,
1043
- userLabel: Schema10.String,
937
+ userLabel: Schema15.String,
1044
938
  /** ISO-8601 UTC, null when the participant has no synced devices yet. */
1045
- latestSyncAt: Schema10.NullOr(isoUtcString)
939
+ latestSyncAt: Schema15.NullOr(isoUtcString)
1046
940
  });
1047
- var participantsOutput = Schema10.Array(participantSyncSchema);
1048
- var leaderboardInput = Schema10.Struct({
941
+ var participantsOutput = Schema15.Array(participantSyncSchema);
942
+ var leaderboardInput = Schema15.Struct({
1049
943
  ...rangeFields,
1050
944
  limit: leaderboardLimitSchema,
1051
945
  timeZone: defaultTimeZoneSchema
1052
- }).check(Schema10.makeFilter(validateDateRange));
1053
- var leaderboardRowSchema = Schema10.Struct({
1054
- rank: safeInteger.check(Schema10.isGreaterThanOrEqualTo(1)),
946
+ }).check(Schema15.makeFilter(validateDateRange));
947
+ var leaderboardRowSchema = Schema15.Struct({
948
+ rank: safeInteger.check(Schema15.isGreaterThanOrEqualTo(1)),
1055
949
  userId: uuidSchema,
1056
- userLabel: Schema10.String,
1057
- usd: Schema10.Number.check(Schema10.isGreaterThanOrEqualTo(0)),
950
+ userLabel: Schema15.String,
951
+ usd: Schema15.Number.check(Schema15.isGreaterThanOrEqualTo(0)),
1058
952
  tokens: nonNegativeSafeInteger,
1059
953
  sessions: nonNegativeSafeInteger
1060
954
  });
1061
- var leaderboardOutput = Schema10.Array(leaderboardRowSchema);
955
+ var leaderboardOutput = Schema15.Array(leaderboardRowSchema);
1062
956
  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)),
957
+ var topDimensionRowSchema = Schema15.Struct({
958
+ rank: safeInteger.check(Schema15.isGreaterThanOrEqualTo(1)),
959
+ name: Schema15.String,
960
+ usd: Schema15.Number.check(Schema15.isGreaterThanOrEqualTo(0)),
1067
961
  tokens: nonNegativeSafeInteger,
1068
- sessions: Schema10.NullOr(nonNegativeSafeInteger)
962
+ sessions: Schema15.NullOr(nonNegativeSafeInteger)
1069
963
  });
1070
- var topDimensionsOutput = Schema10.Struct({
1071
- harnesses: Schema10.Array(topDimensionRowSchema),
1072
- models: Schema10.Array(topDimensionRowSchema)
964
+ var topDimensionsOutput = Schema15.Struct({
965
+ harnesses: Schema15.Array(topDimensionRowSchema),
966
+ models: Schema15.Array(topDimensionRowSchema)
1073
967
  });
1074
- var usageDetailsInput = Schema10.Struct({
968
+ var usageDetailsInput = Schema15.Struct({
1075
969
  ...rangeFields,
1076
970
  stackBy: usageDetailStackByEnum,
1077
971
  timeZone: defaultTimeZoneSchema
1078
- }).check(Schema10.makeFilter(validateDateRange));
1079
- var usageDetailsRowSchema = Schema10.Struct({
1080
- id: Schema10.String,
1081
- name: Schema10.String,
972
+ }).check(Schema15.makeFilter(validateDateRange));
973
+ var usageDetailsRowSchema = Schema15.Struct({
974
+ id: Schema15.String,
975
+ name: Schema15.String,
1082
976
  tokens: nonNegativeSafeInteger,
1083
- sessions: Schema10.NullOr(nonNegativeSafeInteger),
1084
- usd: Schema10.Number.check(Schema10.isGreaterThanOrEqualTo(0)),
1085
- totalUsd: Schema10.Number.check(Schema10.isGreaterThanOrEqualTo(0))
977
+ sessions: Schema15.NullOr(nonNegativeSafeInteger),
978
+ usd: Schema15.Number.check(Schema15.isGreaterThanOrEqualTo(0)),
979
+ totalUsd: Schema15.Number.check(Schema15.isGreaterThanOrEqualTo(0))
1086
980
  });
1087
- var usageDetailsOutput = Schema10.Array(usageDetailsRowSchema);
981
+ var usageDetailsOutput = Schema15.Array(usageDetailsRowSchema);
1088
982
 
1089
983
  // ../../packages/api/src/rpc/stats.ts
1090
984
  var StatsRpcs = RpcGroup3.make(
1091
985
  Rpc3.make("stats.devices", {
1092
986
  success: devicesOutput,
1093
- error: Schema11.Union([DatabaseError, UnauthorizedError])
987
+ error: Schema16.Union([DatabaseError, UnauthorizedError])
1094
988
  }),
1095
989
  Rpc3.make("stats.participants", {
1096
990
  success: participantsOutput,
1097
- error: Schema11.Union([DatabaseError, UnauthorizedError])
991
+ error: Schema16.Union([DatabaseError, UnauthorizedError])
1098
992
  }),
1099
993
  Rpc3.make("stats.leaderboard", {
1100
994
  payload: leaderboardInput,
1101
995
  success: leaderboardOutput,
1102
- error: Schema11.Union([DatabaseError, UnauthorizedError])
996
+ error: Schema16.Union([DatabaseError, UnauthorizedError])
1103
997
  }),
1104
998
  Rpc3.make("stats.topDimensions", {
1105
999
  payload: topDimensionsInput,
1106
1000
  success: topDimensionsOutput,
1107
- error: Schema11.Union([DatabaseError, UnauthorizedError])
1001
+ error: Schema16.Union([DatabaseError, UnauthorizedError])
1108
1002
  }),
1109
1003
  Rpc3.make("stats.summary", {
1110
1004
  payload: summaryInput,
1111
1005
  success: summaryOutput,
1112
- error: Schema11.Union([DatabaseError, UnauthorizedError])
1006
+ error: Schema16.Union([DatabaseError, UnauthorizedError])
1113
1007
  }),
1114
1008
  Rpc3.make("stats.dailyCost", {
1115
1009
  payload: dailyCostInput,
1116
1010
  success: dailyCostOutput,
1117
- error: Schema11.Union([DatabaseError, UnauthorizedError])
1011
+ error: Schema16.Union([DatabaseError, UnauthorizedError])
1118
1012
  }),
1119
1013
  Rpc3.make("stats.dailySessions", {
1120
1014
  payload: dailySessionsInput,
1121
1015
  success: dailySessionsOutput,
1122
- error: Schema11.Union([DatabaseError, UnauthorizedError])
1016
+ error: Schema16.Union([DatabaseError, UnauthorizedError])
1123
1017
  }),
1124
1018
  Rpc3.make("stats.weekdaySessions", {
1125
1019
  payload: weekdaySessionsInput,
1126
1020
  success: weekdaySessionsOutput,
1127
- error: Schema11.Union([DatabaseError, UnauthorizedError])
1021
+ error: Schema16.Union([DatabaseError, UnauthorizedError])
1128
1022
  }),
1129
1023
  Rpc3.make("stats.hourlySessions", {
1130
1024
  payload: hourlySessionsInput,
1131
1025
  success: hourlySessionsOutput,
1132
- error: Schema11.Union([DatabaseError, UnauthorizedError])
1026
+ error: Schema16.Union([DatabaseError, UnauthorizedError])
1133
1027
  }),
1134
1028
  Rpc3.make("stats.usageDetails", {
1135
1029
  payload: usageDetailsInput,
1136
1030
  success: usageDetailsOutput,
1137
- error: Schema11.Union([DatabaseError, UnauthorizedError])
1031
+ error: Schema16.Union([DatabaseError, UnauthorizedError])
1138
1032
  })
1139
1033
  );
1140
1034
 
1141
1035
  // ../../packages/api/src/rpc/tokens.ts
1142
- import { Schema as Schema13 } from "effect";
1036
+ import { Schema as Schema18 } from "effect";
1143
1037
  import { Rpc as Rpc4, RpcGroup as RpcGroup4 } from "effect/unstable/rpc";
1144
1038
 
1145
1039
  // ../../packages/api/src/schemas/tokens.ts
1146
- import { Schema as Schema12 } from "effect";
1040
+ import { Schema as Schema17 } from "effect";
1147
1041
  var MIN_TOKEN_NAME_LENGTH = 1;
1148
1042
  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))
1043
+ var createTokenInput = Schema17.Struct({
1044
+ name: Schema17.String.check(Schema17.isMinLength(MIN_TOKEN_NAME_LENGTH), Schema17.isMaxLength(MAX_TOKEN_NAME_LENGTH))
1151
1045
  });
1152
- var revokeTokenInput = Schema12.Struct({
1046
+ var revokeTokenInput = Schema17.Struct({
1153
1047
  id: uuidV7
1154
1048
  });
1155
- var tokenMetadata = Schema12.Struct({
1049
+ var tokenMetadata = Schema17.Struct({
1156
1050
  id: uuidV7,
1157
- name: Schema12.String,
1158
- prefix: Schema12.String,
1051
+ name: Schema17.String,
1052
+ prefix: Schema17.String,
1159
1053
  createdAt: isoUtcString,
1160
- lastUsedAt: Schema12.NullOr(isoUtcString),
1161
- revokedAt: Schema12.NullOr(isoUtcString)
1054
+ lastUsedAt: Schema17.NullOr(isoUtcString),
1055
+ revokedAt: Schema17.NullOr(isoUtcString)
1162
1056
  });
1163
- var listTokensOutput = Schema12.Struct({
1164
- tokens: Schema12.Array(tokenMetadata)
1057
+ var listTokensOutput = Schema17.Struct({
1058
+ tokens: Schema17.Array(tokenMetadata)
1165
1059
  });
1166
- var createTokenOutput = Schema12.Struct({
1167
- token: Schema12.String,
1060
+ var createTokenOutput = Schema17.Struct({
1061
+ token: Schema17.String,
1168
1062
  metadata: tokenMetadata
1169
1063
  });
1170
- var revokeTokenOutput = Schema12.Struct({
1171
- ok: Schema12.Boolean
1064
+ var revokeTokenOutput = Schema17.Struct({
1065
+ ok: Schema17.Boolean
1172
1066
  });
1173
1067
 
1174
1068
  // ../../packages/api/src/rpc/tokens.ts
1175
1069
  var TokensRpcs = RpcGroup4.make(
1176
1070
  Rpc4.make("tokens.list", {
1177
1071
  success: listTokensOutput,
1178
- error: Schema13.Union([DatabaseError, UnauthorizedError])
1072
+ error: Schema18.Union([DatabaseError, UnauthorizedError])
1179
1073
  }),
1180
1074
  Rpc4.make("tokens.create", {
1181
1075
  payload: createTokenInput,
1182
1076
  success: createTokenOutput,
1183
- error: Schema13.Union([DatabaseError, UnauthorizedError])
1077
+ error: Schema18.Union([DatabaseError, UnauthorizedError])
1184
1078
  }),
1185
1079
  Rpc4.make("tokens.revoke", {
1186
1080
  payload: revokeTokenInput,
1187
1081
  success: revokeTokenOutput,
1188
- error: Schema13.Union([DatabaseError, NotFoundError, UnauthorizedError])
1082
+ error: Schema18.Union([DatabaseError, NotFoundError, UnauthorizedError])
1189
1083
  })
1190
1084
  );
1191
1085
 
@@ -1193,13 +1087,15 @@ var TokensRpcs = RpcGroup4.make(
1193
1087
  var WebRpcs = AuthRpcs.merge(StatsRpcs).merge(TokensRpcs).merge(DevicesRpcs);
1194
1088
 
1195
1089
  // ../../packages/api/src/rpc/sync.ts
1196
- import { Schema as Schema15 } from "effect";
1090
+ import { Schema as Schema20 } from "effect";
1197
1091
  import { Rpc as Rpc5, RpcGroup as RpcGroup5 } from "effect/unstable/rpc";
1198
1092
 
1199
1093
  // ../../packages/api/src/schemas/sync.ts
1200
- import { Effect as Effect6, Schema as Schema14 } from "effect";
1094
+ import { Effect as Effect5, Schema as Schema19 } from "effect";
1201
1095
  var MIN_DEVICE_NAME_LENGTH2 = 1;
1202
1096
  var MAX_DEVICE_NAME_LENGTH2 = 80;
1097
+ var MIN_CLI_VERSION_LENGTH = 1;
1098
+ var MAX_CLI_VERSION_LENGTH = 100;
1203
1099
  var MIN_MODEL_LENGTH = 1;
1204
1100
  var MAX_MODEL_LENGTH = 200;
1205
1101
  var SESSION_KEY_LENGTH = 64;
@@ -1208,28 +1104,38 @@ var MAX_BUCKETS_PER_SYNC = 5e3;
1208
1104
  var MAX_SESSIONS_PER_SYNC = 5e3;
1209
1105
  var MAX_BUCKET_SESSION_COUNT = 1e3;
1210
1106
  var MAX_BUCKET_TOKEN_COUNT = null;
1107
+ var MAX_REPLACEMENT_BATCHES = 1e4;
1108
+ var MAX_REPLACEMENT_FACTS = 1e5;
1211
1109
  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)
1110
+ var MAX_REPLACEMENT_HARNESSES = HARNESSES.length;
1111
+ var harnessEnum = Schema19.Literals(HARNESSES);
1112
+ var deviceNameSchema2 = Schema19.String.check(
1113
+ Schema19.isMinLength(MIN_DEVICE_NAME_LENGTH2),
1114
+ Schema19.isMaxLength(MAX_DEVICE_NAME_LENGTH2)
1115
+ );
1116
+ var cliVersionSchema = Schema19.String.check(
1117
+ Schema19.isMinLength(MIN_CLI_VERSION_LENGTH),
1118
+ Schema19.isMaxLength(MAX_CLI_VERSION_LENGTH)
1119
+ );
1120
+ var legacyCompatibleCliVersionSchema = Schema19.NullOr(cliVersionSchema).pipe(
1121
+ Schema19.withDecodingDefaultTypeKey(Effect5.succeed(null))
1216
1122
  );
1217
- var getWatermarkInput = Schema14.Struct({
1123
+ var getWatermarkInput = Schema19.Struct({
1218
1124
  device: deviceNameSchema2
1219
1125
  });
1220
- var getWatermarkOutput = Schema14.Struct({
1221
- watermarkHourUtc: Schema14.NullOr(isoUtcString),
1222
- sessionKeySalt: hex64.check(Schema14.isLengthBetween(SESSION_KEY_SALT_LENGTH, SESSION_KEY_SALT_LENGTH))
1126
+ var getWatermarkOutput = Schema19.Struct({
1127
+ watermarkHourUtc: Schema19.NullOr(isoUtcString),
1128
+ sessionKeySalt: hex64.check(Schema19.isLengthBetween(SESSION_KEY_SALT_LENGTH, SESSION_KEY_SALT_LENGTH))
1223
1129
  });
1224
1130
  function boundedInteger(max) {
1225
1131
  if (max === null) {
1226
1132
  return nonNegativeSafeInteger;
1227
1133
  }
1228
- return nonNegativeSafeInteger.check(Schema14.isLessThanOrEqualTo(max));
1134
+ return nonNegativeSafeInteger.check(Schema19.isLessThanOrEqualTo(max));
1229
1135
  }
1230
- var bucketInput = Schema14.Struct({
1136
+ var bucketInput = Schema19.Struct({
1231
1137
  harness: harnessEnum,
1232
- model: Schema14.String.check(Schema14.isMinLength(MIN_MODEL_LENGTH), Schema14.isMaxLength(MAX_MODEL_LENGTH)),
1138
+ model: Schema19.String.check(Schema19.isMinLength(MIN_MODEL_LENGTH), Schema19.isMaxLength(MAX_MODEL_LENGTH)),
1233
1139
  hourUtc: isoUtcString,
1234
1140
  inputTokens: boundedInteger(MAX_BUCKET_TOKEN_COUNT),
1235
1141
  outputTokens: boundedInteger(MAX_BUCKET_TOKEN_COUNT),
@@ -1237,110 +1143,421 @@ var bucketInput = Schema14.Struct({
1237
1143
  cacheCreateTokens: boundedInteger(MAX_BUCKET_TOKEN_COUNT),
1238
1144
  sessionCount: boundedInteger(MAX_BUCKET_SESSION_COUNT)
1239
1145
  });
1240
- var sessionInput = Schema14.Struct({
1146
+ var sessionInput = Schema19.Struct({
1241
1147
  harness: harnessEnum,
1242
- sessionKey: hex64.check(Schema14.isLengthBetween(SESSION_KEY_LENGTH, SESSION_KEY_LENGTH)),
1148
+ sessionKey: hex64.check(Schema19.isLengthBetween(SESSION_KEY_LENGTH, SESSION_KEY_LENGTH)),
1243
1149
  startedAt: isoUtcString,
1244
1150
  lastSeenAt: isoUtcString
1245
1151
  });
1246
- var upsertBucketsInput = Schema14.Struct({
1152
+ var upsertBucketsInput = Schema19.Struct({
1247
1153
  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([])))
1154
+ cliVersion: legacyCompatibleCliVersionSchema,
1155
+ buckets: Schema19.Array(bucketInput).check(Schema19.isMaxLength(MAX_BUCKETS_PER_SYNC)),
1156
+ sessions: Schema19.Array(sessionInput).check(Schema19.isMaxLength(MAX_SESSIONS_PER_SYNC)).pipe(Schema19.withDecodingDefaultTypeKey(Effect5.succeed([])))
1250
1157
  });
1251
- var upsertBucketsOutput = Schema14.Struct({
1158
+ var upsertBucketsOutput = Schema19.Struct({
1252
1159
  upserted: boundedInteger(null),
1253
1160
  upsertedSessions: boundedInteger(null)
1254
1161
  });
1162
+ var replacementId = uuidV7;
1163
+ var createUsageReplacementInput = Schema19.Struct({
1164
+ device: deviceNameSchema2,
1165
+ cliVersion: legacyCompatibleCliVersionSchema,
1166
+ startHourUtc: isoUtcString,
1167
+ endHourUtc: isoUtcString,
1168
+ harnesses: Schema19.Array(harnessEnum).check(Schema19.isMinLength(1), Schema19.isMaxLength(MAX_REPLACEMENT_HARNESSES))
1169
+ });
1170
+ var createUsageReplacementOutput = Schema19.Struct({
1171
+ replacementId,
1172
+ startHourUtc: isoUtcString,
1173
+ endHourUtc: isoUtcString,
1174
+ harnesses: Schema19.Array(harnessEnum),
1175
+ expiresAt: isoUtcString
1176
+ });
1177
+ var stageUsageReplacementBatchInput = Schema19.Struct({
1178
+ device: deviceNameSchema2,
1179
+ replacementId,
1180
+ batchIndex: boundedInteger(MAX_REPLACEMENT_BATCHES),
1181
+ batchDigest: hex64,
1182
+ buckets: Schema19.Array(bucketInput).check(Schema19.isMaxLength(MAX_BUCKETS_PER_SYNC)),
1183
+ sessions: Schema19.Array(sessionInput).check(Schema19.isMaxLength(MAX_SESSIONS_PER_SYNC))
1184
+ });
1185
+ var stageUsageReplacementBatchOutput = Schema19.Struct({
1186
+ stagedBuckets: boundedInteger(MAX_BUCKETS_PER_SYNC),
1187
+ stagedSessions: boundedInteger(MAX_SESSIONS_PER_SYNC),
1188
+ alreadyStaged: Schema19.Boolean
1189
+ });
1190
+ var finalizeUsageReplacementInput = Schema19.Struct({
1191
+ device: deviceNameSchema2,
1192
+ replacementId,
1193
+ expectedBatchCount: boundedInteger(MAX_REPLACEMENT_BATCHES),
1194
+ expectedBucketCount: boundedInteger(MAX_REPLACEMENT_FACTS),
1195
+ expectedSessionCount: boundedInteger(MAX_REPLACEMENT_FACTS),
1196
+ digest: hex64
1197
+ });
1198
+ var finalizeUsageReplacementOutput = Schema19.Struct({
1199
+ replacementId,
1200
+ committedAt: isoUtcString,
1201
+ replacedBuckets: boundedInteger(MAX_REPLACEMENT_FACTS),
1202
+ replacedSessions: boundedInteger(MAX_REPLACEMENT_FACTS),
1203
+ digest: hex64
1204
+ });
1255
1205
 
1256
1206
  // ../../packages/api/src/rpc/sync.ts
1207
+ var replacementErrors = Schema20.Union([
1208
+ ConflictError,
1209
+ DatabaseError,
1210
+ NotFoundError,
1211
+ SyncQuotaExceededError,
1212
+ SyncTimestampRejectedError,
1213
+ UnauthorizedError,
1214
+ ValidationError
1215
+ ]);
1257
1216
  var SyncRpcs = RpcGroup5.make(
1258
1217
  Rpc5.make("sync.getWatermark", {
1259
1218
  payload: getWatermarkInput,
1260
1219
  success: getWatermarkOutput,
1261
- error: Schema15.Union([DatabaseError, UnauthorizedError])
1220
+ error: Schema20.Union([DatabaseError, UnauthorizedError])
1262
1221
  }),
1263
1222
  Rpc5.make("sync.upsertBuckets", {
1264
1223
  payload: upsertBucketsInput,
1265
1224
  success: upsertBucketsOutput,
1266
- error: Schema15.Union([DatabaseError, SyncQuotaExceededError, SyncTimestampRejectedError, UnauthorizedError])
1225
+ error: Schema20.Union([DatabaseError, SyncQuotaExceededError, SyncTimestampRejectedError, UnauthorizedError])
1226
+ }),
1227
+ Rpc5.make("sync.createUsageReplacement", {
1228
+ payload: createUsageReplacementInput,
1229
+ success: createUsageReplacementOutput,
1230
+ error: replacementErrors
1231
+ }),
1232
+ Rpc5.make("sync.stageUsageReplacementBatch", {
1233
+ payload: stageUsageReplacementBatchInput,
1234
+ success: stageUsageReplacementBatchOutput,
1235
+ error: replacementErrors
1236
+ }),
1237
+ Rpc5.make("sync.finalizeUsageReplacement", {
1238
+ payload: finalizeUsageReplacementInput,
1239
+ success: finalizeUsageReplacementOutput,
1240
+ error: replacementErrors
1267
1241
  })
1268
1242
  );
1269
1243
 
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
- )
1244
+ // ../../packages/api/src/sync-replacement-canonical.ts
1245
+ function normalizedIso(value) {
1246
+ return new Date(value).toISOString();
1247
+ }
1248
+ function canonicalReplacementLines(facts) {
1249
+ const lines = [
1250
+ ...facts.buckets.map(
1251
+ (bucket) => JSON.stringify([
1252
+ "bucket",
1253
+ bucket.harness,
1254
+ bucket.model,
1255
+ normalizedIso(bucket.hourUtc),
1256
+ bucket.inputTokens,
1257
+ bucket.outputTokens,
1258
+ bucket.cacheReadTokens,
1259
+ bucket.cacheCreateTokens,
1260
+ bucket.sessionCount
1261
+ ])
1262
+ ),
1263
+ ...facts.sessions.map(
1264
+ (session) => JSON.stringify([
1265
+ "session",
1266
+ session.harness,
1267
+ session.sessionKey,
1268
+ normalizedIso(session.startedAt),
1269
+ normalizedIso(session.lastSeenAt)
1270
+ ])
1316
1271
  )
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;
1272
+ ];
1273
+ return lines.sort();
1322
1274
  }
1323
- function stripTrailingSlash(url) {
1324
- return url.endsWith("/") ? url.slice(0, -1) : url;
1275
+ function canonicalReplacementPayload(facts) {
1276
+ return canonicalReplacementLines(facts).join("\n");
1325
1277
  }
1326
1278
 
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 = {
1279
+ // src/sync/run.ts
1280
+ import { Effect as Effect9, Stream as Stream4 } from "effect";
1281
+
1282
+ // src/sync/aggregate.ts
1283
+ import { createHmac } from "crypto";
1284
+ import { Effect as Effect6, Schema as Schema21, Stream as Stream2 } from "effect";
1285
+ var UsageAggregationErrorFields = {
1286
+ message: Schema21.String,
1287
+ cause: Schema21.optionalKey(Schema21.Defect())
1288
+ };
1289
+ var UsageAggregationError = class extends Schema21.TaggedErrorClass()(
1290
+ "UsageAggregationError",
1291
+ UsageAggregationErrorFields
1292
+ ) {
1293
+ };
1294
+ function toIso(hourUtc) {
1295
+ return hourUtc instanceof Date ? hourUtc.toISOString() : hourUtc;
1296
+ }
1297
+ function key(b) {
1298
+ return `${b.harness} ${b.model} ${toIso(b.hourUtc)}`;
1299
+ }
1300
+ function finalize(acc) {
1301
+ return {
1302
+ harness: acc.harness,
1303
+ model: acc.model,
1304
+ hourUtc: acc.hourUtc,
1305
+ inputTokens: acc.inputTokens,
1306
+ outputTokens: acc.outputTokens,
1307
+ cacheReadTokens: acc.cacheReadTokens,
1308
+ cacheCreateTokens: acc.cacheCreateTokens,
1309
+ sessionCount: acc.sessionIds.size
1310
+ };
1311
+ }
1312
+ var SESSION_KEY_SALT_PATTERN = /^[a-f0-9]{64}$/u;
1313
+ function createSessionKey(harness, localSessionId, sessionKeySalt) {
1314
+ if (!SESSION_KEY_SALT_PATTERN.test(sessionKeySalt)) {
1315
+ throw new Error("invalid session key salt");
1316
+ }
1317
+ return createHmac("sha256", Buffer.from(sessionKeySalt, "hex")).update("oopst/session-key/v1").update("\0").update(harness).update("\0").update(localSessionId).digest("hex");
1318
+ }
1319
+ function sessionMapKey(harness, sessionKey) {
1320
+ return `${harness} ${sessionKey}`;
1321
+ }
1322
+ function finalizeSession(acc) {
1323
+ return {
1324
+ harness: acc.harness,
1325
+ sessionKey: acc.sessionKey,
1326
+ startedAt: acc.startedAt.toISOString(),
1327
+ lastSeenAt: acc.lastSeenAt.toISOString()
1328
+ };
1329
+ }
1330
+ function addSessionAccumulator(sessionAccumulators, bucket, localSessionId, sessionKeySalt) {
1331
+ const sessionKey = createSessionKey(bucket.harness, localSessionId, sessionKeySalt);
1332
+ const sessionAccumulatorKey = sessionMapKey(bucket.harness, sessionKey);
1333
+ const existingSession = sessionAccumulators.get(sessionAccumulatorKey);
1334
+ if (existingSession) {
1335
+ if (bucket.sessionStartedAt.getTime() < existingSession.startedAt.getTime()) {
1336
+ existingSession.startedAt = bucket.sessionStartedAt;
1337
+ }
1338
+ if (bucket.sessionLastSeenAt.getTime() > existingSession.lastSeenAt.getTime()) {
1339
+ existingSession.lastSeenAt = bucket.sessionLastSeenAt;
1340
+ }
1341
+ return;
1342
+ }
1343
+ sessionAccumulators.set(sessionAccumulatorKey, {
1344
+ harness: bucket.harness,
1345
+ sessionKey,
1346
+ startedAt: bucket.sessionStartedAt,
1347
+ lastSeenAt: bucket.sessionLastSeenAt
1348
+ });
1349
+ }
1350
+ function createUsageAggregationState() {
1351
+ return {
1352
+ accumulators: /* @__PURE__ */ new Map(),
1353
+ sessionAccumulators: /* @__PURE__ */ new Map()
1354
+ };
1355
+ }
1356
+ function addBucketToAggregationState(state, bucket, sessionKeySalt, replacementRange) {
1357
+ const includeBucket = isReplacementBucketEligible(bucket, replacementRange);
1358
+ if (includeBucket) {
1359
+ const k = key(bucket);
1360
+ let acc = state.accumulators.get(k);
1361
+ if (!acc) {
1362
+ acc = {
1363
+ harness: bucket.harness,
1364
+ model: bucket.model,
1365
+ hourUtc: toIso(bucket.hourUtc),
1366
+ inputTokens: 0,
1367
+ outputTokens: 0,
1368
+ cacheReadTokens: 0,
1369
+ cacheCreateTokens: 0,
1370
+ sessionIds: /* @__PURE__ */ new Set()
1371
+ };
1372
+ state.accumulators.set(k, acc);
1373
+ }
1374
+ acc.inputTokens += bucket.inputTokens;
1375
+ acc.outputTokens += bucket.outputTokens;
1376
+ acc.cacheReadTokens += bucket.cacheReadTokens;
1377
+ acc.cacheCreateTokens += bucket.cacheCreateTokens;
1378
+ for (const id of bucket.sessionIds) {
1379
+ acc.sessionIds.add(id);
1380
+ }
1381
+ }
1382
+ if (isReplacementSessionEligible(bucket, replacementRange)) {
1383
+ for (const id of bucket.sessionIds) {
1384
+ addSessionAccumulator(state.sessionAccumulators, bucket, id, sessionKeySalt);
1385
+ }
1386
+ }
1387
+ return state;
1388
+ }
1389
+ function isReplacementBucketEligible(bucket, range) {
1390
+ if (!range) {
1391
+ return true;
1392
+ }
1393
+ const hourMs = bucket.hourUtc.getTime();
1394
+ return range.harnesses.has(bucket.harness) && hourMs >= range.startHourUtc.getTime() && hourMs < range.endHourUtc.getTime();
1395
+ }
1396
+ function isReplacementSessionEligible(bucket, range) {
1397
+ if (!range) {
1398
+ return true;
1399
+ }
1400
+ return range.harnesses.has(bucket.harness) && bucket.sessionStartedAt.getTime() < range.endHourUtc.getTime() && bucket.sessionLastSeenAt.getTime() >= range.startHourUtc.getTime();
1401
+ }
1402
+ function addBucketToAggregationStateEffect(state, bucket, sessionKeySalt, replacementRange) {
1403
+ return Effect6.try({
1404
+ try: () => addBucketToAggregationState(state, bucket, sessionKeySalt, replacementRange),
1405
+ catch: toUsageAggregationError
1406
+ });
1407
+ }
1408
+ function finalizeUsageBatches(state, batchSize) {
1409
+ const finalized = [];
1410
+ for (const acc of state.accumulators.values()) {
1411
+ finalized.push(finalize(acc));
1412
+ }
1413
+ const finalizedSessions = [];
1414
+ for (const acc of state.sessionAccumulators.values()) {
1415
+ finalizedSessions.push(finalizeSession(acc));
1416
+ }
1417
+ const batches = [];
1418
+ const maxLength = Math.max(finalized.length, finalizedSessions.length);
1419
+ for (let i = 0; i < maxLength; i += batchSize) {
1420
+ batches.push({
1421
+ buckets: finalized.slice(i, i + batchSize),
1422
+ sessions: finalizedSessions.slice(i, i + batchSize)
1423
+ });
1424
+ }
1425
+ return batches;
1426
+ }
1427
+ function toUsageAggregationError(cause) {
1428
+ return new UsageAggregationError({
1429
+ message: `usage aggregation failed: ${errorMessage(cause)}`,
1430
+ cause
1431
+ });
1432
+ }
1433
+ function errorMessage(cause) {
1434
+ return cause instanceof Error ? cause.message : "unknown aggregation error";
1435
+ }
1436
+ function aggregateUsageStream(source, options) {
1437
+ return Stream2.fromIterableEffect(
1438
+ source.pipe(
1439
+ Stream2.runFoldEffect(
1440
+ () => createUsageAggregationState(),
1441
+ (state, bucket) => addBucketToAggregationStateEffect(state, bucket, options.sessionKeySalt, options.replacementRange)
1442
+ ),
1443
+ Effect6.map((state) => finalizeUsageBatches(state, options.batchSize))
1444
+ )
1445
+ );
1446
+ }
1447
+
1448
+ // src/sync/client.ts
1449
+ import { Context as Context6, Effect as Effect7, Layer as Layer3 } from "effect";
1450
+ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
1451
+ import { RpcClient, RpcSerialization } from "effect/unstable/rpc";
1452
+ var AUTHORIZATION_HEADER = "authorization";
1453
+ var SYNC_RPC_PATH = "/rpc/sync";
1454
+ var SyncRpcClient = class extends Context6.Service()("@oopst/cli/SyncRpcClient") {
1455
+ };
1456
+ function syncRpcClientFromPromiseClient(client) {
1457
+ const getWatermark = Effect7.fn("SyncRpcClient.fromPromise.getWatermark")(
1458
+ (input) => Effect7.tryPromise({
1459
+ try: () => client.sync.getWatermark.mutate(input),
1460
+ catch: (err) => err
1461
+ })
1462
+ );
1463
+ const upsertBuckets = Effect7.fn("SyncRpcClient.fromPromise.upsertBuckets")(
1464
+ (input) => Effect7.tryPromise({
1465
+ try: () => client.sync.upsertBuckets.mutate(input),
1466
+ catch: (err) => err
1467
+ })
1468
+ );
1469
+ const createUsageReplacement = Effect7.fn("SyncRpcClient.fromPromise.createUsageReplacement")(
1470
+ (input) => Effect7.tryPromise({
1471
+ try: () => client.sync.createUsageReplacement.mutate(input),
1472
+ catch: (err) => err
1473
+ })
1474
+ );
1475
+ const stageUsageReplacementBatch = Effect7.fn("SyncRpcClient.fromPromise.stageUsageReplacementBatch")(
1476
+ (input) => Effect7.tryPromise({
1477
+ try: () => client.sync.stageUsageReplacementBatch.mutate(input),
1478
+ catch: (err) => err
1479
+ })
1480
+ );
1481
+ const finalizeUsageReplacement = Effect7.fn("SyncRpcClient.fromPromise.finalizeUsageReplacement")(
1482
+ (input) => Effect7.tryPromise({
1483
+ try: () => client.sync.finalizeUsageReplacement.mutate(input),
1484
+ catch: (err) => err
1485
+ })
1486
+ );
1487
+ return SyncRpcClient.of({
1488
+ createUsageReplacement,
1489
+ finalizeUsageReplacement,
1490
+ getWatermark,
1491
+ stageUsageReplacementBatch,
1492
+ upsertBuckets
1493
+ });
1494
+ }
1495
+ function makeSyncRpcClientLayer(options) {
1496
+ return Layer3.effect(
1497
+ SyncRpcClient,
1498
+ Effect7.gen(function* () {
1499
+ const client = yield* RpcClient.make(SyncRpcs);
1500
+ const getWatermark = Effect7.fn("SyncRpcClient.getWatermark")(
1501
+ (input) => client["sync.getWatermark"](input)
1502
+ );
1503
+ const upsertBuckets = Effect7.fn("SyncRpcClient.upsertBuckets")(
1504
+ (input) => client["sync.upsertBuckets"](input)
1505
+ );
1506
+ const createUsageReplacement = Effect7.fn("SyncRpcClient.createUsageReplacement")(
1507
+ (input) => client["sync.createUsageReplacement"](input)
1508
+ );
1509
+ const stageUsageReplacementBatch = Effect7.fn("SyncRpcClient.stageUsageReplacementBatch")(
1510
+ (input) => client["sync.stageUsageReplacementBatch"](input)
1511
+ );
1512
+ const finalizeUsageReplacement = Effect7.fn("SyncRpcClient.finalizeUsageReplacement")(
1513
+ (input) => client["sync.finalizeUsageReplacement"](input)
1514
+ );
1515
+ return SyncRpcClient.of({
1516
+ createUsageReplacement,
1517
+ finalizeUsageReplacement,
1518
+ getWatermark,
1519
+ stageUsageReplacementBatch,
1520
+ upsertBuckets
1521
+ });
1522
+ })
1523
+ ).pipe(Layer3.provide(makeClientLive(options)));
1524
+ }
1525
+ function makeClientLive(options) {
1526
+ let clientLive = RpcClient.layerProtocolHttp({
1527
+ url: stripTrailingSlash(options.serverUrl),
1528
+ transformClient: HttpClient.mapRequest(
1529
+ (request) => request.pipe(
1530
+ HttpClientRequest.appendUrl(SYNC_RPC_PATH),
1531
+ HttpClientRequest.setHeader(AUTHORIZATION_HEADER, `Bearer ${options.token}`)
1532
+ )
1533
+ )
1534
+ }).pipe(Layer3.provide(RpcSerialization.layerJson), Layer3.provide(FetchHttpClient.layer));
1535
+ if (options.fetch) {
1536
+ clientLive = clientLive.pipe(Layer3.provide(Layer3.succeed(FetchHttpClient.Fetch, options.fetch)));
1537
+ }
1538
+ return clientLive;
1539
+ }
1540
+ function stripTrailingSlash(url) {
1541
+ return url.endsWith("/") ? url.slice(0, -1) : url;
1542
+ }
1543
+
1544
+ // src/sync/errors.ts
1545
+ import { Schema as Schema22 } from "effect";
1546
+ var SUCCESS_EXIT_CODE = 0;
1547
+ var ZERO_BUCKETS_EXIT_CODE = 1;
1548
+ var UNAUTHORIZED_EXIT_CODE = 2;
1549
+ var NETWORK_ERROR_EXIT_CODE = 3;
1550
+ var INVALID_ARGUMENTS_EXIT_CODE = 4;
1551
+ var GENERAL_FAILURE_EXIT_CODE = 5;
1552
+ var EXIT_CODES = [
1553
+ SUCCESS_EXIT_CODE,
1554
+ ZERO_BUCKETS_EXIT_CODE,
1555
+ UNAUTHORIZED_EXIT_CODE,
1556
+ NETWORK_ERROR_EXIT_CODE,
1557
+ INVALID_ARGUMENTS_EXIT_CODE,
1558
+ GENERAL_FAILURE_EXIT_CODE
1559
+ ];
1560
+ var EXIT = {
1344
1561
  success: SUCCESS_EXIT_CODE,
1345
1562
  zeroBucketsButSourcesExist: ZERO_BUCKETS_EXIT_CODE,
1346
1563
  unauthorized: UNAUTHORIZED_EXIT_CODE,
@@ -1348,10 +1565,10 @@ var EXIT = {
1348
1565
  invalidArguments: INVALID_ARGUMENTS_EXIT_CODE,
1349
1566
  generalFailure: GENERAL_FAILURE_EXIT_CODE
1350
1567
  };
1351
- var SyncError = class extends Schema16.TaggedErrorClass()("SyncError", {
1352
- message: Schema16.String,
1353
- exitCode: Schema16.Literals(EXIT_CODES),
1354
- cause: Schema16.optionalKey(Schema16.Defect())
1568
+ var SyncError = class extends Schema22.TaggedErrorClass()("SyncError", {
1569
+ message: Schema22.String,
1570
+ exitCode: Schema22.Literals(EXIT_CODES),
1571
+ cause: Schema22.optionalKey(Schema22.Defect())
1355
1572
  }) {
1356
1573
  constructor(exitCode, message, options = {}) {
1357
1574
  super(
@@ -1384,24 +1601,25 @@ function isNetworkError(err) {
1384
1601
  if (code && NETWORK_ERROR_CODES.includes(code)) {
1385
1602
  return true;
1386
1603
  }
1387
- const message = err.message ?? "";
1604
+ const candidateMessage = err.message;
1605
+ const message = typeof candidateMessage === "string" ? candidateMessage : "";
1388
1606
  return NETWORK_ERROR_PATTERN.test(message);
1389
1607
  }
1390
1608
 
1391
1609
  // src/sync/parser-collection.ts
1392
- import { Context as Context5, Effect as Effect8, Layer as Layer4, Schema as Schema17, Stream as Stream3 } from "effect";
1610
+ import { Context as Context7, Effect as Effect8, Layer as Layer4, Schema as Schema23, Stream as Stream3 } from "effect";
1393
1611
  var SCAN_PROGRESS_SESSION_STEP = 500;
1394
1612
  var ParserCollectionErrorFields = {
1395
- message: Schema17.String,
1396
- source: Schema17.String,
1397
- cause: Schema17.optionalKey(Schema17.Defect())
1613
+ message: Schema23.String,
1614
+ source: Schema23.String,
1615
+ cause: Schema23.optionalKey(Schema23.Defect())
1398
1616
  };
1399
- var ParserCollectionError = class extends Schema17.TaggedErrorClass()(
1617
+ var ParserCollectionError = class extends Schema23.TaggedErrorClass()(
1400
1618
  "ParserCollectionError",
1401
1619
  ParserCollectionErrorFields
1402
1620
  ) {
1403
1621
  };
1404
- var ParserCollection = class extends Context5.Service()(
1622
+ var ParserCollection = class extends Context7.Service()(
1405
1623
  "@oopst/cli/ParserCollection"
1406
1624
  ) {
1407
1625
  };
@@ -1524,6 +1742,7 @@ function runSyncEffect(options) {
1524
1742
  function runSyncProgram(options) {
1525
1743
  return Effect9.gen(function* () {
1526
1744
  yield* validateRunSyncOptions(options);
1745
+ const replacement = yield* validateUsageReplacement(options.replacement, options.harness);
1527
1746
  const client = yield* SyncRpcClient;
1528
1747
  const parserCollection = yield* ParserCollection;
1529
1748
  const color = options.color === true;
@@ -1532,46 +1751,100 @@ function runSyncProgram(options) {
1532
1751
  return watermarkResult.result;
1533
1752
  }
1534
1753
  const { sessionKeySalt, watermark } = watermarkResult.value;
1535
- const cutoff = new Date((watermark?.getTime() ?? 0) - options.lookbackDays * MS_PER_DAY2);
1754
+ const cutoff = replacement ? /* @__PURE__ */ new Date(0) : new Date((watermark?.getTime() ?? 0) - options.lookbackDays * MS_PER_DAY2);
1536
1755
  if (options.verbose) {
1537
1756
  yield* logInfo2(options.logger, `watermark=${watermark?.toISOString() ?? "null"} cutoff=${cutoff.toISOString()}`);
1538
1757
  }
1539
1758
  const parsers = yield* Effect9.sync(() => options.parserFactory({ cutoff, device: options.device }));
1540
- const sources = selectSources(options.harness, parsers);
1759
+ const sources = selectSources(options.harness, parsers, replacement?.harnesses);
1541
1760
  const parserStream = yield* parserCollection.stream({
1542
1761
  sources,
1543
1762
  logger: options.logger,
1544
1763
  color
1545
1764
  });
1546
- const usageBatches = usageBatchStream(parserStream, sessionKeySalt);
1765
+ const usageBatches = usageBatchStream(parserStream, sessionKeySalt, replacement?.aggregationRange);
1766
+ if (replacement) {
1767
+ return yield* runReplacementUsage({
1768
+ options,
1769
+ client,
1770
+ color,
1771
+ replacement,
1772
+ usageBatches
1773
+ });
1774
+ }
1775
+ return yield* runOrdinaryUsage(options, client, color, usageBatches);
1776
+ });
1777
+ }
1778
+ function runReplacementUsage(input) {
1779
+ return Effect9.gen(function* () {
1780
+ const { client, color, options, replacement, usageBatches } = input;
1781
+ const batchesResult = yield* recoverSyncFailure(usageBatches.pipe(Stream4.runCollect), options.logger);
1782
+ if (batchesResult._tag === "SyncFailure") {
1783
+ return batchesResult.result;
1784
+ }
1785
+ const batches = batchesResult.value;
1786
+ const plan = summarizeUsageBatchArray(batches);
1547
1787
  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
1788
  yield* logInfo2(
1558
1789
  options.logger,
1559
- phase2(`Aggregated usage by hour and model: ${usageRecordText(plan2.bucketCount)} ready.`, color)
1790
+ `${dryRunPrefix(color)} atomic replacement would stage ${usageRecordText(plan.bucketCount)} and ${localSessionText2(plan.sessionCount)} in ${uploadRequestText(plan.batchCount)}.`
1560
1791
  );
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
1792
  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
- };
1793
+ return resultFromPlan(plan);
1570
1794
  }
1795
+ const uploadResult = yield* recoverSyncFailure(
1796
+ uploadUsageReplacement({
1797
+ batches,
1798
+ client,
1799
+ cliVersion: options.cliVersion,
1800
+ color,
1801
+ device: options.device,
1802
+ logger: options.logger,
1803
+ replacement
1804
+ }),
1805
+ options.logger
1806
+ );
1807
+ if (uploadResult._tag === "SyncFailure") {
1808
+ return uploadResult.result;
1809
+ }
1810
+ yield* logInfo2(
1811
+ options.logger,
1812
+ `${successPrefix(color)} atomically replaced ${usageRecordText(plan.bucketCount)} and ${localSessionText2(plan.sessionCount)}.`
1813
+ );
1814
+ return uploadResult.value;
1815
+ });
1816
+ }
1817
+ function runOrdinaryUsage(options, client, color, usageBatches) {
1818
+ return options.dryRun ? runOrdinaryDryRun(options, color, usageBatches) : uploadOrdinaryUsage(options, client, color, usageBatches);
1819
+ }
1820
+ function runOrdinaryDryRun(options, color, usageBatches) {
1821
+ return Effect9.gen(function* () {
1822
+ const planResult = yield* recoverSyncFailure(summarizeUsageBatchStream(usageBatches), options.logger);
1823
+ if (planResult._tag === "SyncFailure") {
1824
+ return planResult.result;
1825
+ }
1826
+ const plan = planResult.value;
1827
+ const earlyResult = yield* finishDryRun(options, plan, color);
1828
+ if (earlyResult) {
1829
+ return earlyResult;
1830
+ }
1831
+ yield* logInfo2(
1832
+ options.logger,
1833
+ phase2(`Aggregated usage by hour and model: ${usageRecordText(plan.bucketCount)} ready.`, color)
1834
+ );
1835
+ const summary = `${dryRunPrefix(color)} ${usageRecordText(plan.bucketCount)} from ${localSessionText2(plan.sessionCount)} would be uploaded in ${uploadRequestText(plan.batchCount)}.`;
1836
+ yield* logInfo2(options.logger, summary);
1837
+ yield* logInfo2(options.logger, muted("No usage data was uploaded.", color));
1838
+ return resultFromPlan(plan);
1839
+ });
1840
+ }
1841
+ function uploadOrdinaryUsage(options, client, color, usageBatches) {
1842
+ return Effect9.gen(function* () {
1571
1843
  const uploadResult = yield* recoverSyncFailure(
1572
1844
  uploadUsageBatchStream({
1573
1845
  batches: usageBatches,
1574
1846
  client,
1847
+ cliVersion: options.cliVersion,
1575
1848
  color,
1576
1849
  device: options.device,
1577
1850
  logger: options.logger
@@ -1597,8 +1870,10 @@ function runSyncProgram(options) {
1597
1870
  return emptySuccessResult();
1598
1871
  }
1599
1872
  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);
1873
+ yield* logInfo2(
1874
+ options.logger,
1875
+ `${successPrefix(color)} uploaded ${usageRecordText(totals.bucketsUploaded)} from ${localSessionText2(totals.sessionsUploaded)}${verboseUploadSuffix}.`
1876
+ );
1602
1877
  return {
1603
1878
  exitCode: EXIT.success,
1604
1879
  batchesUploaded: totals.batchesUploaded,
@@ -1607,6 +1882,14 @@ function runSyncProgram(options) {
1607
1882
  };
1608
1883
  });
1609
1884
  }
1885
+ function resultFromPlan(plan) {
1886
+ return {
1887
+ exitCode: EXIT.success,
1888
+ batchesUploaded: plan.batchCount,
1889
+ bucketsUploaded: plan.bucketCount,
1890
+ sessionsUploaded: plan.sessionCount
1891
+ };
1892
+ }
1610
1893
  function validateRunSyncOptions(options) {
1611
1894
  return Effect9.gen(function* () {
1612
1895
  if (!options.token) {
@@ -1617,6 +1900,45 @@ function validateRunSyncOptions(options) {
1617
1900
  }
1618
1901
  });
1619
1902
  }
1903
+ function validateUsageReplacement(replacement, harness) {
1904
+ if (!replacement) {
1905
+ return Effect9.succeed(void 0);
1906
+ }
1907
+ return Effect9.try({
1908
+ try: () => {
1909
+ if (harness !== void 0) {
1910
+ throw new SyncError(EXIT.invalidArguments, "--harness cannot be combined with replacement harnesses");
1911
+ }
1912
+ const start = parseUtcHour("replacement start", replacement.startHourUtc);
1913
+ const end = parseUtcHour("replacement end", replacement.endHourUtc);
1914
+ if (end.getTime() <= start.getTime()) {
1915
+ throw new SyncError(EXIT.invalidArguments, "replacement end must be later than replacement start");
1916
+ }
1917
+ const harnesses = [...new Set(replacement.harnesses)].sort();
1918
+ if (harnesses.length === 0 || harnesses.length !== replacement.harnesses.length) {
1919
+ throw new SyncError(EXIT.invalidArguments, "replacement harnesses must be a non-empty set without duplicates");
1920
+ }
1921
+ return {
1922
+ startHourUtc: start.toISOString(),
1923
+ endHourUtc: end.toISOString(),
1924
+ harnesses,
1925
+ aggregationRange: {
1926
+ startHourUtc: start,
1927
+ endHourUtc: end,
1928
+ harnesses: new Set(harnesses)
1929
+ }
1930
+ };
1931
+ },
1932
+ catch: (error) => error instanceof SyncError ? error : new SyncError(EXIT.invalidArguments, "replacement bounds must be exact UTC hours")
1933
+ });
1934
+ }
1935
+ function parseUtcHour(field, value) {
1936
+ const parsed = new Date(value);
1937
+ if (Number.isNaN(parsed.getTime()) || parsed.toISOString() !== value || parsed.getUTCMinutes() !== 0 || parsed.getUTCSeconds() !== 0 || parsed.getUTCMilliseconds() !== 0) {
1938
+ throw new SyncError(EXIT.invalidArguments, `${field} must be an ISO UTC timestamp aligned to an hour`);
1939
+ }
1940
+ return parsed;
1941
+ }
1620
1942
  function fetchWatermark(options, client, color) {
1621
1943
  return Effect9.gen(function* () {
1622
1944
  yield* logInfo2(options.logger, phase2("Checking server sync point...", color));
@@ -1627,29 +1949,24 @@ function fetchWatermark(options, client, color) {
1627
1949
  };
1628
1950
  });
1629
1951
  }
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;
1952
+ function selectSources(harness, parsers, replacementHarnesses) {
1953
+ const replacementSet = replacementHarnesses ? new Set(replacementHarnesses) : void 0;
1954
+ const candidates = [
1955
+ { harness: "codex", label: "Codex", source: parsers.codex },
1956
+ { harness: "claude_code", label: "Claude Code", source: parsers.claudeCode },
1957
+ { harness: "pi", label: "Pi", source: parsers.pi },
1958
+ { harness: "hermes", label: "Hermes", source: parsers.hermes },
1959
+ { harness: "opencode", label: "OpenCode", source: parsers.opencode }
1960
+ ];
1961
+ return candidates.filter(
1962
+ (candidate) => replacementSet ? replacementSet.has(candidate.harness) : harness === void 0 || harness === candidate.harness
1963
+ ).map(({ label, source }) => ({ label, source }));
1648
1964
  }
1649
- function usageBatchStream(source, sessionKeySalt) {
1965
+ function usageBatchStream(source, sessionKeySalt, replacementRange) {
1650
1966
  return aggregateUsageStream(source, {
1651
1967
  batchSize: SYNC_BATCH_SIZE,
1652
- sessionKeySalt
1968
+ sessionKeySalt,
1969
+ replacementRange
1653
1970
  }).pipe(Stream4.filter((batch) => batch.buckets.length > 0 || batch.sessions.length > 0));
1654
1971
  }
1655
1972
  var summarizeUsageBatchStream = Effect9.fn("RunSync.summarizeUsageBatchStream")(
@@ -1723,6 +2040,7 @@ var uploadUsageBatchStream = Effect9.fn("RunSync.uploadUsageBatchStream")(
1723
2040
  );
1724
2041
  const result = yield* options.client.upsertBuckets({
1725
2042
  device: options.device,
2043
+ cliVersion: options.cliVersion,
1726
2044
  buckets: batch.buckets,
1727
2045
  sessions: batch.sessions
1728
2046
  });
@@ -1738,6 +2056,52 @@ var uploadUsageBatchStream = Effect9.fn("RunSync.uploadUsageBatchStream")(
1738
2056
  return { plan, totals };
1739
2057
  })
1740
2058
  );
2059
+ var uploadUsageReplacement = Effect9.fn("RunSync.uploadUsageReplacement")(
2060
+ (options) => Effect9.gen(function* () {
2061
+ yield* logInfo2(options.logger, phase2("Creating atomic replacement scope...", options.color));
2062
+ const created = yield* options.client.createUsageReplacement({
2063
+ device: options.device,
2064
+ cliVersion: options.cliVersion,
2065
+ startHourUtc: options.replacement.startHourUtc,
2066
+ endHourUtc: options.replacement.endHourUtc,
2067
+ harnesses: options.replacement.harnesses
2068
+ });
2069
+ const buckets = options.batches.flatMap((batch) => batch.buckets);
2070
+ const sessions = options.batches.flatMap((batch) => batch.sessions);
2071
+ for (const [batchIndex, batch] of options.batches.entries()) {
2072
+ yield* logInfo2(
2073
+ options.logger,
2074
+ phase2(`Staging corrected usage... ${batchIndex + 1}/${options.batches.length}`, options.color)
2075
+ );
2076
+ yield* options.client.stageUsageReplacementBatch({
2077
+ device: options.device,
2078
+ replacementId: created.replacementId,
2079
+ batchIndex,
2080
+ batchDigest: usageReplacementDigest(batch),
2081
+ buckets: batch.buckets,
2082
+ sessions: batch.sessions
2083
+ });
2084
+ }
2085
+ yield* logInfo2(options.logger, phase2("Finalizing corrected usage atomically...", options.color));
2086
+ yield* options.client.finalizeUsageReplacement({
2087
+ device: options.device,
2088
+ replacementId: created.replacementId,
2089
+ expectedBatchCount: options.batches.length,
2090
+ expectedBucketCount: buckets.length,
2091
+ expectedSessionCount: sessions.length,
2092
+ digest: usageReplacementDigest({ buckets, sessions })
2093
+ });
2094
+ return {
2095
+ exitCode: EXIT.success,
2096
+ batchesUploaded: options.batches.length,
2097
+ bucketsUploaded: buckets.length,
2098
+ sessionsUploaded: sessions.length
2099
+ };
2100
+ })
2101
+ );
2102
+ function usageReplacementDigest(facts) {
2103
+ return createHash("sha256").update(canonicalReplacementPayload(facts)).digest("hex");
2104
+ }
1741
2105
  function recoverSyncFailure(effect, logger) {
1742
2106
  return Effect9.gen(function* () {
1743
2107
  return syncStepSuccess(yield* effect);
@@ -1812,7 +2176,7 @@ function uploadRequestText(value) {
1812
2176
  }
1813
2177
 
1814
2178
  // src/services/sync-program.ts
1815
- var SyncProgram = class extends Context6.Service()("@oopst/cli/SyncProgram") {
2179
+ var SyncProgram = class extends Context8.Service()("@oopst/cli/SyncProgram") {
1816
2180
  };
1817
2181
  var SyncProgramLive = Layer5.effect(
1818
2182
  SyncProgram,
@@ -1833,49 +2197,60 @@ function runSyncProgram2(options) {
1833
2197
  import { dirname, join as join3 } from "path";
1834
2198
  import { createInterface } from "readline/promises";
1835
2199
  import { NodeCrypto, NodeFileSystem as NodeFileSystem3 } from "@effect/platform-node";
1836
- import { Context as Context7, Crypto, Effect as Effect11, FileSystem as FileSystem4, Layer as Layer6, Schema as Schema18 } from "effect";
1837
- var CRON_START_MARKER = "# OopsT sync start";
1838
- var CRON_END_MARKER = "# OopsT sync end";
1839
- var DEFAULT_CRON_PATHS = ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"];
1840
- var DEFAULT_PACKAGE_NAME = "@cookielab.io/oopst";
1841
- var CONFIG_FILE_MODE = 384;
1842
- var LINE_SPLIT_PATTERN = /\r?\n/u;
1843
- var SetupError = class extends Schema18.TaggedErrorClass()("SetupError", {
1844
- message: Schema18.String,
1845
- exitCode: Schema18.Number,
1846
- cause: Schema18.optionalKey(Schema18.Defect())
1847
- }) {
1848
- constructor(message, options = {}) {
1849
- const exitCode = options.exitCode ?? EXIT.generalFailure;
1850
- super(
1851
- options.cause === void 0 ? { message, exitCode } : { message, exitCode, cause: options.cause }
1852
- );
1853
- }
1854
- };
1855
- var SetupPromptService = class extends Context7.Service()(
1856
- "@oopst/cli/SetupPromptService"
2200
+ import { Crypto, Effect as Effect11, FileSystem as FileSystem4, Layer as Layer6, Schema as Schema24 } from "effect";
2201
+
2202
+ // src/services/setup-cron-services.ts
2203
+ import { Context as Context9 } from "effect";
2204
+ var SetupCronCommandService = class extends Context9.Service()(
2205
+ "@oopst/cli/SetupCronCommandService"
1857
2206
  ) {
1858
2207
  };
1859
- var SetupConfigFileService = class extends Context7.Service()(
1860
- "@oopst/cli/SetupConfigFileService"
2208
+ var SetupRandomService = class extends Context9.Service()(
2209
+ "@oopst/cli/SetupRandomService"
1861
2210
  ) {
1862
2211
  };
1863
- var SetupCronCommandService = class extends Context7.Service()(
1864
- "@oopst/cli/SetupCronCommandService"
2212
+
2213
+ // src/services/setup-prompt-services.ts
2214
+ import { Context as Context10 } from "effect";
2215
+ var SetupPromptService = class extends Context10.Service()(
2216
+ "@oopst/cli/SetupPromptService"
1865
2217
  ) {
1866
2218
  };
1867
- var SetupRandomService = class extends Context7.Service()(
1868
- "@oopst/cli/SetupRandomService"
2219
+ var SetupConfigFileService = class extends Context10.Service()(
2220
+ "@oopst/cli/SetupConfigFileService"
1869
2221
  ) {
1870
2222
  };
1871
- var SetupOutputService = class extends Context7.Service()(
2223
+
2224
+ // src/services/setup-runtime-services.ts
2225
+ import { Context as Context11 } from "effect";
2226
+ var SetupOutputService = class extends Context11.Service()(
1872
2227
  "@oopst/cli/SetupOutputService"
1873
2228
  ) {
1874
2229
  };
1875
- var SetupEnvironmentService = class extends Context7.Service()(
2230
+ var SetupEnvironmentService = class extends Context11.Service()(
1876
2231
  "@oopst/cli/SetupEnvironmentService"
1877
2232
  ) {
1878
2233
  };
2234
+
2235
+ // src/setup.ts
2236
+ var CRON_START_MARKER = "# OopsT sync start";
2237
+ var CRON_END_MARKER = "# OopsT sync end";
2238
+ var DEFAULT_CRON_PATHS = ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"];
2239
+ var DEFAULT_PACKAGE_NAME = "@cookielab.io/oopst";
2240
+ var CONFIG_FILE_MODE = 384;
2241
+ var LINE_SPLIT_PATTERN = /\r?\n/u;
2242
+ var SetupError = class extends Schema24.TaggedErrorClass()("SetupError", {
2243
+ message: Schema24.String,
2244
+ exitCode: Schema24.Number,
2245
+ cause: Schema24.optionalKey(Schema24.Defect())
2246
+ }) {
2247
+ constructor(message, options = {}) {
2248
+ const exitCode = options.exitCode ?? EXIT.generalFailure;
2249
+ super(
2250
+ options.cause === void 0 ? { message, exitCode } : { message, exitCode, cause: options.cause }
2251
+ );
2252
+ }
2253
+ };
1879
2254
  function errorMessage3(cause) {
1880
2255
  return cause instanceof Error ? cause.message : String(cause);
1881
2256
  }
@@ -2065,7 +2440,11 @@ function commandFailureOptions(result) {
2065
2440
  return result.error === void 0 ? {} : { cause: result.error };
2066
2441
  }
2067
2442
  function commandFailureReason(result, fallback) {
2068
- return result.error?.message ?? result.stderr.trim() ?? fallback;
2443
+ if (result.error !== void 0) {
2444
+ return result.error.message;
2445
+ }
2446
+ const stderr2 = result.stderr.trim();
2447
+ return stderr2.length > 0 ? stderr2 : fallback;
2069
2448
  }
2070
2449
  function existingCrontabFromResult(result) {
2071
2450
  if (result.status === 0) {
@@ -2270,29 +2649,29 @@ import { dirname as dirname3, isAbsolute as isAbsolute3, join as join8, matchesG
2270
2649
 
2271
2650
  // ../../packages/parsers/src/claude/index.ts
2272
2651
  import { basename, dirname as dirname2 } from "path";
2273
- import { Effect as Effect17, Stream as Stream10 } from "effect";
2652
+ import { Effect as Effect18, Stream as Stream11 } from "effect";
2274
2653
 
2275
2654
  // ../../packages/parsers/src/config.ts
2276
2655
  import { homedir as homedir2 } from "os";
2277
2656
  import { isAbsolute as isAbsolute2, join as join4 } from "path";
2278
- import process3 from "process";
2279
- import { Config as Config2, ConfigProvider as ConfigProvider2, Effect as Effect12, Schema as Schema19 } from "effect";
2280
- var runtimeEnv2 = process3["env"];
2281
- var RawParserEnvSchema = Schema19.Struct({
2657
+ import { env as processEnv2 } from "process";
2658
+ import { Config as Config2, ConfigProvider as ConfigProvider2, Effect as Effect12, Schema as Schema25 } from "effect";
2659
+ var runtimeEnv2 = processEnv2;
2660
+ var RawParserEnvSchema = Schema25.Struct({
2282
2661
  // biome-ignore lint/style/useNamingConvention: ENV vars are SCREAMING_SNAKE_CASE by convention
2283
- CLAUDE_CONFIG_DIR: Schema19.optional(Schema19.Trim),
2662
+ CLAUDE_CONFIG_DIR: Schema25.optional(Schema25.Trim),
2284
2663
  // biome-ignore lint/style/useNamingConvention: ENV vars are SCREAMING_SNAKE_CASE by convention
2285
- HERMES_CODING_AGENT_DIR: Schema19.optional(Schema19.Trim),
2664
+ HERMES_CODING_AGENT_DIR: Schema25.optional(Schema25.Trim),
2286
2665
  // biome-ignore lint/style/useNamingConvention: ENV vars are SCREAMING_SNAKE_CASE by convention
2287
- HERMES_CODING_AGENT_SESSION_DIR: Schema19.optional(Schema19.Trim),
2666
+ HERMES_CODING_AGENT_SESSION_DIR: Schema25.optional(Schema25.Trim),
2288
2667
  // biome-ignore lint/style/useNamingConvention: ENV vars are SCREAMING_SNAKE_CASE by convention
2289
- OPENCODE_DATA_DIR: Schema19.optional(Schema19.Trim),
2668
+ OPENCODE_DATA_DIR: Schema25.optional(Schema25.Trim),
2290
2669
  // biome-ignore lint/style/useNamingConvention: ENV vars are SCREAMING_SNAKE_CASE by convention
2291
- OPENCODE_DB: Schema19.optional(Schema19.Trim),
2670
+ OPENCODE_DB: Schema25.optional(Schema25.Trim),
2292
2671
  // biome-ignore lint/style/useNamingConvention: ENV vars are SCREAMING_SNAKE_CASE by convention
2293
- PI_CODING_AGENT_DIR: Schema19.optional(Schema19.Trim),
2672
+ PI_CODING_AGENT_DIR: Schema25.optional(Schema25.Trim),
2294
2673
  // biome-ignore lint/style/useNamingConvention: ENV vars are SCREAMING_SNAKE_CASE by convention
2295
- PI_CODING_AGENT_SESSION_DIR: Schema19.optional(Schema19.Trim)
2674
+ PI_CODING_AGENT_SESSION_DIR: Schema25.optional(Schema25.Trim)
2296
2675
  });
2297
2676
  var parserEnvConfig = Config2.schema(RawParserEnvSchema);
2298
2677
  var emptyToUndefined = (value) => value === void 0 || value.length === 0 ? void 0 : value;
@@ -2346,18 +2725,18 @@ function resolveOpenCodeRoot(config = loadParserConfig(), homeDir = readParserHo
2346
2725
  // ../../packages/parsers/src/effect/file-walker.ts
2347
2726
  import { join as join5 } from "path";
2348
2727
  import { NodeFileSystem as NodeFileSystem4 } from "@effect/platform-node";
2349
- import { Context as Context8, Effect as Effect13, FileSystem as FileSystem5, Layer as Layer7, Option as Option2, Schema as Schema20, Stream as Stream5 } from "effect";
2350
- var FileWalkerFilesystemError = class extends Schema20.TaggedErrorClass()(
2728
+ import { Context as Context12, Effect as Effect13, FileSystem as FileSystem5, Layer as Layer7, Option as Option2, Schema as Schema26, Stream as Stream5 } from "effect";
2729
+ var FileWalkerFilesystemError = class extends Schema26.TaggedErrorClass()(
2351
2730
  "FileWalkerFilesystemError",
2352
2731
  {
2353
- message: Schema20.String,
2354
- operation: Schema20.Literals(["readdir", "stat"]),
2355
- path: Schema20.String,
2356
- cause: Schema20.optionalKey(Schema20.Defect())
2732
+ message: Schema26.String,
2733
+ operation: Schema26.Literals(["readdir", "stat"]),
2734
+ path: Schema26.String,
2735
+ cause: Schema26.optionalKey(Schema26.Defect())
2357
2736
  }
2358
2737
  ) {
2359
2738
  };
2360
- var FileWalker = class extends Context8.Service()("@oopst/parsers/FileWalker") {
2739
+ var FileWalker = class extends Context12.Service()("@oopst/parsers/FileWalker") {
2361
2740
  };
2362
2741
  var toFileWalkerFilesystemError = (operation, path) => (error) => makeFileWalkerFilesystemError(operation, path, error);
2363
2742
  var makeFileWalkerFilesystemError = (operation, path, error) => new FileWalkerFilesystemError({
@@ -2490,27 +2869,199 @@ var stderrLogger = createWarningLogger(stderr);
2490
2869
 
2491
2870
  // ../../packages/parsers/src/claude/bucketize.ts
2492
2871
  import { createReadStream as createReadStream2 } from "fs";
2872
+ import { normalize as normalize3 } from "path";
2493
2873
  import { createInterface as createInterface3 } from "readline";
2494
- import { Cause as Cause2, Effect as Effect16, Queue as Queue2, Stream as Stream9 } from "effect";
2874
+ import { Cause as Cause2, Effect as Effect17, Queue as Queue2, Stream as Stream10 } from "effect";
2495
2875
 
2496
2876
  // ../../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";
2877
+ import { Effect as Effect16, Stream as Stream9 } from "effect";
2500
2878
 
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
- }) {
2879
+ // ../../packages/parsers/src/codex/counters.ts
2880
+ var SEEN_RAW_TOTALS_LIMIT = 64;
2881
+ var ZERO_CODEX_USAGE = {
2882
+ inputTokens: 0,
2883
+ outputTokens: 0,
2884
+ cacheReadTokens: 0
2508
2885
  };
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
- });
2886
+ function normalizeCodexUsage(usage) {
2887
+ return {
2888
+ inputTokens: Math.max(0, usage.input_tokens ?? 0),
2889
+ outputTokens: Math.max(0, usage.output_tokens ?? 0),
2890
+ cacheReadTokens: Math.max(0, usage.cached_input_tokens ?? usage.cache_read_input_tokens ?? 0)
2891
+ };
2892
+ }
2893
+ function sameCodexUsage(a, b) {
2894
+ return a.inputTokens === b.inputTokens && a.outputTokens === b.outputTokens && a.cacheReadTokens === b.cacheReadTokens;
2895
+ }
2896
+ function positiveCodexUsage(usage) {
2897
+ return usage.inputTokens > 0 || usage.outputTokens > 0 || usage.cacheReadTokens > 0;
2898
+ }
2899
+ function addCodexUsage(a, b) {
2900
+ return {
2901
+ inputTokens: a.inputTokens + b.inputTokens,
2902
+ outputTokens: a.outputTokens + b.outputTokens,
2903
+ cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens
2904
+ };
2905
+ }
2906
+ function minCodexUsage(a, b) {
2907
+ return {
2908
+ inputTokens: Math.min(a.inputTokens, b.inputTokens),
2909
+ outputTokens: Math.min(a.outputTokens, b.outputTokens),
2910
+ cacheReadTokens: Math.min(a.cacheReadTokens, b.cacheReadTokens)
2911
+ };
2912
+ }
2913
+ function maxCodexUsage(a, b) {
2914
+ if (a === void 0) {
2915
+ return b;
2916
+ }
2917
+ return {
2918
+ inputTokens: Math.max(a.inputTokens, b.inputTokens),
2919
+ outputTokens: Math.max(a.outputTokens, b.outputTokens),
2920
+ cacheReadTokens: Math.max(a.cacheReadTokens, b.cacheReadTokens)
2921
+ };
2922
+ }
2923
+ function subtractCodexUsageFloorZero(current, baseline) {
2924
+ return {
2925
+ inputTokens: Math.max(0, current.inputTokens - baseline.inputTokens),
2926
+ outputTokens: Math.max(0, current.outputTokens - baseline.outputTokens),
2927
+ cacheReadTokens: Math.max(0, current.cacheReadTokens - baseline.cacheReadTokens)
2928
+ };
2929
+ }
2930
+ function codexUsageContains(current, baseline) {
2931
+ return current.inputTokens >= baseline.inputTokens && current.outputTokens >= baseline.outputTokens && current.cacheReadTokens >= baseline.cacheReadTokens;
2932
+ }
2933
+ function codexUsageAtMost(current, ceiling) {
2934
+ return current.inputTokens <= ceiling.inputTokens && current.outputTokens <= ceiling.outputTokens && current.cacheReadTokens <= ceiling.cacheReadTokens;
2935
+ }
2936
+ function makeCodexContainmentState() {
2937
+ return {
2938
+ countedUsage: void 0,
2939
+ rawBaseline: void 0,
2940
+ watermark: void 0,
2941
+ seenRawTotals: [],
2942
+ sawDivergentTotals: false,
2943
+ sawInterleavedTotals: false,
2944
+ sawUnresolvedTotal: false
2945
+ };
2946
+ }
2947
+ function containedCodexDelta(watermark, counted, current) {
2948
+ const water = watermark ?? ZERO_CODEX_USAGE;
2949
+ const accepted = counted ?? ZERO_CODEX_USAGE;
2950
+ const component = (highWatermark, countedUsage, currentUsage) => currentUsage >= highWatermark ? Math.max(0, currentUsage - Math.max(highWatermark, countedUsage)) : Math.max(0, currentUsage - countedUsage);
2951
+ return {
2952
+ inputTokens: component(water.inputTokens, accepted.inputTokens, current.inputTokens),
2953
+ outputTokens: component(water.outputTokens, accepted.outputTokens, current.outputTokens),
2954
+ cacheReadTokens: component(water.cacheReadTokens, accepted.cacheReadTokens, current.cacheReadTokens)
2955
+ };
2956
+ }
2957
+ function divergentCodexDelta(rawBaseline, countedBaseline, current) {
2958
+ const raw = rawBaseline ?? ZERO_CODEX_USAGE;
2959
+ const counted = countedBaseline ?? ZERO_CODEX_USAGE;
2960
+ const component = (rawUsage, countedUsage, currentUsage) => currentUsage >= rawUsage ? Math.max(0, currentUsage - rawUsage) : Math.max(0, currentUsage - countedUsage);
2961
+ return {
2962
+ inputTokens: component(raw.inputTokens, counted.inputTokens, current.inputTokens),
2963
+ outputTokens: component(raw.outputTokens, counted.outputTokens, current.outputTokens),
2964
+ cacheReadTokens: component(raw.cacheReadTokens, counted.cacheReadTokens, current.cacheReadTokens)
2965
+ };
2966
+ }
2967
+ function rememberCodexTotal(state, total) {
2968
+ state.watermark = maxCodexUsage(state.watermark, total);
2969
+ if (state.seenRawTotals.some((seen) => sameCodexUsage(seen, total))) {
2970
+ return;
2971
+ }
2972
+ state.seenRawTotals.push(total);
2973
+ if (state.seenRawTotals.length > SEEN_RAW_TOTALS_LIMIT) {
2974
+ state.seenRawTotals.splice(0, state.seenRawTotals.length - SEEN_RAW_TOTALS_LIMIT);
2975
+ }
2976
+ }
2977
+ function applyCodexSnapshotWithLast(state, last, total, watermarkBaseline) {
2978
+ const base = state.countedUsage ?? ZERO_CODEX_USAGE;
2979
+ if (total === void 0) {
2980
+ state.countedUsage = addCodexUsage(base, last);
2981
+ state.rawBaseline = state.countedUsage;
2982
+ state.watermark = maxCodexUsage(state.watermark, state.countedUsage);
2983
+ return last;
2984
+ }
2985
+ let delta = last;
2986
+ if (state.sawInterleavedTotals) {
2987
+ delta = minCodexUsage(last, containedCodexDelta(watermarkBaseline, state.countedUsage, total));
2988
+ } else {
2989
+ const totalDelta = subtractCodexUsageFloorZero(total, watermarkBaseline ?? ZERO_CODEX_USAGE);
2990
+ const useTotalDelta = !state.sawDivergentTotals && watermarkBaseline !== void 0 && codexUsageContains(total, watermarkBaseline) && codexUsageAtMost(totalDelta, last);
2991
+ if (useTotalDelta) {
2992
+ delta = totalDelta;
2993
+ }
2994
+ }
2995
+ state.countedUsage = addCodexUsage(base, delta);
2996
+ state.rawBaseline = total;
2997
+ state.sawDivergentTotals ||= !sameCodexUsage(total, state.countedUsage);
2998
+ return delta;
2999
+ }
3000
+ function applyCodexTotalOnlySnapshot(state, total, watermarkBaseline) {
3001
+ let delta;
3002
+ if (state.sawInterleavedTotals) {
3003
+ delta = containedCodexDelta(watermarkBaseline, state.countedUsage, total);
3004
+ } else if (state.sawDivergentTotals) {
3005
+ delta = divergentCodexDelta(watermarkBaseline, state.countedUsage, total);
3006
+ } else {
3007
+ delta = subtractCodexUsageFloorZero(total, watermarkBaseline ?? ZERO_CODEX_USAGE);
3008
+ }
3009
+ state.countedUsage = addCodexUsage(state.countedUsage ?? ZERO_CODEX_USAGE, delta);
3010
+ state.rawBaseline = total;
3011
+ state.sawDivergentTotals ||= !sameCodexUsage(total, state.countedUsage);
3012
+ return delta;
3013
+ }
3014
+ function applyCodexContainedSnapshot(state, last, total, options = {}) {
3015
+ if (total !== void 0 && state.seenRawTotals.some((seen) => sameCodexUsage(seen, total))) {
3016
+ return void 0;
3017
+ }
3018
+ if (total !== void 0 && state.watermark !== void 0 && !codexUsageContains(total, state.watermark)) {
3019
+ state.sawInterleavedTotals = true;
3020
+ }
3021
+ const watermarkBaseline = state.watermark ?? state.rawBaseline;
3022
+ const effectiveLast = total !== void 0 && options.totalOnlyBeforeInterleaving === true && !state.sawInterleavedTotals ? void 0 : last;
3023
+ let delta;
3024
+ if (effectiveLast !== void 0) {
3025
+ delta = applyCodexSnapshotWithLast(state, effectiveLast, total, watermarkBaseline);
3026
+ } else if (total !== void 0) {
3027
+ delta = applyCodexTotalOnlySnapshot(state, total, watermarkBaseline);
3028
+ }
3029
+ if (total !== void 0) {
3030
+ rememberCodexTotal(state, total);
3031
+ }
3032
+ return delta !== void 0 && positiveCodexUsage(delta) ? delta : void 0;
3033
+ }
3034
+ function applyCodexUnresolvedSnapshot(state, last, total) {
3035
+ if (total === void 0) {
3036
+ return applyCodexContainedSnapshot(state, last, void 0);
3037
+ }
3038
+ if (state.seenRawTotals.some((seen) => sameCodexUsage(seen, total))) {
3039
+ return void 0;
3040
+ }
3041
+ if (state.watermark !== void 0 && !codexUsageContains(total, state.watermark)) {
3042
+ state.sawInterleavedTotals = true;
3043
+ }
3044
+ const watermarkBaseline = state.watermark ?? state.rawBaseline;
3045
+ let delta;
3046
+ if (state.sawUnresolvedTotal && last !== void 0) {
3047
+ delta = minCodexUsage(last, subtractCodexUsageFloorZero(total, watermarkBaseline ?? ZERO_CODEX_USAGE));
3048
+ state.countedUsage = addCodexUsage(state.countedUsage ?? ZERO_CODEX_USAGE, delta);
3049
+ state.rawBaseline = state.countedUsage;
3050
+ }
3051
+ state.sawUnresolvedTotal = true;
3052
+ rememberCodexTotal(state, total);
3053
+ return delta !== void 0 && positiveCodexUsage(delta) ? delta : void 0;
3054
+ }
3055
+ function cumulativeCodexDelta(current, previous) {
3056
+ if (previous === void 0) {
3057
+ return current;
3058
+ }
3059
+ return {
3060
+ inputTokens: current.inputTokens < previous.inputTokens ? current.inputTokens : current.inputTokens - previous.inputTokens,
3061
+ outputTokens: current.outputTokens < previous.outputTokens ? current.outputTokens : current.outputTokens - previous.outputTokens,
3062
+ cacheReadTokens: current.cacheReadTokens < previous.cacheReadTokens ? current.cacheReadTokens : current.cacheReadTokens - previous.cacheReadTokens
3063
+ };
3064
+ }
2514
3065
 
2515
3066
  // ../../packages/parsers/src/codex/events.ts
2516
3067
  function isObject(value) {
@@ -2519,6 +3070,12 @@ function isObject(value) {
2519
3070
  function isFiniteNumber(value) {
2520
3071
  return typeof value === "number" && Number.isFinite(value);
2521
3072
  }
3073
+ var MAX_CODEX_SOURCE_ID_LENGTH = 512;
3074
+ var MAX_CODEX_TIMESTAMP_LENGTH = 128;
3075
+ var MAX_CODEX_LOCAL_PATH_LENGTH = 4096;
3076
+ function isOptionalBoundedString(value, maxLength) {
3077
+ return value === void 0 || typeof value === "string" && value.length <= maxLength;
3078
+ }
2522
3079
  function isCodexUsageCounters(value) {
2523
3080
  if (!isObject(value)) {
2524
3081
  return false;
@@ -2549,6 +3106,11 @@ function isCodexTokenCountEvent(value) {
2549
3106
  if (payload["type"] !== "token_count") {
2550
3107
  return false;
2551
3108
  }
3109
+ for (const key6 of ["id", "turn_id", "turnId"]) {
3110
+ if (!isOptionalBoundedString(payload[key6], MAX_CODEX_SOURCE_ID_LENGTH)) {
3111
+ return false;
3112
+ }
3113
+ }
2552
3114
  const info = payload["info"];
2553
3115
  if (info !== void 0) {
2554
3116
  if (!isObject(info)) {
@@ -2557,6 +3119,11 @@ function isCodexTokenCountEvent(value) {
2557
3119
  if (info["model"] !== void 0 && typeof info["model"] !== "string") {
2558
3120
  return false;
2559
3121
  }
3122
+ for (const key6 of ["id", "turn_id", "turnId"]) {
3123
+ if (!isOptionalBoundedString(info[key6], MAX_CODEX_SOURCE_ID_LENGTH)) {
3124
+ return false;
3125
+ }
3126
+ }
2560
3127
  if (info["last_token_usage"] !== void 0 && !isCodexUsageCounters(info["last_token_usage"])) {
2561
3128
  return false;
2562
3129
  }
@@ -2572,6 +3139,28 @@ function isCodexTokenCountEvent(value) {
2572
3139
  }
2573
3140
  return true;
2574
3141
  }
3142
+ function isCodexTaskStartedEvent(value) {
3143
+ if (!isObject(value) || value["type"] !== "event_msg" || typeof value["timestamp"] !== "string") {
3144
+ return false;
3145
+ }
3146
+ const payload = value["payload"];
3147
+ if (!isObject(payload) || payload["type"] !== "task_started") {
3148
+ return false;
3149
+ }
3150
+ for (const key6 of ["id", "turn_id", "turnId"]) {
3151
+ if (!isOptionalBoundedString(payload[key6], MAX_CODEX_SOURCE_ID_LENGTH)) {
3152
+ return false;
3153
+ }
3154
+ }
3155
+ const info = payload["info"];
3156
+ if (info === void 0) {
3157
+ return true;
3158
+ }
3159
+ if (!isObject(info)) {
3160
+ return false;
3161
+ }
3162
+ return ["id", "turn_id", "turnId"].every((key6) => isOptionalBoundedString(info[key6], MAX_CODEX_SOURCE_ID_LENGTH));
3163
+ }
2575
3164
  function isCodexTurnContextEvent(value) {
2576
3165
  if (!isObject(value)) {
2577
3166
  return false;
@@ -2610,18 +3199,66 @@ function isCodexSessionMetaEvent(value) {
2610
3199
  if (value["type"] !== "session_meta") {
2611
3200
  return false;
2612
3201
  }
2613
- if (value["timestamp"] !== void 0 && typeof value["timestamp"] !== "string") {
3202
+ if (!isOptionalBoundedString(value["timestamp"], MAX_CODEX_TIMESTAMP_LENGTH)) {
2614
3203
  return false;
2615
3204
  }
3205
+ for (const key6 of ["id", "session_id", "sessionId"]) {
3206
+ if (!isOptionalBoundedString(value[key6], MAX_CODEX_SOURCE_ID_LENGTH)) {
3207
+ return false;
3208
+ }
3209
+ }
2616
3210
  const payload = value["payload"];
2617
3211
  if (!isObject(payload)) {
2618
3212
  return false;
2619
3213
  }
2620
- if (payload["cwd"] !== void 0 && typeof payload["cwd"] !== "string") {
3214
+ if (!isOptionalBoundedString(payload["cwd"], MAX_CODEX_LOCAL_PATH_LENGTH)) {
3215
+ return false;
3216
+ }
3217
+ for (const key6 of [
3218
+ "id",
3219
+ "session_id",
3220
+ "sessionId",
3221
+ "forked_from_id",
3222
+ "forkedFromId",
3223
+ "parent_thread_id",
3224
+ "parent_session_id",
3225
+ "parentSessionId"
3226
+ ]) {
3227
+ if (!isOptionalBoundedString(payload[key6], MAX_CODEX_SOURCE_ID_LENGTH)) {
3228
+ return false;
3229
+ }
3230
+ }
3231
+ if (!isOptionalBoundedString(payload["timestamp"], MAX_CODEX_TIMESTAMP_LENGTH)) {
2621
3232
  return false;
2622
3233
  }
2623
3234
  return true;
2624
3235
  }
3236
+ function nonEmpty(value) {
3237
+ const trimmed = value?.trim();
3238
+ return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
3239
+ }
3240
+ function extractCodexSourceMetadata(event) {
3241
+ const sourceSessionId = nonEmpty(
3242
+ event.payload.id ?? event.id ?? event.payload.session_id ?? event.payload.sessionId ?? event.session_id ?? event.sessionId
3243
+ );
3244
+ const canonicalSessionId = nonEmpty(
3245
+ event.payload.session_id ?? event.payload.sessionId ?? event.session_id ?? event.sessionId
3246
+ );
3247
+ return {
3248
+ sourceSessionId,
3249
+ canonicalSessionId,
3250
+ lineageSessionId: canonicalSessionId ?? sourceSessionId,
3251
+ parentSourceSessionId: nonEmpty(
3252
+ event.payload.forked_from_id ?? event.payload.forkedFromId ?? event.payload.parent_session_id ?? event.payload.parentSessionId
3253
+ ),
3254
+ // Native agent files also carry parent_thread_id for structural thread
3255
+ // ownership. It is not evidence that token counters share a cumulative
3256
+ // scale, so retain it as bounded metadata without using it as a baseline.
3257
+ parentThreadId: nonEmpty(event.payload.parent_thread_id),
3258
+ forkedAt: nonEmpty(event.payload.timestamp ?? event.timestamp),
3259
+ cwd: event.payload.cwd
3260
+ };
3261
+ }
2625
3262
  function extractCodexUsageSnapshot(event) {
2626
3263
  return {
2627
3264
  last: event.payload.info?.last_token_usage,
@@ -2631,30 +3268,36 @@ function extractCodexUsageSnapshot(event) {
2631
3268
  function pickCodexEventModel(event) {
2632
3269
  return event.payload.info?.model ?? event.payload.model ?? event.model;
2633
3270
  }
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
- };
3271
+ function pickCodexTurnId(event) {
3272
+ return nonEmpty(
3273
+ event.payload.turn_id ?? event.payload.turnId ?? event.payload.id ?? event.payload.info?.turn_id ?? event.payload.info?.turnId ?? event.payload.info?.id
3274
+ );
2657
3275
  }
3276
+
3277
+ // ../../packages/parsers/src/codex/lineage.ts
3278
+ import { createReadStream } from "fs";
3279
+ import { open, stat } from "fs/promises";
3280
+ import { normalize as normalize2 } from "path";
3281
+ import { createInterface as createInterface2 } from "readline";
3282
+ import { Cause, Effect as Effect15, Queue, Stream as Stream8 } from "effect";
3283
+
3284
+ // ../../packages/parsers/src/types.ts
3285
+ import { Schema as Schema27, Stream as Stream7 } from "effect";
3286
+ var ParserSourceReadError = class extends Schema27.TaggedErrorClass()("ParserSourceReadError", {
3287
+ message: Schema27.String,
3288
+ path: Schema27.String,
3289
+ cause: Schema27.optionalKey(Schema27.Defect())
3290
+ }) {
3291
+ };
3292
+ var makeParserSourceReadError = (path, error) => new ParserSourceReadError({
3293
+ message: `Parser source read failed: ${error instanceof Error ? error.message : String(error)}`,
3294
+ path,
3295
+ cause: error
3296
+ });
3297
+
3298
+ // ../../packages/parsers/src/codex/lineage.ts
3299
+ var CANDIDATE_TAIL_BYTES = 262144;
3300
+ var CANDIDATE_INSPECTION_CONCURRENCY = 8;
2658
3301
  function lineStreamFromFile(path) {
2659
3302
  return Stream8.callback(
2660
3303
  (queue) => Effect15.acquireRelease(
@@ -2699,44 +3342,442 @@ function lineStreamFromFile(path) {
2699
3342
  )
2700
3343
  );
2701
3344
  }
2702
- function normalizeUsage(usage) {
3345
+ async function inspectSourceMetadata(path) {
3346
+ const stream = createReadStream(path, { encoding: "utf8" });
3347
+ const lines = createInterface2({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
3348
+ let first;
3349
+ const sourceSessionIds = /* @__PURE__ */ new Set();
3350
+ try {
3351
+ for await (const line of lines) {
3352
+ if (!line.includes("session_meta")) {
3353
+ continue;
3354
+ }
3355
+ let parsed;
3356
+ try {
3357
+ parsed = JSON.parse(line);
3358
+ } catch {
3359
+ continue;
3360
+ }
3361
+ if (isCodexSessionMetaEvent(parsed)) {
3362
+ const metadata = extractCodexSourceMetadata(parsed);
3363
+ first ??= metadata;
3364
+ if (metadata.sourceSessionId !== void 0) {
3365
+ sourceSessionIds.add(metadata.sourceSessionId);
3366
+ }
3367
+ }
3368
+ }
3369
+ return { first, containsMultipleSessionIds: sourceSessionIds.size > 1 };
3370
+ } finally {
3371
+ lines.close();
3372
+ stream.destroy();
3373
+ }
3374
+ }
3375
+ async function hasCompleteTail(path, size) {
3376
+ if (size <= 0) {
3377
+ return false;
3378
+ }
3379
+ const handle = await open(path, "r");
3380
+ try {
3381
+ const bytesToRead = Math.min(size, CANDIDATE_TAIL_BYTES);
3382
+ const tail = Buffer.allocUnsafe(bytesToRead);
3383
+ await handle.read(tail, 0, bytesToRead, size - bytesToRead);
3384
+ const lastLine = tail.toString("utf8").split("\n").findLast((line) => line.trim().length > 0)?.trim();
3385
+ if (!lastLine) {
3386
+ return false;
3387
+ }
3388
+ try {
3389
+ JSON.parse(lastLine);
3390
+ return true;
3391
+ } catch {
3392
+ return false;
3393
+ }
3394
+ } finally {
3395
+ await handle.close();
3396
+ }
3397
+ }
3398
+ function inspectCodexSourceCandidate(path) {
3399
+ return Effect15.tryPromise({
3400
+ try: async () => {
3401
+ const info = await stat(path);
3402
+ const inspectedMetadata = await inspectSourceMetadata(path);
3403
+ return {
3404
+ path,
3405
+ normalizedPath: normalize2(path),
3406
+ metadata: inspectedMetadata.first,
3407
+ containsMultipleSessionIds: inspectedMetadata.containsMultipleSessionIds,
3408
+ complete: await hasCompleteTail(path, info.size),
3409
+ mtimeMs: info.mtimeMs,
3410
+ size: info.size
3411
+ };
3412
+ },
3413
+ catch: (error) => makeParserSourceReadError(path, error)
3414
+ });
3415
+ }
3416
+ function candidateComesFirst(a, b) {
3417
+ if (a.complete !== b.complete) {
3418
+ return a.complete ? -1 : 1;
3419
+ }
3420
+ if (a.size !== b.size) {
3421
+ return b.size - a.size;
3422
+ }
3423
+ if (a.mtimeMs !== b.mtimeMs) {
3424
+ return b.mtimeMs - a.mtimeMs;
3425
+ }
3426
+ return a.normalizedPath.localeCompare(b.normalizedPath);
3427
+ }
3428
+ function candidateConflictKind(candidates) {
3429
+ const parents = new Set(candidates.map((candidate) => candidate.metadata?.parentSourceSessionId ?? ""));
3430
+ if (parents.size > 1) {
3431
+ return "parent";
3432
+ }
3433
+ const forkBoundaries = new Set(candidates.map((candidate) => candidate.metadata?.forkedAt ?? ""));
3434
+ return forkBoundaries.size > 1 ? "fork_boundary" : void 0;
3435
+ }
3436
+ function cycleSessionIds(sources) {
3437
+ const parents = /* @__PURE__ */ new Map();
3438
+ const knownIds = new Set(
3439
+ sources.flatMap((source) => source.sourceSessionId === void 0 ? [] : [source.sourceSessionId])
3440
+ );
3441
+ for (const source of sources) {
3442
+ if (source.sourceSessionId !== void 0 && source.parentSourceSessionId !== void 0 && knownIds.has(source.parentSourceSessionId)) {
3443
+ parents.set(source.sourceSessionId, source.parentSourceSessionId);
3444
+ }
3445
+ }
3446
+ const cycles = /* @__PURE__ */ new Set();
3447
+ for (const sourceId of [...knownIds].sort()) {
3448
+ const positions = /* @__PURE__ */ new Map();
3449
+ const chain = [];
3450
+ let current = sourceId;
3451
+ while (current !== void 0) {
3452
+ const cycleStart = positions.get(current);
3453
+ if (cycleStart !== void 0) {
3454
+ for (const member of chain.slice(cycleStart)) {
3455
+ cycles.add(member);
3456
+ }
3457
+ break;
3458
+ }
3459
+ positions.set(current, chain.length);
3460
+ chain.push(current);
3461
+ current = parents.get(current);
3462
+ }
3463
+ }
3464
+ return cycles;
3465
+ }
3466
+ function groupCodexSourceCandidates(candidates) {
3467
+ const groups = /* @__PURE__ */ new Map();
3468
+ for (const candidate of candidates) {
3469
+ const sourceId = candidate.metadata?.sourceSessionId;
3470
+ const groupKey = sourceId === void 0 ? `path:${candidate.normalizedPath}` : `session:${sourceId}`;
3471
+ const group = groups.get(groupKey) ?? [];
3472
+ group.push(candidate);
3473
+ groups.set(groupKey, group);
3474
+ }
3475
+ return groups;
3476
+ }
3477
+ function indexedSourceFromCandidates(group, cutoffMs, logger) {
3478
+ group.sort(candidateComesFirst);
3479
+ const [canonical] = group;
3480
+ if (canonical === void 0) {
3481
+ return void 0;
3482
+ }
3483
+ const conflict = candidateConflictKind(group);
3484
+ if (conflict !== void 0) {
3485
+ logger.warn("conflicting codex source candidates", {
3486
+ candidateCount: group.length,
3487
+ conflict
3488
+ });
3489
+ }
3490
+ const sourceSessionId = canonical.metadata?.sourceSessionId;
2703
3491
  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)
3492
+ path: canonical.path,
3493
+ sourceIdentity: sourceSessionId ?? canonical.normalizedPath,
3494
+ sourceSessionId,
3495
+ lineageSessionId: canonical.metadata?.lineageSessionId,
3496
+ parentSourceSessionId: canonical.metadata?.parentSourceSessionId,
3497
+ forkedAt: canonical.metadata?.forkedAt,
3498
+ cwd: canonical.metadata?.cwd,
3499
+ containsMultipleSessionIds: canonical.containsMultipleSessionIds || canonical.metadata?.canonicalSessionId !== void 0,
3500
+ eligible: group.some((candidate) => candidate.mtimeMs >= cutoffMs)
2707
3501
  };
2708
3502
  }
2709
- function sameUsage(a, b) {
2710
- return a.inputTokens === b.inputTokens && a.outputTokens === b.outputTokens && a.cacheReadTokens === b.cacheReadTokens;
3503
+ function indexCodexLineageSessions(sources) {
3504
+ const bySessionId = /* @__PURE__ */ new Map();
3505
+ for (const source of sources) {
3506
+ const { lineageSessionId } = source;
3507
+ if (lineageSessionId === void 0) {
3508
+ continue;
3509
+ }
3510
+ const existing = bySessionId.get(lineageSessionId);
3511
+ if (existing === void 0 || source.sourceSessionId === lineageSessionId && existing.sourceSessionId !== lineageSessionId) {
3512
+ bySessionId.set(lineageSessionId, source);
3513
+ }
3514
+ }
3515
+ return bySessionId;
2711
3516
  }
2712
- function positiveUsage(usage) {
2713
- return usage.inputTokens > 0 || usage.outputTokens > 0 || usage.cacheReadTokens > 0;
3517
+ var buildCodexSourceIndex = Effect15.fn("CodexLineage.buildSourceIndex")(function* (paths, cutoff, logger) {
3518
+ const uniquePaths = [...new Set(paths.map(normalize2))].sort();
3519
+ const candidates = yield* Effect15.forEach(uniquePaths, inspectCodexSourceCandidate, {
3520
+ concurrency: CANDIDATE_INSPECTION_CONCURRENCY
3521
+ });
3522
+ const groups = groupCodexSourceCandidates(candidates);
3523
+ const sources = [];
3524
+ for (const groupKey of [...groups.keys()].sort()) {
3525
+ const group = groups.get(groupKey) ?? [];
3526
+ const source = indexedSourceFromCandidates(group, cutoff.getTime(), logger);
3527
+ if (source !== void 0) {
3528
+ sources.push(source);
3529
+ }
3530
+ }
3531
+ sources.sort((a, b) => a.sourceIdentity.localeCompare(b.sourceIdentity));
3532
+ return {
3533
+ sources,
3534
+ bySessionId: indexCodexLineageSessions(sources),
3535
+ cycleSessionIds: cycleSessionIds(sources)
3536
+ };
3537
+ });
3538
+ function parseTimestamp(timestamp) {
3539
+ const parsed = Date.parse(timestamp);
3540
+ return Number.isFinite(parsed) ? parsed : void 0;
2714
3541
  }
2715
- function cumulativeDelta(current, previous) {
2716
- if (previous === void 0) {
2717
- return current;
3542
+ function parseSnapshotIncrement(state, last, total) {
3543
+ if (last !== void 0) {
3544
+ return normalizeCodexUsage(last);
3545
+ }
3546
+ return total === void 0 ? void 0 : cumulativeCodexDelta(total, state.previousTotal);
3547
+ }
3548
+ function accumulateCodexSnapshot(state, raw) {
3549
+ let parsed;
3550
+ try {
3551
+ parsed = JSON.parse(raw);
3552
+ } catch {
3553
+ return state;
3554
+ }
3555
+ if (!isCodexTokenCountEvent(parsed)) {
3556
+ return state;
3557
+ }
3558
+ const snapshot = extractCodexUsageSnapshot(parsed);
3559
+ const total = snapshot.total === void 0 ? void 0 : normalizeCodexUsage(snapshot.total);
3560
+ if (state.containment !== void 0) {
3561
+ const last = snapshot.last === void 0 ? void 0 : normalizeCodexUsage(snapshot.last);
3562
+ const increment2 = applyCodexContainedSnapshot(state.containment, last, total);
3563
+ if (increment2 === void 0) {
3564
+ return state;
3565
+ }
3566
+ state.counted = addCodexUsage(state.counted, increment2);
3567
+ state.snapshots.push({
3568
+ timestamp: parsed.timestamp,
3569
+ timestampMs: parseTimestamp(parsed.timestamp),
3570
+ usage: state.counted
3571
+ });
3572
+ return state;
3573
+ }
3574
+ if (total !== void 0 && state.previousTotal !== void 0 && sameCodexUsage(total, state.previousTotal)) {
3575
+ return state;
3576
+ }
3577
+ const increment = parseSnapshotIncrement(state, snapshot.last, total);
3578
+ if (total !== void 0) {
3579
+ state.previousTotal = total;
3580
+ }
3581
+ if (increment === void 0 || !positiveCodexUsage(increment)) {
3582
+ return state;
3583
+ }
3584
+ state.counted = addCodexUsage(state.counted, increment);
3585
+ state.snapshots.push({
3586
+ timestamp: parsed.timestamp,
3587
+ timestampMs: parseTimestamp(parsed.timestamp),
3588
+ usage: state.counted
3589
+ });
3590
+ return state;
3591
+ }
3592
+ function collectCodexSnapshots(path, containsMultipleSessionIds) {
3593
+ return lineStreamFromFile(path).pipe(
3594
+ Stream8.runFold(
3595
+ () => ({
3596
+ snapshots: [],
3597
+ containment: containsMultipleSessionIds ? makeCodexContainmentState() : void 0,
3598
+ counted: ZERO_CODEX_USAGE,
3599
+ previousTotal: void 0
3600
+ }),
3601
+ accumulateCodexSnapshot
3602
+ ),
3603
+ Effect15.map((state) => state.snapshots)
3604
+ );
3605
+ }
3606
+ function selectInheritedUsage(snapshots, forkedAt, forkedAtMs) {
3607
+ let inherited = ZERO_CODEX_USAGE;
3608
+ for (const snapshot of snapshots) {
3609
+ const atOrBefore = snapshot.timestampMs === void 0 ? snapshot.timestamp <= forkedAt : snapshot.timestampMs <= forkedAtMs;
3610
+ if (atOrBefore) {
3611
+ inherited = snapshot.usage;
3612
+ }
2718
3613
  }
3614
+ return inherited;
3615
+ }
3616
+ function makeCodexLineageResolver(index, logger) {
3617
+ const snapshotsBySessionId = /* @__PURE__ */ new Map();
3618
+ const warned = /* @__PURE__ */ new Set();
3619
+ const unresolved = (source, reason) => {
3620
+ const warningKey = `${source.sourceIdentity}:${reason}`;
3621
+ if (!warned.has(warningKey)) {
3622
+ warned.add(warningKey);
3623
+ logger.warn("codex fork baseline unresolved", { reason });
3624
+ }
3625
+ return { _tag: "Unresolved", reason };
3626
+ };
3627
+ const requestFor = (source) => {
3628
+ const parentId = source.parentSourceSessionId;
3629
+ if (parentId === void 0) {
3630
+ return { _tag: "Root" };
3631
+ }
3632
+ if (source.sourceSessionId !== void 0 && index.cycleSessionIds.has(source.sourceSessionId)) {
3633
+ return unresolved(source, "cycle");
3634
+ }
3635
+ const parent = index.bySessionId.get(parentId);
3636
+ if (parent === void 0) {
3637
+ return unresolved(source, "missing_parent");
3638
+ }
3639
+ const { forkedAt } = source;
3640
+ const forkedAtMs = forkedAt === void 0 ? void 0 : parseTimestamp(forkedAt);
3641
+ if (forkedAt === void 0 || forkedAtMs === void 0) {
3642
+ return unresolved(source, "invalid_fork_boundary");
3643
+ }
3644
+ return { _tag: "Ready", forkedAt, forkedAtMs, parent, parentId };
3645
+ };
2719
3646
  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
3647
+ baselineFor: Effect15.fn("CodexLineage.baselineFor")(function* (source) {
3648
+ const request = requestFor(source);
3649
+ if (request._tag !== "Ready") {
3650
+ return request;
3651
+ }
3652
+ let snapshots = snapshotsBySessionId.get(request.parentId);
3653
+ if (snapshots === void 0) {
3654
+ snapshots = yield* collectCodexSnapshots(request.parent.path, request.parent.containsMultipleSessionIds);
3655
+ snapshotsBySessionId.set(request.parentId, snapshots);
3656
+ }
3657
+ return {
3658
+ _tag: "Resolved",
3659
+ usage: selectInheritedUsage(snapshots, request.forkedAt, request.forkedAtMs)
3660
+ };
3661
+ })
2723
3662
  };
2724
3663
  }
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)) {
3664
+
3665
+ // ../../packages/parsers/src/codex/bucketize.ts
3666
+ function startOfUtcHour(d) {
3667
+ return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), 0, 0, 0));
3668
+ }
3669
+ function key2(hourUtc, model) {
3670
+ return `${hourUtc.toISOString()}|${model}`;
3671
+ }
3672
+ function shouldExcludePath(path, excludePath) {
3673
+ return path !== void 0 && excludePath?.(path) === true;
3674
+ }
3675
+ function makeCodexFileAccumulator(options) {
3676
+ const lineageBaseline = options.lineageBaseline ?? { _tag: "Root" };
3677
+ return {
3678
+ buckets: /* @__PURE__ */ new Map(),
3679
+ candidates: [],
3680
+ currentModel: void 0,
3681
+ currentTurnId: void 0,
3682
+ containment: options.containInterleavedTotals === true ? makeCodexContainmentState() : void 0,
3683
+ excluded: false,
3684
+ lineNo: 0,
3685
+ nextEventIndex: 0,
3686
+ lineageKind: lineageBaseline._tag,
3687
+ inheritedBaselineUsage: lineageBaseline._tag === "Resolved" ? lineageBaseline.usage : void 0,
3688
+ sawTurnContext: false,
3689
+ previousTotalUsage: lineageBaseline._tag === "Resolved" ? lineageBaseline.usage : void 0,
3690
+ remainingInheritedUsage: lineageBaseline._tag === "Resolved" ? lineageBaseline.usage : void 0,
3691
+ pendingUsage: [],
3692
+ sessionLastSeenAt: void 0,
3693
+ sessionStartedAt: void 0
3694
+ };
3695
+ }
3696
+ function subtractRemainingInheritedUsage(acc, rawIncrement) {
3697
+ const remaining = acc.remainingInheritedUsage;
3698
+ if (remaining === void 0) {
3699
+ return rawIncrement;
3700
+ }
3701
+ acc.remainingInheritedUsage = subtractCodexUsageFloorZero(remaining, rawIncrement);
3702
+ return subtractCodexUsageFloorZero(rawIncrement, remaining);
3703
+ }
3704
+ function positiveIncrement(increment) {
3705
+ return positiveCodexUsage(increment) ? increment : void 0;
3706
+ }
3707
+ function unresolvedUsageIncrement(acc, total) {
3708
+ if (total === void 0) {
3709
+ return void 0;
3710
+ }
3711
+ const previous = acc.previousTotalUsage;
3712
+ acc.previousTotalUsage = total;
3713
+ if (previous === void 0 || sameCodexUsage(previous, total)) {
3714
+ return void 0;
3715
+ }
3716
+ return positiveIncrement(cumulativeCodexDelta(total, previous));
3717
+ }
3718
+ function resolvedUsageIncrement(acc, last, total) {
3719
+ if (total !== void 0) {
3720
+ const previous = acc.previousTotalUsage;
3721
+ if (previous !== void 0 && sameCodexUsage(previous, total)) {
2729
3722
  return void 0;
2730
3723
  }
2731
- const increment2 = last === void 0 ? cumulativeDelta(normalizedTotal, acc.previousTotalUsage) : normalizeUsage(last);
2732
- acc.previousTotalUsage = normalizedTotal;
2733
- return positiveUsage(increment2) ? increment2 : void 0;
3724
+ acc.previousTotalUsage = total;
3725
+ acc.remainingInheritedUsage = void 0;
3726
+ return positiveIncrement(cumulativeCodexDelta(total, previous));
2734
3727
  }
2735
3728
  if (last === void 0) {
2736
3729
  return void 0;
2737
3730
  }
2738
- const increment = normalizeUsage(last);
2739
- return positiveUsage(increment) ? increment : void 0;
3731
+ const rawIncrement = normalizeCodexUsage(last);
3732
+ return positiveIncrement(subtractRemainingInheritedUsage(acc, rawIncrement));
3733
+ }
3734
+ function containedUsageIncrement(acc, last, total) {
3735
+ const { containment } = acc;
3736
+ if (containment === void 0) {
3737
+ return void 0;
3738
+ }
3739
+ const normalizedTotal = total === void 0 ? void 0 : normalizeCodexUsage(total);
3740
+ const normalizedLast = last === void 0 ? void 0 : normalizeCodexUsage(last);
3741
+ if (acc.lineageKind === "Unresolved") {
3742
+ return applyCodexUnresolvedSnapshot(containment, normalizedLast, normalizedTotal);
3743
+ }
3744
+ if (acc.lineageKind === "Resolved") {
3745
+ const adjustedTotal = normalizedTotal === void 0 || acc.inheritedBaselineUsage === void 0 ? normalizedTotal : subtractCodexUsageFloorZero(normalizedTotal, acc.inheritedBaselineUsage);
3746
+ const adjustedLast = normalizedTotal === void 0 && normalizedLast !== void 0 ? subtractRemainingInheritedUsage(acc, normalizedLast) : normalizedLast;
3747
+ const increment = applyCodexContainedSnapshot(containment, adjustedLast, adjustedTotal, {
3748
+ totalOnlyBeforeInterleaving: true
3749
+ });
3750
+ if (normalizedTotal !== void 0) {
3751
+ acc.remainingInheritedUsage = void 0;
3752
+ }
3753
+ return increment;
3754
+ }
3755
+ return applyCodexContainedSnapshot(containment, normalizedLast, normalizedTotal);
3756
+ }
3757
+ function rootUsageIncrement(acc, last, total) {
3758
+ if (total !== void 0) {
3759
+ const previous = acc.previousTotalUsage;
3760
+ if (previous !== void 0 && sameCodexUsage(previous, total)) {
3761
+ return void 0;
3762
+ }
3763
+ const increment = last === void 0 ? cumulativeCodexDelta(total, previous) : normalizeCodexUsage(last);
3764
+ acc.previousTotalUsage = total;
3765
+ return positiveIncrement(increment);
3766
+ }
3767
+ return last === void 0 ? void 0 : positiveIncrement(normalizeCodexUsage(last));
3768
+ }
3769
+ function usageIncrement(acc, last, total) {
3770
+ if (acc.containment !== void 0) {
3771
+ return containedUsageIncrement(acc, last, total);
3772
+ }
3773
+ const normalizedTotal = total === void 0 ? void 0 : normalizeCodexUsage(total);
3774
+ if (acc.lineageKind === "Resolved") {
3775
+ return resolvedUsageIncrement(acc, last, normalizedTotal);
3776
+ }
3777
+ if (acc.lineageKind === "Unresolved") {
3778
+ return unresolvedUsageIncrement(acc, normalizedTotal);
3779
+ }
3780
+ return rootUsageIncrement(acc, last, normalizedTotal);
2740
3781
  }
2741
3782
  function addToBucket(acc, hourUtc, model, delta) {
2742
3783
  const { inputTokens, outputTokens, cacheReadTokens } = delta;
@@ -2761,11 +3802,16 @@ function addToBucket(acc, hourUtc, model, delta) {
2761
3802
  existing.cacheReadTokens += cacheReadTokens;
2762
3803
  return true;
2763
3804
  }
3805
+ function recordUsageCandidate(acc, usage, model) {
3806
+ if (!addToBucket(acc, startOfUtcHour(usage.eventAt), model, usage.usage)) {
3807
+ return;
3808
+ }
3809
+ acc.candidates.push({ ...usage, model });
3810
+ markSessionSeen(acc, usage.eventAt);
3811
+ }
2764
3812
  function flushPendingUsage(acc, model) {
2765
3813
  for (const pending of acc.pendingUsage) {
2766
- if (addToBucket(acc, startOfUtcHour(pending.eventAt), model, pending.usage)) {
2767
- markSessionSeen(acc, pending.eventAt);
2768
- }
3814
+ recordUsageCandidate(acc, pending, model);
2769
3815
  }
2770
3816
  acc.pendingUsage = [];
2771
3817
  }
@@ -2798,6 +3844,7 @@ function accumulateCodexLine(context, acc, raw) {
2798
3844
  if (shouldExcludePath(parsed.payload.cwd, options.excludePath)) {
2799
3845
  acc.excluded = true;
2800
3846
  acc.buckets.clear();
3847
+ acc.candidates = [];
2801
3848
  acc.pendingUsage = [];
2802
3849
  }
2803
3850
  return acc;
@@ -2806,6 +3853,7 @@ function accumulateCodexLine(context, acc, raw) {
2806
3853
  if (shouldExcludePath(parsed.payload.cwd, options.excludePath)) {
2807
3854
  acc.excluded = true;
2808
3855
  acc.buckets.clear();
3856
+ acc.candidates = [];
2809
3857
  acc.pendingUsage = [];
2810
3858
  return acc;
2811
3859
  }
@@ -2817,6 +3865,10 @@ function accumulateCodexLine(context, acc, raw) {
2817
3865
  }
2818
3866
  return acc;
2819
3867
  }
3868
+ if (isCodexTaskStartedEvent(parsed)) {
3869
+ acc.currentTurnId = pickCodexTurnId(parsed);
3870
+ return acc;
3871
+ }
2820
3872
  if (!isCodexTokenCountEvent(parsed)) {
2821
3873
  return acc;
2822
3874
  }
@@ -2830,24 +3882,25 @@ function accumulateCodexLine(context, acc, raw) {
2830
3882
  return acc;
2831
3883
  }
2832
3884
  const eventAt = new Date(tsMs);
3885
+ const eventIndex = acc.nextEventIndex;
3886
+ acc.nextEventIndex += 1;
3887
+ const turnId = pickCodexTurnId(parsed) ?? acc.currentTurnId;
3888
+ const candidate = { eventAt, eventIndex, turnId, usage: delta };
2833
3889
  const eventModel = pickCodexEventModel(parsed);
2834
3890
  const model = acc.sawTurnContext ? acc.currentModel ?? eventModel : eventModel ?? acc.currentModel;
2835
3891
  if (model === void 0) {
2836
- acc.pendingUsage.push({ eventAt, usage: delta });
3892
+ acc.pendingUsage.push(candidate);
2837
3893
  return acc;
2838
3894
  }
2839
- if (addToBucket(acc, startOfUtcHour(eventAt), model, delta)) {
2840
- markSessionSeen(acc, eventAt);
2841
- }
3895
+ recordUsageCandidate(acc, candidate, model);
2842
3896
  return acc;
2843
3897
  }
2844
- function bucketsFromAccumulator(path, acc) {
3898
+ function bucketsFromAccumulator(path, options, acc) {
2845
3899
  if (acc.excluded || acc.sessionStartedAt === void 0 || acc.sessionLastSeenAt === void 0) {
2846
3900
  return [];
2847
3901
  }
2848
- const sessionStartedAt = acc.sessionStartedAt;
2849
- const sessionLastSeenAt = acc.sessionLastSeenAt;
2850
- const sessionIds = /* @__PURE__ */ new Set([path]);
3902
+ const { sessionLastSeenAt, sessionStartedAt } = acc;
3903
+ const sessionIds = /* @__PURE__ */ new Set([options.sessionIdentity ?? path]);
2851
3904
  return Array.from(acc.buckets.values(), (bucket) => ({
2852
3905
  harness: "codex",
2853
3906
  model: bucket.model,
@@ -2861,13 +3914,37 @@ function bucketsFromAccumulator(path, acc) {
2861
3914
  sessionLastSeenAt
2862
3915
  }));
2863
3916
  }
2864
- function bucketizeCodexFileStream(path, logger, options = {}) {
3917
+ function candidatesFromAccumulator(path, options, acc) {
3918
+ if (acc.excluded || acc.sessionStartedAt === void 0 || acc.sessionLastSeenAt === void 0) {
3919
+ return [];
3920
+ }
3921
+ const sessionIdentity = options.sessionIdentity ?? path;
3922
+ const canonicalSessionIdentity = options.canonicalSessionIdentity ?? sessionIdentity;
3923
+ return acc.candidates.map((candidate) => ({
3924
+ cacheReadTokens: candidate.usage.cacheReadTokens,
3925
+ canonicalSessionIdentity,
3926
+ eventIndex: candidate.eventIndex,
3927
+ hourUtc: startOfUtcHour(candidate.eventAt),
3928
+ inputTokens: candidate.usage.inputTokens,
3929
+ model: candidate.model,
3930
+ outputTokens: candidate.usage.outputTokens,
3931
+ sessionIdentity,
3932
+ sessionLastSeenAt: acc.sessionLastSeenAt,
3933
+ sessionStartedAt: acc.sessionStartedAt,
3934
+ turnId: candidate.turnId
3935
+ }));
3936
+ }
3937
+ function codexFileUsageEffect(path, logger, options = {}) {
2865
3938
  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
- )
3939
+ return lineStreamFromFile(path).pipe(
3940
+ Stream9.runFold(
3941
+ () => makeCodexFileAccumulator(options),
3942
+ (acc, raw) => accumulateCodexLine(context, acc, raw)
3943
+ ),
3944
+ Effect16.map((acc) => ({
3945
+ buckets: bucketsFromAccumulator(path, options, acc),
3946
+ candidates: candidatesFromAccumulator(path, options, acc)
3947
+ }))
2871
3948
  );
2872
3949
  }
2873
3950
 
@@ -2878,6 +3955,12 @@ function isObject2(value) {
2878
3955
  function isFiniteNumber2(value) {
2879
3956
  return typeof value === "number" && Number.isFinite(value);
2880
3957
  }
3958
+ var MAX_CLAUDE_SOURCE_ID_LENGTH = 512;
3959
+ var MAX_CLAUDE_TIMESTAMP_LENGTH = 128;
3960
+ var MAX_CLAUDE_LOCAL_PATH_LENGTH = 4096;
3961
+ function isOptionalBoundedString2(value, maxLength) {
3962
+ return value === void 0 || typeof value === "string" && value.length <= maxLength;
3963
+ }
2881
3964
  function isClaudeUsage(value) {
2882
3965
  if (!isObject2(value)) {
2883
3966
  return false;
@@ -2898,20 +3981,23 @@ function isClaudeAssistantEvent(value) {
2898
3981
  if (value["type"] !== "assistant") {
2899
3982
  return false;
2900
3983
  }
2901
- if (typeof value["timestamp"] !== "string") {
3984
+ if (typeof value["timestamp"] !== "string" || value["timestamp"].length > MAX_CLAUDE_TIMESTAMP_LENGTH) {
2902
3985
  return false;
2903
3986
  }
2904
- if (value["cwd"] !== void 0 && typeof value["cwd"] !== "string") {
3987
+ if (!isOptionalBoundedString2(value["cwd"], MAX_CLAUDE_LOCAL_PATH_LENGTH)) {
2905
3988
  return false;
2906
3989
  }
2907
- if (value["requestId"] !== void 0 && typeof value["requestId"] !== "string") {
3990
+ if (!isOptionalBoundedString2(value["requestId"], MAX_CLAUDE_SOURCE_ID_LENGTH)) {
3991
+ return false;
3992
+ }
3993
+ if (value["isSidechain"] !== void 0 && typeof value["isSidechain"] !== "boolean") {
2908
3994
  return false;
2909
3995
  }
2910
3996
  const message = value["message"];
2911
3997
  if (!isObject2(message)) {
2912
3998
  return false;
2913
3999
  }
2914
- if (message["id"] !== void 0 && typeof message["id"] !== "string") {
4000
+ if (!isOptionalBoundedString2(message["id"], MAX_CLAUDE_SOURCE_ID_LENGTH)) {
2915
4001
  return false;
2916
4002
  }
2917
4003
  if (typeof message["model"] !== "string") {
@@ -2930,23 +4016,19 @@ function key3(hourUtc, model) {
2930
4016
  function shouldExcludePath2(path, excludePath) {
2931
4017
  return path !== void 0 && excludePath?.(path) === true;
2932
4018
  }
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
- };
4019
+ function makeClaudeFileCandidateAccumulator() {
4020
+ return { excluded: false, identified: /* @__PURE__ */ new Map(), lineNo: 0, unidentified: [] };
4021
+ }
4022
+ function makeClaudeSessionAccumulator() {
4023
+ return { buckets: /* @__PURE__ */ new Map(), sessionLastSeenAt: void 0, sessionStartedAt: void 0 };
2942
4024
  }
2943
4025
  function lineStreamFromFile2(path) {
2944
- return Stream9.callback(
2945
- (queue) => Effect16.acquireRelease(
2946
- Effect16.sync(() => {
4026
+ return Stream10.callback(
4027
+ (queue) => Effect17.acquireRelease(
4028
+ Effect17.sync(() => {
2947
4029
  let done = false;
2948
4030
  const stream = createReadStream2(path, { encoding: "utf8" });
2949
- const rl = createInterface3({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
4031
+ const lines = createInterface3({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
2950
4032
  const offer = (line) => {
2951
4033
  if (!done) {
2952
4034
  Queue2.offerUnsafe(queue, line);
@@ -2965,61 +4047,32 @@ function lineStreamFromFile2(path) {
2965
4047
  }
2966
4048
  };
2967
4049
  stream.once("error", fail);
2968
- rl.once("error", fail);
2969
- rl.once("close", end);
2970
- rl.on("line", offer);
4050
+ lines.once("error", fail);
4051
+ lines.once("close", end);
4052
+ lines.on("line", offer);
2971
4053
  return {
2972
4054
  close: () => {
2973
4055
  done = true;
2974
- rl.off("close", end);
2975
- rl.off("error", fail);
2976
- rl.off("line", offer);
4056
+ lines.off("close", end);
4057
+ lines.off("error", fail);
4058
+ lines.off("line", offer);
2977
4059
  stream.off("error", fail);
2978
- rl.close();
2979
- stream.destroy();
2980
- }
2981
- };
2982
- }),
2983
- (resource) => Effect16.sync(() => resource.close())
2984
- )
2985
- );
2986
- }
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) {
2993
- return false;
2994
- }
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;
4060
+ lines.close();
4061
+ stream.destroy();
4062
+ }
4063
+ };
4064
+ }),
4065
+ (resource) => Effect17.sync(() => resource.close())
4066
+ )
4067
+ );
3013
4068
  }
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;
4069
+ function eventIsEligible(timestampMs3, options) {
4070
+ if (options.cutoff !== void 0 && timestampMs3 < options.cutoff.getTime()) {
4071
+ return false;
3020
4072
  }
4073
+ return options.endExclusive === void 0 || timestampMs3 < options.endExclusive.getTime();
3021
4074
  }
3022
- function accumulateClaudeLine(context, acc, raw) {
4075
+ function accumulateClaudeCandidate(context, acc, raw) {
3023
4076
  const { logger, options, path } = context;
3024
4077
  acc.lineNo += 1;
3025
4078
  if (acc.excluded) {
@@ -3041,37 +4094,129 @@ function accumulateClaudeLine(context, acc, raw) {
3041
4094
  }
3042
4095
  if (shouldExcludePath2(parsed.cwd, options.excludePath)) {
3043
4096
  acc.excluded = true;
3044
- acc.buckets.clear();
4097
+ acc.identified.clear();
4098
+ acc.unidentified = [];
3045
4099
  return acc;
3046
4100
  }
3047
- const tsMs = Date.parse(parsed.timestamp);
3048
- if (!Number.isFinite(tsMs)) {
4101
+ const timestampMs3 = Date.parse(parsed.timestamp);
4102
+ if (!(Number.isFinite(timestampMs3) && eventIsEligible(timestampMs3, options))) {
3049
4103
  return acc;
3050
4104
  }
3051
- if (options.cutoff !== void 0 && tsMs < options.cutoff.getTime()) {
3052
- return acc;
4105
+ const tupleKey = parsed.message.id === void 0 || parsed.requestId === void 0 ? void 0 : JSON.stringify([parsed.message.id, parsed.requestId]);
4106
+ const candidate = {
4107
+ eventAt: new Date(timestampMs3),
4108
+ eventOrder: acc.lineNo,
4109
+ isSidechain: parsed.isSidechain === true,
4110
+ model: parsed.message.model,
4111
+ pathRole: options.pathRole ?? "parent",
4112
+ sessionId: options.sessionId ?? path,
4113
+ sourceOrder: normalize3(path),
4114
+ tupleKey,
4115
+ usage: parsed.message.usage
4116
+ };
4117
+ if (tupleKey === void 0) {
4118
+ acc.unidentified.push(candidate);
4119
+ } else {
4120
+ acc.identified.set(tupleKey, candidate);
3053
4121
  }
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;
4122
+ return acc;
4123
+ }
4124
+ function candidatesFromAccumulator2(acc) {
4125
+ return acc.excluded ? [] : [...acc.identified.values(), ...acc.unidentified];
4126
+ }
4127
+ function collectClaudeFileCandidates(path, logger, options = {}) {
4128
+ const context = { logger, options, path };
4129
+ return lineStreamFromFile2(path).pipe(
4130
+ Stream10.runFold(makeClaudeFileCandidateAccumulator, (acc, raw) => accumulateClaudeCandidate(context, acc, raw)),
4131
+ Effect17.map(candidatesFromAccumulator2)
4132
+ );
4133
+ }
4134
+ function normalizedUsage(usage) {
4135
+ return {
4136
+ inputTokens: Math.max(0, usage.input_tokens ?? 0),
4137
+ outputTokens: Math.max(0, usage.output_tokens ?? 0),
4138
+ cacheReadTokens: Math.max(0, usage.cache_read_input_tokens ?? 0),
4139
+ cacheCreateTokens: Math.max(0, usage.cache_creation_input_tokens ?? 0)
4140
+ };
4141
+ }
4142
+ function sameUsage(left, right) {
4143
+ const a = normalizedUsage(left);
4144
+ const b = normalizedUsage(right);
4145
+ return a.inputTokens === b.inputTokens && a.outputTokens === b.outputTokens && a.cacheReadTokens === b.cacheReadTokens && a.cacheCreateTokens === b.cacheCreateTokens;
4146
+ }
4147
+ function candidateWins(next, current) {
4148
+ if (next.model === current.model && sameUsage(next.usage, current.usage)) {
4149
+ if (next.isSidechain !== current.isSidechain) {
4150
+ return !next.isSidechain;
4151
+ }
4152
+ if (next.pathRole !== current.pathRole) {
4153
+ return next.pathRole === "parent";
3059
4154
  }
3060
- acc.seenTuples.add(tuple);
3061
4155
  }
3062
- const eventAt = new Date(tsMs);
3063
- if (addToBucket2(acc, startOfUtcHour(eventAt), message.model, message.usage)) {
3064
- markSessionSeen2(acc, eventAt);
4156
+ const timestampOrder = next.eventAt.getTime() - current.eventAt.getTime();
4157
+ if (timestampOrder !== 0) {
4158
+ return timestampOrder > 0;
3065
4159
  }
3066
- return acc;
4160
+ if (next.eventOrder !== current.eventOrder) {
4161
+ return next.eventOrder > current.eventOrder;
4162
+ }
4163
+ return next.sourceOrder.localeCompare(current.sourceOrder) < 0;
3067
4164
  }
3068
- function bucketsFromAccumulator2(path, options, acc) {
3069
- if (acc.excluded || acc.sessionStartedAt === void 0 || acc.sessionLastSeenAt === void 0) {
4165
+ function reconcileClaudeCandidates(candidates) {
4166
+ const identified = /* @__PURE__ */ new Map();
4167
+ const unidentified = [];
4168
+ for (const candidate of candidates) {
4169
+ if (candidate.tupleKey === void 0) {
4170
+ unidentified.push(candidate);
4171
+ continue;
4172
+ }
4173
+ const current = identified.get(candidate.tupleKey);
4174
+ if (current === void 0 || candidateWins(candidate, current)) {
4175
+ identified.set(candidate.tupleKey, candidate);
4176
+ }
4177
+ }
4178
+ return [...identified.values(), ...unidentified];
4179
+ }
4180
+ function addToBucket2(acc, candidate) {
4181
+ const usage = normalizedUsage(candidate.usage);
4182
+ const { cacheCreateTokens, cacheReadTokens, inputTokens, outputTokens } = usage;
4183
+ if (inputTokens === 0 && outputTokens === 0 && cacheReadTokens === 0 && cacheCreateTokens === 0) {
4184
+ return false;
4185
+ }
4186
+ const hourUtc = startOfUtcHour(candidate.eventAt);
4187
+ const bucketKey = key3(hourUtc, candidate.model);
4188
+ const existing = acc.buckets.get(bucketKey);
4189
+ if (existing === void 0) {
4190
+ acc.buckets.set(bucketKey, {
4191
+ hourUtc,
4192
+ model: candidate.model,
4193
+ inputTokens,
4194
+ outputTokens,
4195
+ cacheReadTokens,
4196
+ cacheCreateTokens
4197
+ });
4198
+ return true;
4199
+ }
4200
+ existing.inputTokens += inputTokens;
4201
+ existing.outputTokens += outputTokens;
4202
+ existing.cacheReadTokens += cacheReadTokens;
4203
+ existing.cacheCreateTokens += cacheCreateTokens;
4204
+ return true;
4205
+ }
4206
+ function markSessionSeen2(acc, eventAt) {
4207
+ if (acc.sessionStartedAt === void 0 || eventAt.getTime() < acc.sessionStartedAt.getTime()) {
4208
+ acc.sessionStartedAt = eventAt;
4209
+ }
4210
+ if (acc.sessionLastSeenAt === void 0 || eventAt.getTime() > acc.sessionLastSeenAt.getTime()) {
4211
+ acc.sessionLastSeenAt = eventAt;
4212
+ }
4213
+ }
4214
+ function bucketsForSession(sessionId, acc) {
4215
+ const { sessionLastSeenAt, sessionStartedAt } = acc;
4216
+ if (sessionStartedAt === void 0 || sessionLastSeenAt === void 0) {
3070
4217
  return [];
3071
4218
  }
3072
- const sessionStartedAt = acc.sessionStartedAt;
3073
- const sessionLastSeenAt = acc.sessionLastSeenAt;
3074
- const sessionIds = /* @__PURE__ */ new Set([options.sessionId ?? path]);
4219
+ const sessionIds = /* @__PURE__ */ new Set([sessionId]);
3075
4220
  return Array.from(acc.buckets.values(), (bucket) => ({
3076
4221
  harness: "claude_code",
3077
4222
  model: bucket.model,
@@ -3085,70 +4230,80 @@ function bucketsFromAccumulator2(path, options, acc) {
3085
4230
  sessionLastSeenAt
3086
4231
  }));
3087
4232
  }
4233
+ function bucketizeClaudeCandidates(candidates) {
4234
+ const sessions = /* @__PURE__ */ new Map();
4235
+ for (const candidate of reconcileClaudeCandidates(candidates)) {
4236
+ const acc = sessions.get(candidate.sessionId) ?? makeClaudeSessionAccumulator();
4237
+ if (addToBucket2(acc, candidate)) {
4238
+ markSessionSeen2(acc, candidate.eventAt);
4239
+ }
4240
+ sessions.set(candidate.sessionId, acc);
4241
+ }
4242
+ return [...sessions.entries()].flatMap(([sessionId, acc]) => bucketsForSession(sessionId, acc));
4243
+ }
3088
4244
  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)))
4245
+ return Stream10.unwrap(
4246
+ collectClaudeFileCandidates(path, logger, options).pipe(
4247
+ Effect17.map((candidates) => Stream10.fromIterable(bucketizeClaudeCandidates(candidates)))
3097
4248
  )
3098
4249
  );
3099
4250
  }
3100
4251
 
3101
4252
  // ../../packages/parsers/src/claude/index.ts
3102
- function sourceSessionId(path) {
4253
+ function sourceContext(path) {
3103
4254
  const fileDir = dirname2(path);
3104
4255
  if (basename(fileDir) !== "subagents") {
3105
- return path;
4256
+ return { pathRole: "parent", sessionId: path };
3106
4257
  }
3107
4258
  const sessionDir = dirname2(fileDir);
3108
- return `${sessionDir}.jsonl`;
4259
+ return { pathRole: "subagent", sessionId: `${sessionDir}.jsonl` };
3109
4260
  }
3110
4261
  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
4262
+ return Stream11.unwrap(
4263
+ files.pipe(
4264
+ Stream11.runCollect,
4265
+ Effect18.map((paths) => [...paths].sort()),
4266
+ Effect18.flatMap(
4267
+ (paths) => Effect18.forEach(paths, (path) => {
4268
+ const source = sourceContext(path);
4269
+ return collectClaudeFileCandidates(path, logger, {
4270
+ ...options,
4271
+ pathRole: source.pathRole,
4272
+ sessionId: source.sessionId
3122
4273
  });
3123
- },
3124
- { concurrency: 1 }
3125
- )
3126
- );
3127
- });
4274
+ })
4275
+ ),
4276
+ Effect18.map((filesCandidates) => Stream11.fromIterable(bucketizeClaudeCandidates(filesCandidates.flat())))
4277
+ )
4278
+ );
3128
4279
  }
3129
- var parseClaudeEffect = Effect17.fn("ClaudeParser.parseClaudeEffect")(
3130
- (opts) => Stream10.runCollect(parseClaudeStream(opts))
4280
+ var parseClaudeEffect = Effect18.fn("ClaudeParser.parseClaudeEffect")(
4281
+ (opts) => Stream11.runCollect(parseClaudeStream(opts))
3131
4282
  );
3132
- var parseClaudeStream = (opts) => Stream10.unwrap(
3133
- Effect17.gen(function* () {
4283
+ var parseClaudeStream = (opts) => Stream11.unwrap(
4284
+ Effect18.gen(function* () {
3134
4285
  const logger = opts.logger ?? silentLogger;
3135
4286
  const root = opts.root ?? resolveClaudeRoot();
3136
4287
  if ((yield* pathKindEffectStrict(root)) === "file") {
4288
+ const source = sourceContext(root);
3137
4289
  return bucketizeClaudeFileStream(root, logger, {
3138
4290
  cutoff: opts.cutoff,
4291
+ endExclusive: opts.endExclusive,
3139
4292
  excludePath: opts.excludePath,
3140
- sessionId: sourceSessionId(root)
4293
+ pathRole: source.pathRole,
4294
+ sessionId: source.sessionId
3141
4295
  });
3142
4296
  }
3143
4297
  return bucketizeClaudeSourcesStream(walkStreamStrict(root, opts.cutoff), logger, {
3144
4298
  cutoff: opts.cutoff,
4299
+ endExclusive: opts.endExclusive,
3145
4300
  excludePath: opts.excludePath
3146
4301
  });
3147
4302
  })
3148
4303
  );
3149
4304
 
3150
4305
  // ../../packages/parsers/src/codex/index.ts
3151
- import { Effect as Effect18, Stream as Stream11 } from "effect";
4306
+ import { Effect as Effect19, Stream as Stream12 } from "effect";
3152
4307
  var codexRoots = (opts, root) => {
3153
4308
  const roots = [root];
3154
4309
  if (opts.root === void 0 && opts.archivedRoot === void 0) {
@@ -3158,53 +4313,135 @@ var codexRoots = (opts, root) => {
3158
4313
  }
3159
4314
  return roots;
3160
4315
  };
3161
- var parseCodexEffect = Effect18.fn("CodexParser.parseCodexEffect")(
3162
- (opts) => Stream11.runCollect(parseCodexStream(opts))
4316
+ function candidateIdentity(candidate) {
4317
+ return [
4318
+ candidate.canonicalSessionIdentity,
4319
+ candidate.turnId ?? "",
4320
+ String(candidate.eventIndex),
4321
+ candidate.hourUtc.toISOString().slice(0, 10),
4322
+ candidate.model,
4323
+ String(candidate.inputTokens),
4324
+ String(candidate.cacheReadTokens),
4325
+ String(candidate.outputTokens)
4326
+ ].join("");
4327
+ }
4328
+ function reconcileCodexCandidates(candidates) {
4329
+ const seen = /* @__PURE__ */ new Set();
4330
+ const winners = [];
4331
+ for (const candidate of candidates) {
4332
+ const identity = candidateIdentity(candidate);
4333
+ if (seen.has(identity)) {
4334
+ continue;
4335
+ }
4336
+ seen.add(identity);
4337
+ winners.push(candidate);
4338
+ }
4339
+ return winners;
4340
+ }
4341
+ function bucketsFromCodexCandidates(candidates) {
4342
+ const buckets = /* @__PURE__ */ new Map();
4343
+ for (const candidate of candidates) {
4344
+ const identity = `${candidate.sessionIdentity}${candidate.hourUtc.toISOString()}${candidate.model}`;
4345
+ const existing = buckets.get(identity);
4346
+ if (existing === void 0) {
4347
+ buckets.set(identity, {
4348
+ cacheReadTokens: candidate.cacheReadTokens,
4349
+ hourUtc: candidate.hourUtc,
4350
+ inputTokens: candidate.inputTokens,
4351
+ model: candidate.model,
4352
+ outputTokens: candidate.outputTokens,
4353
+ sessionIdentity: candidate.sessionIdentity,
4354
+ sessionLastSeenAt: candidate.sessionLastSeenAt,
4355
+ sessionStartedAt: candidate.sessionStartedAt
4356
+ });
4357
+ continue;
4358
+ }
4359
+ existing.inputTokens += candidate.inputTokens;
4360
+ existing.outputTokens += candidate.outputTokens;
4361
+ existing.cacheReadTokens += candidate.cacheReadTokens;
4362
+ }
4363
+ return [...buckets.values()].map((bucket) => ({
4364
+ harness: "codex",
4365
+ model: bucket.model,
4366
+ hourUtc: bucket.hourUtc,
4367
+ inputTokens: bucket.inputTokens,
4368
+ outputTokens: bucket.outputTokens,
4369
+ cacheReadTokens: bucket.cacheReadTokens,
4370
+ cacheCreateTokens: 0,
4371
+ sessionIds: /* @__PURE__ */ new Set([bucket.sessionIdentity]),
4372
+ sessionStartedAt: bucket.sessionStartedAt,
4373
+ sessionLastSeenAt: bucket.sessionLastSeenAt
4374
+ }));
4375
+ }
4376
+ var parseCodexEffect = Effect19.fn("CodexParser.parseCodexEffect")(
4377
+ (opts) => Stream12.runCollect(parseCodexStream(opts))
3163
4378
  );
3164
- var parseCodexStream = (opts) => Stream11.unwrap(
3165
- Effect18.gen(function* () {
4379
+ var parseCodexStream = (opts) => Stream12.unwrap(
4380
+ Effect19.gen(function* () {
3166
4381
  const logger = opts.logger ?? silentLogger;
3167
4382
  const root = opts.root ?? resolveCodexSessionsRoot();
3168
4383
  if ((yield* pathKindEffectStrict(root)) === "file") {
3169
- return bucketizeCodexFileStream(root, logger, { excludePath: opts.excludePath });
4384
+ return bucketizeIndexedCodexFilesStream(Stream12.succeed(root), opts.cutoff, logger, {
4385
+ excludePath: opts.excludePath
4386
+ });
3170
4387
  }
3171
- return bucketizeCodexFilesStream(walkManyStreamStrict(codexRoots(opts, root), opts.cutoff), logger, {
3172
- excludePath: opts.excludePath
3173
- });
4388
+ return bucketizeIndexedCodexFilesStream(
4389
+ walkManyStreamStrict(codexRoots(opts, root), /* @__PURE__ */ new Date(0)),
4390
+ opts.cutoff,
4391
+ logger,
4392
+ { excludePath: opts.excludePath }
4393
+ );
3174
4394
  })
3175
4395
  );
3176
- function bucketizeCodexFilesStream(files, logger, options) {
3177
- return files.pipe(Stream11.flatMap((file) => bucketizeCodexFileStream(file, logger, options)));
4396
+ function bucketizeIndexedCodexFilesStream(files, cutoff, logger, options) {
4397
+ return Stream12.unwrap(
4398
+ files.pipe(
4399
+ Stream12.runCollect,
4400
+ Effect19.map((paths) => [...paths]),
4401
+ Effect19.flatMap((paths) => buildCodexSourceIndex(paths, cutoff, logger)),
4402
+ Effect19.flatMap(
4403
+ (index) => Effect19.gen(function* () {
4404
+ const resolver = makeCodexLineageResolver(index, logger);
4405
+ const usages = yield* Effect19.forEach(
4406
+ index.sources.filter((source) => source.eligible),
4407
+ (source) => resolver.baselineFor(source).pipe(
4408
+ Effect19.flatMap(
4409
+ (lineageBaseline) => codexFileUsageEffect(source.path, logger, {
4410
+ ...options,
4411
+ canonicalSessionIdentity: source.lineageSessionId ?? source.sourceIdentity,
4412
+ containInterleavedTotals: source.containsMultipleSessionIds,
4413
+ lineageBaseline,
4414
+ sessionIdentity: source.sourceIdentity
4415
+ })
4416
+ )
4417
+ )
4418
+ );
4419
+ const candidates = usages.flatMap((usage) => usage.candidates);
4420
+ return Stream12.fromIterable(bucketsFromCodexCandidates(reconcileCodexCandidates(candidates)));
4421
+ })
4422
+ )
4423
+ )
4424
+ );
3178
4425
  }
3179
4426
 
3180
4427
  // ../../packages/parsers/src/effect/hermes-sqlite.ts
3181
- import { Context as Context9, Effect as Effect19, Layer as Layer8, Schema as Schema22 } from "effect";
4428
+ import { Context as Context13, Effect as Effect20, Layer as Layer8, Schema as Schema28 } from "effect";
3182
4429
  var NODE_SQLITE_SPECIFIER = ["node", "sqlite"].join(":");
3183
4430
  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
- var HermesSqliteError = class extends Schema22.TaggedErrorClass()("HermesSqliteError", {
3201
- message: Schema22.String,
3202
- operation: Schema22.Literals(["load", "open", "query", "close"]),
3203
- path: Schema22.optionalKey(Schema22.String),
3204
- cause: Schema22.optionalKey(Schema22.Defect())
4431
+ var HermesSqliteError = class extends Schema28.TaggedErrorClass()("HermesSqliteError", {
4432
+ message: Schema28.String,
4433
+ operation: Schema28.Literals(["close", "load", "open", "query", "schema"]),
4434
+ path: Schema28.optionalKey(Schema28.String),
4435
+ cause: Schema28.optionalKey(Schema28.Defect())
3205
4436
  }) {
3206
4437
  };
3207
- var HermesSqlite = class extends Context9.Service()("@oopst/parsers/HermesSqlite") {
4438
+ function unsupportedSchema(message) {
4439
+ return { _tag: "UnsupportedHermesSchema", message };
4440
+ }
4441
+ function isUnsupportedSchema(value) {
4442
+ return typeof value === "object" && value !== null && "_tag" in value && value._tag === "UnsupportedHermesSchema";
4443
+ }
4444
+ var HermesSqlite = class extends Context13.Service()("@oopst/parsers/HermesSqlite") {
3208
4445
  };
3209
4446
  var toHermesSqliteError = (operation, path) => (error) => new HermesSqliteError({
3210
4447
  message: `HermesSqlite.${operation} failed: ${error instanceof Error ? error.message : String(error)}`,
@@ -3212,34 +4449,124 @@ var toHermesSqliteError = (operation, path) => (error) => new HermesSqliteError(
3212
4449
  path,
3213
4450
  cause: error
3214
4451
  });
3215
- var loadDatabaseSync = Effect19.fn("HermesSqlite.loadDatabaseSync")(function* () {
3216
- const sqliteModule = yield* Effect19.tryPromise({
4452
+ var loadDatabaseSync = Effect20.fn("HermesSqlite.loadDatabaseSync")(function* () {
4453
+ const sqliteModule = yield* Effect20.tryPromise({
3217
4454
  try: () => import(NODE_SQLITE_SPECIFIER),
3218
4455
  catch: toHermesSqliteError("load")
3219
4456
  });
3220
4457
  return sqliteModule.DatabaseSync;
3221
4458
  });
3222
- var openDatabase = (path, DatabaseSync) => Effect19.try({
4459
+ var openDatabase = (path, DatabaseSync) => Effect20.try({
3223
4460
  try: () => new DatabaseSync(path, { readOnly: true, timeout: 1e3 }),
3224
4461
  catch: toHermesSqliteError("open", path)
3225
4462
  });
3226
- var closeDatabase = (path, db) => Effect19.try({
4463
+ var closeDatabase = (path, db) => Effect20.try({
3227
4464
  try: () => {
3228
4465
  db.close();
3229
4466
  },
3230
4467
  catch: toHermesSqliteError("close", path)
3231
4468
  });
3232
- var querySessionRows = (path, db, cutoff) => Effect19.try({
4469
+ function tableNames(db) {
4470
+ const rows = db.prepare("select name from sqlite_master where type = 'table'").all();
4471
+ return new Set(rows.flatMap((row) => typeof row.name === "string" ? [row.name] : []));
4472
+ }
4473
+ function tableColumns(db, table) {
4474
+ const rows = db.prepare(`pragma table_info(${table})`).all();
4475
+ return new Set(rows.flatMap((row) => typeof row.name === "string" ? [row.name] : []));
4476
+ }
4477
+ function requireColumns(table, columns, required) {
4478
+ const missing = required.filter((column) => !columns.has(column));
4479
+ if (missing.length > 0) {
4480
+ throw unsupportedSchema(`${table} is missing required accounting columns`);
4481
+ }
4482
+ }
4483
+ function selectedColumn(columns, column, alias, qualifier) {
4484
+ const source = qualifier === void 0 ? column : `${qualifier}.${column}`;
4485
+ return columns.has(column) ? `${source} as "${alias}"` : `null as "${alias}"`;
4486
+ }
4487
+ function sessionRows(db, cutoffSeconds) {
4488
+ const columns = tableColumns(db, "sessions");
4489
+ requireColumns("sessions", columns, ["id", "model", "started_at", "ended_at", "input_tokens", "output_tokens"]);
4490
+ const query = `
4491
+ select
4492
+ id,
4493
+ model,
4494
+ ${selectedColumn(columns, "billing_provider", "billingProvider")},
4495
+ started_at as "startedAt",
4496
+ ended_at as "endedAt",
4497
+ input_tokens as "inputTokens",
4498
+ output_tokens as "outputTokens",
4499
+ ${selectedColumn(columns, "cache_read_tokens", "cacheReadTokens")},
4500
+ ${selectedColumn(columns, "cache_write_tokens", "cacheWriteTokens")},
4501
+ ${selectedColumn(columns, "reasoning_tokens", "reasoningTokens")},
4502
+ ${selectedColumn(columns, "cwd", "cwd")}
4503
+ from sessions
4504
+ where ended_at is null or ended_at >= ?
4505
+ order by started_at asc, id asc
4506
+ `;
4507
+ return db.prepare(query).all(cutoffSeconds);
4508
+ }
4509
+ function modelUsageRows(db, cutoffSeconds) {
4510
+ const columns = tableColumns(db, "session_model_usage");
4511
+ requireColumns("session_model_usage", columns, ["session_id", "model", "input_tokens", "output_tokens"]);
4512
+ const query = `
4513
+ select
4514
+ usage.session_id as "sessionId",
4515
+ usage.model,
4516
+ ${selectedColumn(columns, "billing_provider", "billingProvider", "usage")},
4517
+ ${selectedColumn(columns, "first_seen", "firstSeen", "usage")},
4518
+ ${selectedColumn(columns, "last_seen", "lastSeen", "usage")},
4519
+ usage.input_tokens as "inputTokens",
4520
+ usage.output_tokens as "outputTokens",
4521
+ ${selectedColumn(columns, "cache_read_tokens", "cacheReadTokens", "usage")},
4522
+ ${selectedColumn(columns, "cache_write_tokens", "cacheWriteTokens", "usage")},
4523
+ ${selectedColumn(columns, "reasoning_tokens", "reasoningTokens", "usage")}
4524
+ from session_model_usage as usage
4525
+ inner join sessions on sessions.id = usage.session_id
4526
+ where sessions.ended_at is null or sessions.ended_at >= ?
4527
+ order by usage.session_id asc, usage.model asc
4528
+ `;
4529
+ return db.prepare(query).all(cutoffSeconds);
4530
+ }
4531
+ function attachModelUsageRows(sessions, usageRows) {
4532
+ const bySession = /* @__PURE__ */ new Map();
4533
+ for (const row of usageRows) {
4534
+ if (typeof row.sessionId !== "string") {
4535
+ continue;
4536
+ }
4537
+ const rows = bySession.get(row.sessionId) ?? [];
4538
+ rows.push(row);
4539
+ bySession.set(row.sessionId, rows);
4540
+ }
4541
+ return sessions.map((row) => ({
4542
+ ...row,
4543
+ modelUsageRows: typeof row.id === "string" ? bySession.get(row.id) ?? [] : []
4544
+ }));
4545
+ }
4546
+ var querySessionRows = (path, db, cutoff) => Effect20.try({
3233
4547
  try: () => {
3234
- const statement = db.prepare(HERMES_SESSION_QUERY);
3235
- return statement.all(cutoff.getTime() / MILLISECONDS_PER_SECOND);
4548
+ const tables = tableNames(db);
4549
+ if (!tables.has("sessions")) {
4550
+ throw unsupportedSchema("sessions table is unavailable");
4551
+ }
4552
+ const cutoffSeconds = cutoff.getTime() / MILLISECONDS_PER_SECOND;
4553
+ const sessions = sessionRows(db, cutoffSeconds);
4554
+ if (!tables.has("session_model_usage")) {
4555
+ return sessions;
4556
+ }
4557
+ return attachModelUsageRows(sessions, modelUsageRows(db, cutoffSeconds));
3236
4558
  },
3237
- catch: toHermesSqliteError("query", path)
4559
+ catch: (error) => isUnsupportedSchema(error) ? new HermesSqliteError({
4560
+ message: "Hermes SQLite schema does not expose supported accounting aggregates",
4561
+ operation: "schema",
4562
+ path,
4563
+ cause: error
4564
+ }) : toHermesSqliteError("query", path)(error)
3238
4565
  });
3239
4566
  var makeHermesSqlite = () => ({
3240
- readSessionRows: Effect19.fn("HermesSqlite.readSessionRows")(function* (path, cutoff) {
4567
+ readSessionRows: Effect20.fn("HermesSqlite.readSessionRows")(function* (path, cutoff) {
3241
4568
  const DatabaseSync = yield* loadDatabaseSync();
3242
- return yield* Effect19.acquireUseRelease(
4569
+ return yield* Effect20.acquireUseRelease(
3243
4570
  openDatabase(path, DatabaseSync),
3244
4571
  (db) => querySessionRows(path, db, cutoff),
3245
4572
  (db) => closeDatabase(path, db)
@@ -3248,15 +4575,21 @@ var makeHermesSqlite = () => ({
3248
4575
  });
3249
4576
  var HermesSqliteLive = Layer8.effect(
3250
4577
  HermesSqlite,
3251
- Effect19.sync(() => HermesSqlite.of(makeHermesSqlite()))
4578
+ Effect20.sync(() => HermesSqlite.of(makeHermesSqlite()))
3252
4579
  );
3253
4580
 
3254
4581
  // ../../packages/parsers/src/effect/opencode-io.ts
3255
4582
  import { basename as basename2, join as join6 } from "path";
3256
4583
  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";
4584
+ import { Context as Context14, Effect as Effect21, FileSystem as FileSystem6, Layer as Layer9, Option as Option3, Schema as Schema29, Stream as Stream13 } from "effect";
3258
4585
  var NODE_SQLITE_SPECIFIER2 = ["node", "sqlite"].join(":");
3259
4586
  var OPENCODE_DB_FILE_PATTERN2 = /^opencode(?:-[^/]+)?\.db$/u;
4587
+ var MESSAGE_QUERY = `
4588
+ select session_id as "sessionId", time_created as "timeCreated", data
4589
+ from message
4590
+ where time_created >= ?
4591
+ order by time_created asc, id asc
4592
+ `;
3260
4593
  var SESSION_MESSAGE_QUERY = `
3261
4594
  select session_id as "sessionId", time_created as "timeCreated", data
3262
4595
  from session_message
@@ -3271,14 +4604,14 @@ var SESSION_PATH_QUERIES = [
3271
4604
  join project p on p.id = s.project_id
3272
4605
  where p.worktree is not null`
3273
4606
  ];
3274
- var OpenCodeIoError = class extends Schema23.TaggedErrorClass()("OpenCodeIoError", {
3275
- message: Schema23.String,
3276
- operation: Schema23.Literals(["stat", "readdir", "readFile", "parseJson", "load", "open", "query", "close"]),
3277
- path: Schema23.optionalKey(Schema23.String),
3278
- cause: Schema23.optionalKey(Schema23.Defect())
4607
+ var OpenCodeIoError = class extends Schema29.TaggedErrorClass()("OpenCodeIoError", {
4608
+ message: Schema29.String,
4609
+ operation: Schema29.Literals(["stat", "readdir", "readFile", "parseJson", "load", "open", "query", "close"]),
4610
+ path: Schema29.optionalKey(Schema29.String),
4611
+ cause: Schema29.optionalKey(Schema29.Defect())
3279
4612
  }) {
3280
4613
  };
3281
- var OpenCodeIo = class extends Context10.Service()("@oopst/parsers/OpenCodeIo") {
4614
+ var OpenCodeIo = class extends Context14.Service()("@oopst/parsers/OpenCodeIo") {
3282
4615
  };
3283
4616
  var toOpenCodeIoError = (operation, path) => (error) => new OpenCodeIoError({
3284
4617
  message: `OpenCodeIo.${operation} failed: ${error instanceof Error ? error.message : String(error)}`,
@@ -3286,13 +4619,13 @@ var toOpenCodeIoError = (operation, path) => (error) => new OpenCodeIoError({
3286
4619
  path,
3287
4620
  cause: error
3288
4621
  });
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) {
4622
+ var readDir2 = (fs, path) => fs.readDirectory(path).pipe(Effect21.mapError(toOpenCodeIoError("readdir", path)));
4623
+ var readDirBestEffort = (fs, path) => readDir2(fs, path).pipe(Effect21.catch(() => Effect21.succeed([])));
4624
+ var readStat2 = (fs, path) => fs.stat(path).pipe(Effect21.mapError(toOpenCodeIoError("stat", path)));
4625
+ var readStatBestEffort = (fs, path) => readStat2(fs, path).pipe(Effect21.catch(() => Effect21.succeed(void 0)));
4626
+ var pathKind = Effect21.fn("OpenCodeIo.pathKind")(function* (fs, path) {
3294
4627
  return yield* readStat2(fs, path).pipe(
3295
- Effect20.map((info) => {
4628
+ Effect21.map((info) => {
3296
4629
  if (info.type === "File") {
3297
4630
  return "file";
3298
4631
  }
@@ -3301,10 +4634,10 @@ var pathKind = Effect20.fn("OpenCodeIo.pathKind")(function* (fs, path) {
3301
4634
  }
3302
4635
  return "missing";
3303
4636
  }),
3304
- Effect20.catch(() => Effect20.succeed("missing"))
4637
+ Effect21.catch(() => Effect21.succeed("missing"))
3305
4638
  );
3306
4639
  });
3307
- var listOpenCodeDatabases = Effect20.fn("OpenCodeIo.listOpenCodeDatabases")(function* (fs, root) {
4640
+ var listOpenCodeDatabases = Effect21.fn("OpenCodeIo.listOpenCodeDatabases")(function* (fs, root) {
3308
4641
  const entries = yield* readDirBestEffort(fs, root);
3309
4642
  const databasePaths = [];
3310
4643
  for (const entry of entries) {
@@ -3318,7 +4651,7 @@ var listOpenCodeDatabases = Effect20.fn("OpenCodeIo.listOpenCodeDatabases")(func
3318
4651
  }
3319
4652
  return databasePaths;
3320
4653
  });
3321
- var listProjectLegacyRoots = Effect20.fn("OpenCodeIo.listProjectLegacyRoots")(function* (fs, root) {
4654
+ var listProjectLegacyRoots = Effect21.fn("OpenCodeIo.listProjectLegacyRoots")(function* (fs, root) {
3322
4655
  const projectRoot = join6(root, "project");
3323
4656
  const entries = yield* readDirBestEffort(fs, projectRoot);
3324
4657
  const roots = [];
@@ -3330,7 +4663,7 @@ var listProjectLegacyRoots = Effect20.fn("OpenCodeIo.listProjectLegacyRoots")(fu
3330
4663
  }
3331
4664
  return roots;
3332
4665
  });
3333
- var legacyRoots = Effect20.fn("OpenCodeIo.legacyRoots")(function* (fs, root) {
4666
+ var legacyRoots = Effect21.fn("OpenCodeIo.legacyRoots")(function* (fs, root) {
3334
4667
  return [
3335
4668
  join6(root, "storage", "message"),
3336
4669
  join6(root, "storage", "session", "message"),
@@ -3341,26 +4674,26 @@ var isRecent = (info, cutoff) => Option3.match(info.mtime, {
3341
4674
  onNone: () => false,
3342
4675
  onSome: (mtime) => mtime.getTime() >= cutoff.getTime()
3343
4676
  });
3344
- var streamJsonFiles = (fs, root, cutoff) => Stream12.suspend(
3345
- () => Stream12.fromIterableEffect(readDirBestEffort(fs, root)).pipe(
3346
- Stream12.flatMap((entry) => {
4677
+ var streamJsonFiles = (fs, root, cutoff) => Stream13.suspend(
4678
+ () => Stream13.fromIterableEffect(readDirBestEffort(fs, root)).pipe(
4679
+ Stream13.flatMap((entry) => {
3347
4680
  const full = join6(root, entry);
3348
- return Stream12.fromEffect(readStatBestEffort(fs, full)).pipe(
3349
- Stream12.flatMap((info) => {
4681
+ return Stream13.fromEffect(readStatBestEffort(fs, full)).pipe(
4682
+ Stream13.flatMap((info) => {
3350
4683
  if (info?.type === "Directory") {
3351
4684
  return streamJsonFiles(fs, full, cutoff);
3352
4685
  }
3353
4686
  if (info?.type === "File" && entry.endsWith(".json") && isRecent(info, cutoff)) {
3354
- return Stream12.succeed(full);
4687
+ return Stream13.succeed(full);
3355
4688
  }
3356
- return Stream12.empty;
4689
+ return Stream13.empty;
3357
4690
  })
3358
4691
  );
3359
4692
  })
3360
4693
  )
3361
4694
  );
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) {
4695
+ var streamManyJsonFiles = (fs, roots, cutoff) => Stream13.fromIterable(roots).pipe(Stream13.flatMap((root) => streamJsonFiles(fs, root, cutoff)));
4696
+ var discoverSources = Effect21.fn("OpenCodeIo.discoverSources")(function* (fs, root, cutoff) {
3364
4697
  const kind = yield* pathKind(fs, root);
3365
4698
  if (kind === "missing") {
3366
4699
  return { _tag: "Missing" };
@@ -3379,26 +4712,26 @@ var discoverSources = Effect20.fn("OpenCodeIo.discoverSources")(function* (fs, r
3379
4712
  return { _tag: "LegacyJsonFiles", paths: streamManyJsonFiles(fs, roots, cutoff) };
3380
4713
  });
3381
4714
  var readJsonFile = (fs, path) => fs.readFileString(path, "utf8").pipe(
3382
- Effect20.mapError(toOpenCodeIoError("readFile", path)),
3383
- Effect20.flatMap(
3384
- (text) => Effect20.try({
4715
+ Effect21.mapError(toOpenCodeIoError("readFile", path)),
4716
+ Effect21.flatMap(
4717
+ (text) => Effect21.try({
3385
4718
  try: () => JSON.parse(text),
3386
4719
  catch: toOpenCodeIoError("parseJson", path)
3387
4720
  })
3388
4721
  )
3389
4722
  );
3390
- var loadDatabaseSync2 = Effect20.fn("OpenCodeIo.loadDatabaseSync")(function* () {
3391
- const sqliteModule = yield* Effect20.tryPromise({
4723
+ var loadDatabaseSync2 = Effect21.fn("OpenCodeIo.loadDatabaseSync")(function* () {
4724
+ const sqliteModule = yield* Effect21.tryPromise({
3392
4725
  try: () => import(NODE_SQLITE_SPECIFIER2),
3393
4726
  catch: toOpenCodeIoError("load")
3394
4727
  });
3395
4728
  return sqliteModule.DatabaseSync;
3396
4729
  });
3397
- var openDatabase2 = (path, DatabaseSync) => Effect20.try({
4730
+ var openDatabase2 = (path, DatabaseSync) => Effect21.try({
3398
4731
  try: () => new DatabaseSync(path, { readOnly: true, timeout: 1e3 }),
3399
4732
  catch: toOpenCodeIoError("open", path)
3400
4733
  });
3401
- var closeDatabase2 = (path, db) => Effect20.try({
4734
+ var closeDatabase2 = (path, db) => Effect21.try({
3402
4735
  try: () => {
3403
4736
  db.close();
3404
4737
  },
@@ -3423,19 +4756,43 @@ function readSessionPaths(db) {
3423
4756
  }
3424
4757
  return paths;
3425
4758
  }
3426
- var querySqliteRows = (path, db, cutoff) => Effect20.try({
3427
- try: () => {
3428
- const statement = db.prepare(SESSION_MESSAGE_QUERY);
3429
- return {
3430
- sessionMessages: statement.all(cutoff.getTime()),
3431
- sessionPaths: readSessionPaths(db)
3432
- };
3433
- },
4759
+ function queryMessageRows(db, sql, cutoff) {
4760
+ return db.prepare(sql).all(cutoff.getTime());
4761
+ }
4762
+ function isMissingTableError(error, table) {
4763
+ return error instanceof Error && error.message.includes(`no such table: ${table}`);
4764
+ }
4765
+ function readOpenCodeMessages(db, cutoff) {
4766
+ let messages;
4767
+ try {
4768
+ messages = queryMessageRows(db, MESSAGE_QUERY, cutoff);
4769
+ if (messages.length > 0) {
4770
+ return messages;
4771
+ }
4772
+ } catch (error) {
4773
+ if (!isMissingTableError(error, "message")) {
4774
+ throw error;
4775
+ }
4776
+ }
4777
+ try {
4778
+ return queryMessageRows(db, SESSION_MESSAGE_QUERY, cutoff);
4779
+ } catch (error) {
4780
+ if (messages !== void 0 && isMissingTableError(error, "session_message")) {
4781
+ return messages;
4782
+ }
4783
+ throw error;
4784
+ }
4785
+ }
4786
+ var querySqliteRows = (path, db, cutoff) => Effect21.try({
4787
+ try: () => ({
4788
+ sessionMessages: readOpenCodeMessages(db, cutoff),
4789
+ sessionPaths: readSessionPaths(db)
4790
+ }),
3434
4791
  catch: toOpenCodeIoError("query", path)
3435
4792
  });
3436
- var readSqlite = Effect20.fn("OpenCodeIo.readSqlite")(function* (path, cutoff) {
4793
+ var readSqlite = Effect21.fn("OpenCodeIo.readSqlite")(function* (path, cutoff) {
3437
4794
  const DatabaseSync = yield* loadDatabaseSync2();
3438
- return yield* Effect20.acquireUseRelease(
4795
+ return yield* Effect21.acquireUseRelease(
3439
4796
  openDatabase2(path, DatabaseSync),
3440
4797
  (db) => querySqliteRows(path, db, cutoff),
3441
4798
  (db) => closeDatabase2(path, db)
@@ -3448,17 +4805,17 @@ var makeOpenCodeIo = (fs) => ({
3448
4805
  });
3449
4806
  var OpenCodeIoLive = Layer9.effect(
3450
4807
  OpenCodeIo,
3451
- Effect20.gen(function* () {
4808
+ Effect21.gen(function* () {
3452
4809
  const fs = yield* FileSystem6.FileSystem;
3453
4810
  return OpenCodeIo.of(makeOpenCodeIo(fs));
3454
4811
  })
3455
4812
  ).pipe(Layer9.provide(NodeFileSystem5.layer));
3456
4813
 
3457
4814
  // ../../packages/parsers/src/opencode/index.ts
3458
- import { Effect as Effect22, Stream as Stream13 } from "effect";
4815
+ import { Effect as Effect23, Stream as Stream14 } from "effect";
3459
4816
 
3460
4817
  // ../../packages/parsers/src/opencode/bucketize.ts
3461
- import { Effect as Effect21 } from "effect";
4818
+ import { Effect as Effect22 } from "effect";
3462
4819
 
3463
4820
  // ../../packages/parsers/src/opencode/events.ts
3464
4821
  function isObject3(value) {
@@ -3679,7 +5036,7 @@ function* finalizedBuckets(accumulators) {
3679
5036
  function parseJson(value) {
3680
5037
  return typeof value === "string" ? JSON.parse(value) : value;
3681
5038
  }
3682
- var bucketizeOpenCodeDatabaseEffect = Effect21.fn("bucketizeOpenCodeDatabaseEffect")(
5039
+ var bucketizeOpenCodeDatabaseEffect = Effect22.fn("bucketizeOpenCodeDatabaseEffect")(
3683
5040
  function* (path, cutoff, logger, excludePath) {
3684
5041
  const io = yield* OpenCodeIo;
3685
5042
  const rows = yield* io.readSqlite(path, cutoff);
@@ -3702,7 +5059,7 @@ var bucketizeOpenCodeDatabaseEffect = Effect21.fn("bucketizeOpenCodeDatabaseEffe
3702
5059
  return [...finalizedBuckets(accumulators)];
3703
5060
  }
3704
5061
  );
3705
- var bucketizeOpenCodeJsonFileEffect = Effect21.fn("bucketizeOpenCodeJsonFileEffect")(
5062
+ var bucketizeOpenCodeJsonFileEffect = Effect22.fn("bucketizeOpenCodeJsonFileEffect")(
3706
5063
  function* (path) {
3707
5064
  const io = yield* OpenCodeIo;
3708
5065
  const parsed = yield* io.readJsonFile(path);
@@ -3717,23 +5074,23 @@ var bucketizeOpenCodeJsonFileEffect = Effect21.fn("bucketizeOpenCodeJsonFileEffe
3717
5074
  );
3718
5075
 
3719
5076
  // ../../packages/parsers/src/opencode/index.ts
3720
- var discoverOpenCodeSourcesEffect = Effect22.fn("discoverOpenCodeSourcesEffect")(function* (root, cutoff) {
5077
+ var discoverOpenCodeSourcesEffect = Effect23.fn("discoverOpenCodeSourcesEffect")(function* (root, cutoff) {
3721
5078
  const io = yield* OpenCodeIo;
3722
5079
  return yield* io.discoverSources(root, cutoff);
3723
5080
  });
3724
- var parseOpenCodeEffect = Effect22.fn(
5081
+ var parseOpenCodeEffect = Effect23.fn(
3725
5082
  "OpenCodeParser.parseOpenCodeEffect"
3726
- )((opts) => Stream13.runCollect(parseOpenCodeStream(opts)));
3727
- var parseOpenCodeStream = (opts) => Stream13.unwrap(
3728
- Effect22.gen(function* () {
5083
+ )((opts) => Stream14.runCollect(parseOpenCodeStream(opts)));
5084
+ var parseOpenCodeStream = (opts) => Stream14.unwrap(
5085
+ Effect23.gen(function* () {
3729
5086
  const logger = opts.logger ?? silentLogger;
3730
5087
  const root = opts.root ?? resolveOpenCodeRoot();
3731
5088
  const sources = yield* discoverOpenCodeSourcesEffect(root, opts.cutoff);
3732
5089
  if (sources._tag === "Missing") {
3733
- return Stream13.empty;
5090
+ return Stream14.empty;
3734
5091
  }
3735
5092
  if (sources._tag === "JsonFile") {
3736
- return openCodeJsonFileStreams(Stream13.succeed(sources.path));
5093
+ return openCodeJsonFileStreams(Stream14.succeed(sources.path));
3737
5094
  }
3738
5095
  if (sources._tag === "DatabaseFiles") {
3739
5096
  return openCodeDatabaseStreams(sources.paths, opts, logger);
@@ -3742,28 +5099,28 @@ var parseOpenCodeStream = (opts) => Stream13.unwrap(
3742
5099
  })
3743
5100
  );
3744
5101
  function openCodeDatabaseStreams(paths, opts, logger) {
3745
- let stream = Stream13.empty;
5102
+ let stream = Stream14.empty;
3746
5103
  for (const dbPath of paths) {
3747
5104
  stream = stream.pipe(
3748
- Stream13.concat(
3749
- Stream13.fromIterableEffect(bucketizeOpenCodeDatabaseEffect(dbPath, opts.cutoff, logger, opts.excludePath))
5105
+ Stream14.concat(
5106
+ Stream14.fromIterableEffect(bucketizeOpenCodeDatabaseEffect(dbPath, opts.cutoff, logger, opts.excludePath))
3750
5107
  )
3751
5108
  );
3752
5109
  }
3753
5110
  return stream;
3754
5111
  }
3755
5112
  function openCodeJsonFileStreams(paths) {
3756
- return paths.pipe(Stream13.flatMap((file) => Stream13.fromIterableEffect(bucketizeOpenCodeJsonFileEffect(file))));
5113
+ return paths.pipe(Stream14.flatMap((file) => Stream14.fromIterableEffect(bucketizeOpenCodeJsonFileEffect(file))));
3757
5114
  }
3758
5115
 
3759
5116
  // ../../packages/parsers/src/pi/index.ts
3760
5117
  import { basename as basename3, join as join7 } from "path";
3761
- import { Effect as Effect24, Layer as Layer10, Stream as Stream15 } from "effect";
5118
+ import { Effect as Effect25, Layer as Layer10, Stream as Stream16 } from "effect";
3762
5119
 
3763
5120
  // ../../packages/parsers/src/pi/bucketize.ts
3764
5121
  import { createReadStream as createReadStream3 } from "fs";
3765
5122
  import { createInterface as createInterface4 } from "readline";
3766
- import { Cause as Cause3, Effect as Effect23, Queue as Queue3, Stream as Stream14 } from "effect";
5123
+ import { Cause as Cause3, Effect as Effect24, Queue as Queue3, Stream as Stream15 } from "effect";
3767
5124
 
3768
5125
  // ../../packages/parsers/src/pi/events.ts
3769
5126
  function isObject4(value) {
@@ -3842,9 +5199,9 @@ function key5(hourUtc, model) {
3842
5199
  return `${hourUtc.toISOString()}|${model}`;
3843
5200
  }
3844
5201
  function lineStreamFromFile3(path) {
3845
- return Stream14.callback(
3846
- (queue) => Effect23.acquireRelease(
3847
- Effect23.sync(() => {
5202
+ return Stream15.callback(
5203
+ (queue) => Effect24.acquireRelease(
5204
+ Effect24.sync(() => {
3848
5205
  let done = false;
3849
5206
  const stream = createReadStream3(path, { encoding: "utf8" });
3850
5207
  const rl = createInterface4({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
@@ -3881,7 +5238,7 @@ function lineStreamFromFile3(path) {
3881
5238
  }
3882
5239
  };
3883
5240
  }),
3884
- (resource) => Effect23.sync(() => resource.close())
5241
+ (resource) => Effect24.sync(() => resource.close())
3885
5242
  )
3886
5243
  );
3887
5244
  }
@@ -3899,6 +5256,33 @@ function modelId2(provider, model) {
3899
5256
  }
3900
5257
  return `${trimmedProvider}/${trimmedModel}`;
3901
5258
  }
5259
+ function hermesModelId(provider, model, fallbackProvider) {
5260
+ if (typeof model !== "string") {
5261
+ return void 0;
5262
+ }
5263
+ const trimmedModel = model.trim();
5264
+ if (trimmedModel.length === 0) {
5265
+ return void 0;
5266
+ }
5267
+ const directProvider = typeof provider === "string" ? provider.trim() : "";
5268
+ const fallback = typeof fallbackProvider === "string" ? fallbackProvider.trim() : "";
5269
+ const sourceProvider = directProvider.length > 0 ? directProvider : fallback;
5270
+ if (sourceProvider.length > 0) {
5271
+ const normalizedProvider = normalizeProvider(sourceProvider);
5272
+ if (trimmedModel.startsWith(`${sourceProvider}/`)) {
5273
+ return `${normalizedProvider}/${trimmedModel.slice(sourceProvider.length + 1)}`;
5274
+ }
5275
+ if (trimmedModel.startsWith(`${normalizedProvider}/`)) {
5276
+ return trimmedModel;
5277
+ }
5278
+ return `${normalizedProvider}/${trimmedModel}`;
5279
+ }
5280
+ const separator = trimmedModel.indexOf("/");
5281
+ if (separator <= 0) {
5282
+ return void 0;
5283
+ }
5284
+ return `${normalizeProvider(trimmedModel.slice(0, separator))}/${trimmedModel.slice(separator + 1)}`;
5285
+ }
3902
5286
  function normalizeProvider(provider) {
3903
5287
  return provider === "openai-codex" ? "openai" : provider;
3904
5288
  }
@@ -3915,53 +5299,134 @@ function timestampMs2(value) {
3915
5299
  }
3916
5300
  return Math.abs(n) < UNIX_MILLISECONDS_THRESHOLD ? n * MILLISECONDS_PER_SECOND2 : n;
3917
5301
  }
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;
5302
+ function normalizeHermesUsage(row) {
5303
+ return {
5304
+ inputTokens: positive2(row.inputTokens),
5305
+ outputTokens: positive2(row.outputTokens),
5306
+ cacheReadTokens: positive2(row.cacheReadTokens),
5307
+ cacheCreateTokens: positive2(row.cacheWriteTokens)
5308
+ };
5309
+ }
5310
+ function addHermesUsage(left, right) {
5311
+ return {
5312
+ inputTokens: left.inputTokens + right.inputTokens,
5313
+ outputTokens: left.outputTokens + right.outputTokens,
5314
+ cacheReadTokens: left.cacheReadTokens + right.cacheReadTokens,
5315
+ cacheCreateTokens: left.cacheCreateTokens + right.cacheCreateTokens
5316
+ };
5317
+ }
5318
+ function subtractHermesUsageFloorZero(current, baseline) {
5319
+ return {
5320
+ inputTokens: Math.max(0, current.inputTokens - baseline.inputTokens),
5321
+ outputTokens: Math.max(0, current.outputTokens - baseline.outputTokens),
5322
+ cacheReadTokens: Math.max(0, current.cacheReadTokens - baseline.cacheReadTokens),
5323
+ cacheCreateTokens: Math.max(0, current.cacheCreateTokens - baseline.cacheCreateTokens)
5324
+ };
5325
+ }
5326
+ function hasHermesUsage(usage) {
5327
+ return usage.inputTokens > 0 || usage.outputTokens > 0 || usage.cacheReadTokens > 0 || usage.cacheCreateTokens > 0;
5328
+ }
5329
+ function maxModelLastSeen(rows) {
5330
+ let latest;
5331
+ for (const row of rows) {
5332
+ const at = timestampMs2(row.lastSeen) ?? timestampMs2(row.firstSeen);
5333
+ if (at !== void 0 && (latest === void 0 || at > latest)) {
5334
+ latest = at;
5335
+ }
3933
5336
  }
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) {
5337
+ return latest;
5338
+ }
5339
+ function makeHermesBucket(options) {
5340
+ if (!hasHermesUsage(options.usage)) {
3939
5341
  return void 0;
3940
5342
  }
3941
- const startedAt = new Date(startedAtMs);
5343
+ const startedAt = new Date(options.startedAtMs);
3942
5344
  return {
3943
5345
  harness: "hermes",
3944
- model,
3945
- hourUtc: startOfUtcHour(startedAt),
3946
- inputTokens,
3947
- outputTokens,
3948
- cacheReadTokens,
3949
- cacheCreateTokens,
3950
- sessionIds: /* @__PURE__ */ new Set([`hermes:${sessionId}`]),
5346
+ model: options.model,
5347
+ hourUtc: startOfUtcHour(new Date(options.attributedAtMs)),
5348
+ inputTokens: options.usage.inputTokens,
5349
+ outputTokens: options.usage.outputTokens,
5350
+ cacheReadTokens: options.usage.cacheReadTokens,
5351
+ cacheCreateTokens: options.usage.cacheCreateTokens,
5352
+ sessionIds: /* @__PURE__ */ new Set([`hermes:${options.sessionId}`]),
3951
5353
  sessionStartedAt: startedAt,
3952
- sessionLastSeenAt: new Date(lastSeenAtMs)
5354
+ sessionLastSeenAt: new Date(options.lastSeenAtMs)
3953
5355
  };
3954
5356
  }
3955
- function bucketizeHermesRows(rows, excludePath) {
5357
+ function bucketizeHermesModelUsage(options) {
3956
5358
  const buckets = [];
3957
- for (const row of rows) {
3958
- const bucket = hermesRowToBucket(row, excludePath);
5359
+ let attributedUsage = {
5360
+ cacheCreateTokens: 0,
5361
+ cacheReadTokens: 0,
5362
+ inputTokens: 0,
5363
+ outputTokens: 0
5364
+ };
5365
+ for (const usageRow of options.rows) {
5366
+ const model = hermesModelId(usageRow.billingProvider, usageRow.model, options.fallbackProvider);
5367
+ if (model === void 0) {
5368
+ continue;
5369
+ }
5370
+ const usage = normalizeHermesUsage(usageRow);
5371
+ const bucket = makeHermesBucket({
5372
+ attributedAtMs: timestampMs2(usageRow.firstSeen) ?? options.startedAtMs,
5373
+ lastSeenAtMs: options.lastSeenAtMs,
5374
+ model,
5375
+ sessionId: options.sessionId,
5376
+ startedAtMs: options.startedAtMs,
5377
+ usage
5378
+ });
3959
5379
  if (bucket !== void 0) {
3960
5380
  buckets.push(bucket);
5381
+ attributedUsage = addHermesUsage(attributedUsage, usage);
5382
+ }
5383
+ }
5384
+ return { attributedUsage, buckets };
5385
+ }
5386
+ function hermesRowToBuckets(row, excludePath) {
5387
+ if (typeof row.cwd === "string" && excludePath?.(row.cwd) === true) {
5388
+ return [];
5389
+ }
5390
+ const sessionId = typeof row.id === "string" ? row.id : void 0;
5391
+ const startedAtMs = timestampMs2(row.startedAt);
5392
+ if (sessionId === void 0 || startedAtMs === void 0) {
5393
+ return [];
5394
+ }
5395
+ const modelUsageRows2 = row.modelUsageRows ?? [];
5396
+ const lastSeenAtMs = timestampMs2(row.endedAt) ?? maxModelLastSeen(modelUsageRows2) ?? startedAtMs;
5397
+ const modelUsage = bucketizeHermesModelUsage({
5398
+ fallbackProvider: row.billingProvider,
5399
+ lastSeenAtMs,
5400
+ rows: modelUsageRows2,
5401
+ sessionId,
5402
+ startedAtMs
5403
+ });
5404
+ const buckets = [...modelUsage.buckets];
5405
+ const aggregateUsage = normalizeHermesUsage(row);
5406
+ const residualUsage = subtractHermesUsageFloorZero(aggregateUsage, modelUsage.attributedUsage);
5407
+ const residualModel = hermesModelId(row.billingProvider, row.model);
5408
+ if (residualModel !== void 0) {
5409
+ const residual = makeHermesBucket({
5410
+ attributedAtMs: startedAtMs,
5411
+ lastSeenAtMs,
5412
+ model: residualModel,
5413
+ sessionId,
5414
+ startedAtMs,
5415
+ usage: residualUsage
5416
+ });
5417
+ if (residual !== void 0) {
5418
+ buckets.push(residual);
3961
5419
  }
3962
5420
  }
3963
5421
  return buckets;
3964
5422
  }
5423
+ function bucketizeHermesRows(rows, excludePath) {
5424
+ const buckets = [];
5425
+ for (const row of rows) {
5426
+ buckets.push(...hermesRowToBuckets(row, excludePath));
5427
+ }
5428
+ return buckets;
5429
+ }
3965
5430
  function makePiStyleFileAccumulator(options) {
3966
5431
  return {
3967
5432
  buckets: /* @__PURE__ */ new Map(),
@@ -4054,12 +5519,11 @@ function accumulatePiStyleLine(context, acc, raw) {
4054
5519
  existing.cacheCreateTokens += cacheCreateTokens;
4055
5520
  return acc;
4056
5521
  }
4057
- function bucketsFromAccumulator3(path, harness, acc) {
5522
+ function bucketsFromAccumulator2(path, harness, acc) {
4058
5523
  if (acc.excluded || acc.sessionStartedAt === void 0 || acc.sessionLastSeenAt === void 0) {
4059
5524
  return [];
4060
5525
  }
4061
- const sessionStartedAt = acc.sessionStartedAt;
4062
- const sessionLastSeenAt = acc.sessionLastSeenAt;
5526
+ const { sessionLastSeenAt, sessionStartedAt } = acc;
4063
5527
  const sessionIds = /* @__PURE__ */ new Set([path]);
4064
5528
  return Array.from(acc.buckets.values(), (bucket) => ({
4065
5529
  harness,
@@ -4076,17 +5540,17 @@ function bucketsFromAccumulator3(path, harness, acc) {
4076
5540
  }
4077
5541
  function bucketizePiStyleFileStream(path, harness, logger, options = {}) {
4078
5542
  const context = { logger, options, path };
4079
- return Stream14.unwrap(
5543
+ return Stream15.unwrap(
4080
5544
  lineStreamFromFile3(path).pipe(
4081
- Stream14.runFold(
5545
+ Stream15.runFold(
4082
5546
  () => makePiStyleFileAccumulator(options),
4083
5547
  (acc, raw) => accumulatePiStyleLine(context, acc, raw)
4084
5548
  ),
4085
- Effect23.map((acc) => Stream14.fromIterable(bucketsFromAccumulator3(path, harness, acc)))
5549
+ Effect24.map((acc) => Stream15.fromIterable(bucketsFromAccumulator2(path, harness, acc)))
4086
5550
  )
4087
5551
  );
4088
5552
  }
4089
- var bucketizeHermesDatabaseEffect = Effect23.fn(
5553
+ var bucketizeHermesDatabaseEffect = Effect24.fn(
4090
5554
  "bucketizeHermesDatabaseEffect"
4091
5555
  )(function* (path, cutoff, excludePath) {
4092
5556
  const sqlite = yield* HermesSqlite;
@@ -4095,14 +5559,14 @@ var bucketizeHermesDatabaseEffect = Effect23.fn(
4095
5559
  });
4096
5560
 
4097
5561
  // ../../packages/parsers/src/pi/index.ts
4098
- var isFileBestEffortEffect = (path) => pathKindEffect(path).pipe(Effect24.map((kind) => kind === "file"));
5562
+ var isFileBestEffortEffect = (path) => pathKindEffect(path).pipe(Effect25.map((kind) => kind === "file"));
4099
5563
  function bucketizePiStyleSourcesStream(files, harness, logger, options) {
4100
5564
  return files.pipe(
4101
- Stream15.flatMap((file) => bucketizePiStyleFileStream(file, harness, logger, options), { concurrency: 1 })
5565
+ Stream16.flatMap((file) => bucketizePiStyleFileStream(file, harness, logger, options), { concurrency: 1 })
4102
5566
  );
4103
5567
  }
4104
- var parsePiStyleStream = (opts, harness, defaultRoot) => Stream15.unwrap(
4105
- Effect24.gen(function* () {
5568
+ var parsePiStyleStream = (opts, harness, defaultRoot) => Stream16.unwrap(
5569
+ Effect25.gen(function* () {
4106
5570
  const logger = opts.logger ?? silentLogger;
4107
5571
  const root = opts.root ?? defaultRoot();
4108
5572
  const kind = yield* pathKindEffectStrict(root);
@@ -4120,25 +5584,23 @@ var parsePiStyleStream = (opts, harness, defaultRoot) => Stream15.unwrap(
4120
5584
  });
4121
5585
  })
4122
5586
  );
4123
- var parsePiEffect = Effect24.fn("PiParser.parsePiEffect")((opts) => Stream15.runCollect(parsePiStream(opts)));
5587
+ var parsePiEffect = Effect25.fn("PiParser.parsePiEffect")((opts) => Stream16.runCollect(parsePiStream(opts)));
4124
5588
  var parsePiStream = (opts) => parsePiStyleStream(opts, "pi", resolvePiRoot);
4125
5589
  var parserLive = Layer10.mergeAll(FileWalkerLive, HermesSqliteLive);
4126
- var parseHermesEffect = Effect24.fn(
5590
+ var parseHermesEffect = Effect25.fn(
4127
5591
  "HermesParser.parseHermesEffect"
4128
- )((opts) => Stream15.runCollect(parseHermesStream(opts)));
4129
- var parseHermesStream = (opts) => Stream15.unwrap(
4130
- Effect24.gen(function* () {
5592
+ )((opts) => Stream16.runCollect(parseHermesStream(opts)));
5593
+ var parseHermesStream = (opts) => Stream16.unwrap(
5594
+ Effect25.gen(function* () {
4131
5595
  const logger = opts.logger ?? silentLogger;
4132
5596
  const root = opts.root ?? resolveHermesRoot();
4133
5597
  const kind = yield* pathKindEffectStrict(root);
4134
5598
  if (kind === "missing") {
4135
- return asHermesStream(Stream15.empty);
5599
+ return asHermesStream(Stream16.empty);
4136
5600
  }
4137
5601
  if (kind === "file") {
4138
5602
  if (basename3(root).endsWith(".db")) {
4139
- return asHermesStream(
4140
- Stream15.fromIterableEffect(bucketizeHermesDatabaseEffect(root, opts.cutoff, opts.excludePath))
4141
- );
5603
+ return asHermesStream(collectHermesDatabaseStream(root, logger, opts));
4142
5604
  }
4143
5605
  return asHermesStream(
4144
5606
  bucketizePiStyleFileStream(root, "hermes", logger, { cutoff: opts.cutoff, excludePath: opts.excludePath })
@@ -4146,9 +5608,7 @@ var parseHermesStream = (opts) => Stream15.unwrap(
4146
5608
  }
4147
5609
  const stateDb = join7(root, "state.db");
4148
5610
  if (yield* isFileBestEffortEffect(stateDb)) {
4149
- return asHermesStream(
4150
- Stream15.fromIterableEffect(bucketizeHermesDatabaseEffect(stateDb, opts.cutoff, opts.excludePath))
4151
- );
5611
+ return asHermesStream(collectHermesDatabaseStream(stateDb, logger, opts));
4152
5612
  }
4153
5613
  const sessionsRoot2 = join7(root, "sessions");
4154
5614
  if ((yield* pathKindEffect(sessionsRoot2)) === "directory") {
@@ -4164,8 +5624,20 @@ var parseHermesStream = (opts) => Stream15.unwrap(
4164
5624
  function asHermesStream(stream) {
4165
5625
  return stream;
4166
5626
  }
4167
- var collectHermesPiStyleDirectoryStream = (root, logger, opts) => Stream15.unwrap(
4168
- Effect24.sync(() => {
5627
+ function collectHermesDatabaseStream(path, logger, opts) {
5628
+ return Stream16.fromIterableEffect(bucketizeHermesDatabaseEffect(path, opts.cutoff, opts.excludePath)).pipe(
5629
+ Stream16.catchTag("HermesSqliteError", (error) => {
5630
+ if (error.operation !== "schema") {
5631
+ return Stream16.fail(error);
5632
+ }
5633
+ return Stream16.fromEffectDrain(
5634
+ Effect25.sync(() => logger.warn("unsupported hermes sqlite schema", { reason: "unsupported_schema" }))
5635
+ );
5636
+ })
5637
+ );
5638
+ }
5639
+ var collectHermesPiStyleDirectoryStream = (root, logger, opts) => Stream16.unwrap(
5640
+ Effect25.sync(() => {
4169
5641
  const seenMessageIds = /* @__PURE__ */ new Set();
4170
5642
  return bucketizePiStyleSourcesStream(walkStreamStrict(root, opts.cutoff), "hermes", logger, {
4171
5643
  cutoff: opts.cutoff,
@@ -4176,7 +5648,7 @@ var collectHermesPiStyleDirectoryStream = (root, logger, opts) => Stream15.unwra
4176
5648
  );
4177
5649
 
4178
5650
  // src/sync/parser-factory.ts
4179
- import { Effect as Effect25, Layer as Layer11, Stream as Stream16 } from "effect";
5651
+ import { Effect as Effect26, Layer as Layer11, Stream as Stream17 } from "effect";
4180
5652
  var GLOB_PATTERN = /[*?[\]{}]/u;
4181
5653
  var HermesParserLive = Layer11.mergeAll(FileWalkerLive, HermesSqliteLive);
4182
5654
  function sessionsRoot(agentDir, sessionDir, fallbackAgentDir) {
@@ -4241,7 +5713,7 @@ function createExcludeMatcher(options) {
4241
5713
  };
4242
5714
  }
4243
5715
  function resolveParserSourcePlanEffect(options = {}) {
4244
- return Effect25.gen(function* () {
5716
+ return Effect26.gen(function* () {
4245
5717
  const sources = yield* ParserSources;
4246
5718
  const home = yield* sources.homeDir;
4247
5719
  const env = options.env ?? (yield* sources.cliEnv);
@@ -4280,49 +5752,43 @@ function resolveParserSourcePlanEffect(options = {}) {
4280
5752
  });
4281
5753
  }
4282
5754
  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));
5755
+ return stream.pipe(Stream17.provide(FileWalkerLive));
5756
+ }
5757
+ function codexStreamForPlan(plan, cutoff, excludePath) {
5758
+ if (plan.codexSessionsSourceExisted) {
5759
+ return provideFileWalker(
5760
+ parseCodexStream({
5761
+ archivedRoot: plan.codexArchivedSourceExisted ? plan.codexArchivedRoot : void 0,
5762
+ cutoff,
5763
+ excludePath,
5764
+ root: plan.codexSessionsRoot
5765
+ })
5766
+ );
4289
5767
  }
4290
- return stream;
5768
+ if (plan.codexArchivedSourceExisted) {
5769
+ return provideFileWalker(
5770
+ parseCodexStream({ cutoff, excludePath, root: plan.codexArchivedRoot })
5771
+ );
5772
+ }
5773
+ return Stream17.empty;
4291
5774
  }
4292
5775
  function buildFactoryFromPlan(plan) {
4293
5776
  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
- };
5777
+ return (ctx) => ({
5778
+ anySourceExisted: plan.anySourceExisted,
5779
+ codex: codexStreamForPlan(plan, ctx.cutoff, excludePath),
5780
+ claudeCode: plan.claudeSourceExisted ? provideFileWalker(parseClaudeStream({ cutoff: ctx.cutoff, excludePath, root: plan.claudeRoot })) : Stream17.empty,
5781
+ pi: plan.piSourceExisted ? provideFileWalker(parsePiStream({ cutoff: ctx.cutoff, excludePath, root: plan.piRoot })) : Stream17.empty,
5782
+ hermes: plan.hermesSourceExisted ? parseHermesStream({ cutoff: ctx.cutoff, excludePath, root: plan.hermesRoot }).pipe(
5783
+ Stream17.provide(HermesParserLive)
5784
+ ) : Stream17.empty,
5785
+ opencode: plan.opencodeSourceExisted ? parseOpenCodeStream({ cutoff: ctx.cutoff, excludePath, root: plan.opencodeRoot }).pipe(
5786
+ Stream17.provide(OpenCodeIoLive)
5787
+ ) : Stream17.empty
5788
+ });
4323
5789
  }
4324
5790
  function buildRealParserFactoryEffect(options = {}) {
4325
- return Effect25.map(resolveParserSourcePlanEffect(options), buildFactoryFromPlan);
5791
+ return Effect26.map(resolveParserSourcePlanEffect(options), buildFactoryFromPlan);
4326
5792
  }
4327
5793
 
4328
5794
  // src/index.ts
@@ -4344,7 +5810,7 @@ function optionalValue(value) {
4344
5810
  return Option4.getOrUndefined(value);
4345
5811
  }
4346
5812
  function isEntrypoint() {
4347
- const [, argvPath] = process4.argv;
5813
+ const [, argvPath] = process2.argv;
4348
5814
  return argvPath ? realpathSync(fileURLToPath2(import.meta.url)) === realpathSync(argvPath) : false;
4349
5815
  }
4350
5816
  function parseHarnessFilter(value) {
@@ -4356,8 +5822,40 @@ function parseHarnessFilter(value) {
4356
5822
  }
4357
5823
  throw new SyncError(EXIT.invalidArguments, `error: --harness must be one of ${SUPPORTED_HARNESSES.join("|")}`);
4358
5824
  }
5825
+ function parseReplacementFlags(raw) {
5826
+ const startHourUtc = optionalValue(raw.replaceStart);
5827
+ const endHourUtc = optionalValue(raw.replaceEnd);
5828
+ const harnessList = optionalValue(raw.replaceHarnesses);
5829
+ if (startHourUtc === void 0 && endHourUtc === void 0 && harnessList === void 0) {
5830
+ if (raw.confirmReplacement) {
5831
+ throw new SyncError(EXIT.invalidArguments, "error: --confirm-replacement requires an atomic replacement scope");
5832
+ }
5833
+ return void 0;
5834
+ }
5835
+ if (!(startHourUtc && endHourUtc && harnessList)) {
5836
+ throw new SyncError(
5837
+ EXIT.invalidArguments,
5838
+ "error: --replace-start, --replace-end, and --replace-harnesses must be provided together"
5839
+ );
5840
+ }
5841
+ const harnesses = harnessList.split(",").map((value) => parseHarnessFilter(value.trim()));
5842
+ if (harnesses.some((harness) => harness === void 0)) {
5843
+ throw new SyncError(EXIT.invalidArguments, "error: replacement harness list cannot be empty");
5844
+ }
5845
+ if (!(raw.dryRun || raw.confirmReplacement)) {
5846
+ throw new SyncError(
5847
+ EXIT.invalidArguments,
5848
+ "error: atomic replacement uploads require --confirm-replacement (use --dry-run to inspect first)"
5849
+ );
5850
+ }
5851
+ return {
5852
+ startHourUtc,
5853
+ endHourUtc,
5854
+ harnesses
5855
+ };
5856
+ }
4359
5857
  function syncCommandErrorExitCode(err) {
4360
- return Effect26.gen(function* () {
5858
+ return Effect27.gen(function* () {
4361
5859
  const console = yield* CliConsole;
4362
5860
  if (err instanceof CliConfigError) {
4363
5861
  yield* console.writeStderrLine(err.message);
@@ -4372,7 +5870,7 @@ function syncCommandErrorExitCode(err) {
4372
5870
  });
4373
5871
  }
4374
5872
  function setupCommandErrorExitCode(err) {
4375
- return Effect26.gen(function* () {
5873
+ return Effect27.gen(function* () {
4376
5874
  const console = yield* CliConsole;
4377
5875
  if (err instanceof CliConfigError || err instanceof SetupError) {
4378
5876
  yield* console.writeStderrLine(err.message);
@@ -4384,26 +5882,27 @@ function setupCommandErrorExitCode(err) {
4384
5882
  }
4385
5883
  function recoverCommandExitCode(effect) {
4386
5884
  return effect.pipe(
4387
- Effect26.catch((err) => syncCommandErrorExitCode(err)),
4388
- Effect26.catchDefect((err) => syncCommandErrorExitCode(err))
5885
+ Effect27.catch((err) => syncCommandErrorExitCode(err)),
5886
+ Effect27.catchDefect((err) => syncCommandErrorExitCode(err))
4389
5887
  );
4390
5888
  }
4391
5889
  function recoverSetupExitCode(effect) {
4392
5890
  return effect.pipe(
4393
- Effect26.catch((err) => setupCommandErrorExitCode(err)),
4394
- Effect26.catchDefect((err) => setupCommandErrorExitCode(err))
5891
+ Effect27.catch((err) => setupCommandErrorExitCode(err)),
5892
+ Effect27.catchDefect((err) => setupCommandErrorExitCode(err))
4395
5893
  );
4396
5894
  }
4397
5895
  function exitCodeToCommandEffect(exitCode) {
4398
- return exitCode === EXIT.success ? Effect26.void : Effect26.fail(new CliExit(exitCode));
5896
+ return exitCode === EXIT.success ? Effect27.void : Effect27.fail(new CliExit(exitCode));
4399
5897
  }
4400
5898
  function runSyncCommand(raw) {
4401
5899
  return recoverCommandExitCode(
4402
- Effect26.gen(function* () {
5900
+ Effect27.gen(function* () {
4403
5901
  const configReader = yield* CliConfigReader;
4404
5902
  const console = yield* CliConsole;
5903
+ const metadata = yield* CliPackageMetadata;
4405
5904
  const env = yield* configReader.read();
4406
- const config = yield* Effect26.try({
5905
+ const config = yield* Effect27.try({
4407
5906
  try: () => resolveSyncConfig(
4408
5907
  {
4409
5908
  api: optionalValue(raw.api),
@@ -4417,10 +5916,14 @@ function runSyncCommand(raw) {
4417
5916
  catch: (err) => err
4418
5917
  });
4419
5918
  const { device, token } = config;
4420
- const harness = yield* Effect26.try({
5919
+ const harness = yield* Effect27.try({
4421
5920
  try: () => parseHarnessFilter(optionalValue(raw.harness)),
4422
5921
  catch: (err) => err
4423
5922
  });
5923
+ const replacement = yield* Effect27.try({
5924
+ try: () => parseReplacementFlags(raw),
5925
+ catch: (err) => err
5926
+ });
4424
5927
  if (!(token && device)) {
4425
5928
  yield* console.writeStderrLine(
4426
5929
  "error: --token and --device are required (or set ~/.oopst.yml / OOPST_TOKEN / OOPST_DEVICE)"
@@ -4428,7 +5931,7 @@ function runSyncCommand(raw) {
4428
5931
  return EXIT.invalidArguments;
4429
5932
  }
4430
5933
  const factory = yield* buildRealParserFactoryEffect({ env, exclude: config.exclude }).pipe(
4431
- Effect26.provide(ParserSourcesLive)
5934
+ Effect27.provide(ParserSourcesLive)
4432
5935
  );
4433
5936
  let sourcesExistedHint = false;
4434
5937
  const parserFactory = (ctx) => {
@@ -4438,10 +5941,12 @@ function runSyncCommand(raw) {
4438
5941
  };
4439
5942
  const result = yield* runSyncProgram2({
4440
5943
  token,
5944
+ cliVersion: metadata.version,
4441
5945
  device,
4442
5946
  serverUrl: config.serverUrl,
4443
5947
  lookbackDays: config.lookbackDays,
4444
5948
  harness,
5949
+ replacement,
4445
5950
  dryRun: raw.dryRun,
4446
5951
  verbose: raw.verbose,
4447
5952
  color: yield* console.stdoutIsTTY,
@@ -4453,16 +5958,16 @@ function runSyncCommand(raw) {
4453
5958
  });
4454
5959
  return result.exitCode;
4455
5960
  })
4456
- ).pipe(Effect26.flatMap(exitCodeToCommandEffect));
5961
+ ).pipe(Effect27.flatMap(exitCodeToCommandEffect));
4457
5962
  }
4458
5963
  function runSetupCommand(raw) {
4459
5964
  return recoverSetupExitCode(
4460
- Effect26.gen(function* () {
5965
+ Effect27.gen(function* () {
4461
5966
  const console = yield* CliConsole;
4462
5967
  const metadata = yield* CliPackageMetadata;
4463
5968
  const cliProcess = yield* CliProcess;
4464
5969
  const nodePath = yield* cliProcess.execPath;
4465
- const result = yield* Effect26.tryPromise({
5970
+ const result = yield* Effect27.tryPromise({
4466
5971
  try: () => runSetup({
4467
5972
  api: optionalValue(raw.api),
4468
5973
  server: optionalValue(raw.server),
@@ -4475,7 +5980,7 @@ function runSetupCommand(raw) {
4475
5980
  });
4476
5981
  return result.exitCode;
4477
5982
  })
4478
- ).pipe(Effect26.flatMap(exitCodeToCommandEffect));
5983
+ ).pipe(Effect27.flatMap(exitCodeToCommandEffect));
4479
5984
  }
4480
5985
  var apiFlag = Flag.string("api").pipe(
4481
5986
  Flag.withDescription("API base URL (or server.api / OOPST_SERVER)"),
@@ -4498,6 +6003,22 @@ var harnessFlag = Flag.string("harness").pipe(
4498
6003
  Flag.withDescription("Restrict to codex|claude_code|pi|hermes|opencode"),
4499
6004
  Flag.optional
4500
6005
  );
6006
+ var replaceStartFlag = Flag.string("replace-start").pipe(
6007
+ Flag.withDescription("Atomic replacement start as an inclusive UTC hour"),
6008
+ Flag.optional
6009
+ );
6010
+ var replaceEndFlag = Flag.string("replace-end").pipe(
6011
+ Flag.withDescription("Atomic replacement end as an exclusive UTC hour"),
6012
+ Flag.optional
6013
+ );
6014
+ var replaceHarnessesFlag = Flag.string("replace-harnesses").pipe(
6015
+ Flag.withDescription("Comma-separated harness set for an atomic replacement"),
6016
+ Flag.optional
6017
+ );
6018
+ var confirmReplacementFlag = Flag.boolean("confirm-replacement").pipe(
6019
+ Flag.withDescription("Confirm that the declared replacement scope may overwrite live facts"),
6020
+ Flag.withDefault(false)
6021
+ );
4501
6022
  var dryRunFlag = Flag.boolean("dry-run").pipe(
4502
6023
  Flag.withDescription("Parse and print without uploading"),
4503
6024
  Flag.withDefault(false)
@@ -4513,6 +6034,10 @@ function buildCli() {
4513
6034
  server: serverFlag,
4514
6035
  lookbackDays: lookbackDaysFlag,
4515
6036
  harness: harnessFlag,
6037
+ replaceStart: replaceStartFlag,
6038
+ replaceEnd: replaceEndFlag,
6039
+ replaceHarnesses: replaceHarnessesFlag,
6040
+ confirmReplacement: confirmReplacementFlag,
4516
6041
  dryRun: dryRunFlag,
4517
6042
  verbose: verboseFlag
4518
6043
  },
@@ -4532,10 +6057,10 @@ function buildCli() {
4532
6057
  );
4533
6058
  }
4534
6059
  function runCli() {
4535
- return Effect26.gen(function* () {
6060
+ return Effect27.gen(function* () {
4536
6061
  const metadata = yield* CliPackageMetadata;
4537
6062
  yield* buildCli().pipe(Command.run({ version: metadata.version }));
4538
- }).pipe(Effect26.provide(CliRuntimeLayer));
6063
+ }).pipe(Effect27.provide(CliRuntimeLayer));
4539
6064
  }
4540
6065
  if (isEntrypoint()) {
4541
6066
  runCli().pipe(NodeRuntime.runMain);