stitchkit 0.99.0 → 0.100.0

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.
Files changed (128) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/agent-runtime/contained-darwin.d.ts +0 -17
  3. package/dist/agent-runtime/contained-darwin.d.ts.map +1 -1
  4. package/dist/agent-runtime/contained-write.d.ts.map +1 -1
  5. package/dist/agent-runtime/invocation-attempt.d.ts +22 -0
  6. package/dist/agent-runtime/invocation-attempt.d.ts.map +1 -0
  7. package/dist/agent-runtime/invocation-failure.d.ts +6 -0
  8. package/dist/agent-runtime/invocation-failure.d.ts.map +1 -0
  9. package/dist/agent-runtime/invocation-payload.d.ts +18 -0
  10. package/dist/agent-runtime/invocation-payload.d.ts.map +1 -0
  11. package/dist/agent-runtime/invocation-read.d.ts +131 -0
  12. package/dist/agent-runtime/invocation-read.d.ts.map +1 -0
  13. package/dist/agent-runtime/invocation-schema.d.ts +378 -0
  14. package/dist/agent-runtime/invocation-schema.d.ts.map +1 -0
  15. package/dist/agent-runtime/invocations.d.ts +46 -0
  16. package/dist/agent-runtime/invocations.d.ts.map +1 -0
  17. package/dist/agent-runtime/run-acquisition.d.ts.map +1 -1
  18. package/dist/agent-runtime/run-execution-state.d.ts +4 -2
  19. package/dist/agent-runtime/run-execution-state.d.ts.map +1 -1
  20. package/dist/agent-runtime/run-execution.d.ts.map +1 -1
  21. package/dist/agent-runtime/run-failure.d.ts.map +1 -1
  22. package/dist/agent-runtime/run-model-attempts.d.ts.map +1 -1
  23. package/dist/agent-runtime/run-operation-lifecycle.d.ts +2 -2
  24. package/dist/agent-runtime/run-operation-lifecycle.d.ts.map +1 -1
  25. package/dist/agent-runtime/run-provider-ledger.d.ts +1 -0
  26. package/dist/agent-runtime/run-provider-ledger.d.ts.map +1 -1
  27. package/dist/agent-runtime/runtime.d.ts +11 -0
  28. package/dist/agent-runtime/runtime.d.ts.map +1 -1
  29. package/dist/agent-runtime/store-create.d.ts.map +1 -1
  30. package/dist/agent-runtime/store-event-once.d.ts +29 -0
  31. package/dist/agent-runtime/store-event-once.d.ts.map +1 -0
  32. package/dist/agent-runtime/store.d.ts +5 -0
  33. package/dist/agent-runtime/store.d.ts.map +1 -1
  34. package/dist/application/change-subscription.d.ts +36 -0
  35. package/dist/application/change-subscription.d.ts.map +1 -0
  36. package/dist/application/diagnostic-journal-contract.d.ts +3 -6
  37. package/dist/application/diagnostic-journal-contract.d.ts.map +1 -1
  38. package/dist/application/notification-delivery.d.ts +32 -0
  39. package/dist/application/notification-delivery.d.ts.map +1 -0
  40. package/dist/application/notification-outbox-state.d.ts +37 -0
  41. package/dist/application/notification-outbox-state.d.ts.map +1 -0
  42. package/dist/application/notification-outbox.d.ts +7 -1
  43. package/dist/application/notification-outbox.d.ts.map +1 -1
  44. package/dist/application/watch-hub-core.d.ts +10 -15
  45. package/dist/application/watch-hub-core.d.ts.map +1 -1
  46. package/dist/application/watch-hub.d.ts +10 -2
  47. package/dist/application/watch-hub.d.ts.map +1 -1
  48. package/dist/browser/session-credentials.d.ts +20 -0
  49. package/dist/browser/session-credentials.d.ts.map +1 -0
  50. package/dist/browser/session.d.ts +31 -0
  51. package/dist/browser/session.d.ts.map +1 -0
  52. package/dist/durability/events.d.ts +6 -0
  53. package/dist/durability/events.d.ts.map +1 -1
  54. package/dist/entrypoints/agent-runtime/coding-tools.js +3 -2
  55. package/dist/entrypoints/agent-runtime/harness.js +5 -3
  56. package/dist/entrypoints/agent-runtime/sandbox.js +3 -2
  57. package/dist/entrypoints/agent-runtime/sqlite/bun.js +3 -3
  58. package/dist/entrypoints/agent-runtime/sqlite/node.js +3 -3
  59. package/dist/entrypoints/agent-runtime/testing.js +3 -3
  60. package/dist/entrypoints/agent-runtime.d.ts +3 -0
  61. package/dist/entrypoints/agent-runtime.d.ts.map +1 -1
  62. package/dist/entrypoints/agent-runtime.js +15 -4
  63. package/dist/entrypoints/application/diagnostic-journal.js +2 -1
  64. package/dist/entrypoints/application.d.ts +3 -1
  65. package/dist/entrypoints/application.d.ts.map +1 -1
  66. package/dist/entrypoints/application.js +629 -189
  67. package/dist/entrypoints/files.js +2 -1
  68. package/dist/entrypoints/index.d.ts +2 -0
  69. package/dist/entrypoints/index.d.ts.map +1 -1
  70. package/dist/entrypoints/index.js +172 -0
  71. package/dist/entrypoints/live.js +68 -13
  72. package/dist/entrypoints/react.d.ts +1 -1
  73. package/dist/entrypoints/react.d.ts.map +1 -1
  74. package/dist/entrypoints/react.js +59 -16
  75. package/dist/{index-wsm5w4ee.js → index-0b1cnze2.js} +35 -3
  76. package/dist/{index-gxrt5h6v.js → index-cyktc4bp.js} +3 -0
  77. package/dist/{index-px2gm2jp.js → index-fhwqwxe0.js} +17 -55
  78. package/dist/{index-e4hkgm5t.js → index-gw8bmyaj.js} +4 -2
  79. package/dist/{index-8p52kd5p.js → index-js7gqrb1.js} +96 -14
  80. package/dist/{index-wt743f7f.js → index-k6gpggb9.js} +2 -1
  81. package/dist/index-q3pyb9n0.js +22 -0
  82. package/dist/{index-sp1tzwnb.js → index-s84he4v7.js} +2 -2
  83. package/dist/{index-jhx828hc.js → index-xwhjjw2x.js} +878 -279
  84. package/dist/{index-1m2thnq0.js → index-y9qn426m.js} +2 -2
  85. package/dist/index-zjjyynm3.js +42 -0
  86. package/dist/internal/coalesced-task.d.ts +15 -0
  87. package/dist/internal/coalesced-task.d.ts.map +1 -0
  88. package/dist/internal/darwin-binding.d.ts +19 -0
  89. package/dist/internal/darwin-binding.d.ts.map +1 -0
  90. package/dist/internal/exclusive-lock.d.ts +20 -7
  91. package/dist/internal/exclusive-lock.d.ts.map +1 -1
  92. package/dist/internal/mutation-queue.d.ts +4 -0
  93. package/dist/internal/mutation-queue.d.ts.map +1 -0
  94. package/dist/internal/process-identity.d.ts +1 -1
  95. package/dist/internal/process-identity.d.ts.map +1 -1
  96. package/dist/internal/process-instance.d.ts +26 -0
  97. package/dist/internal/process-instance.d.ts.map +1 -0
  98. package/dist/live/watch-client-state.d.ts +71 -0
  99. package/dist/live/watch-client-state.d.ts.map +1 -0
  100. package/dist/live/watch-client.d.ts +5 -1
  101. package/dist/live/watch-client.d.ts.map +1 -1
  102. package/dist/live/watch-contract.d.ts +12 -0
  103. package/dist/live/watch-contract.d.ts.map +1 -1
  104. package/dist/react/cache-bridge.d.ts +10 -0
  105. package/dist/react/cache-bridge.d.ts.map +1 -1
  106. package/llms/agent-runtime.2.txt +94 -0
  107. package/llms/agent-runtime.3.txt +24 -0
  108. package/llms/application.2.txt +68 -2
  109. package/llms/application.txt +32 -2
  110. package/llms/files.txt +1 -1
  111. package/llms/live.txt +7 -0
  112. package/llms/react.txt +8 -0
  113. package/llms/server.3.txt +35 -0
  114. package/llms/stitchkit.2.txt +41 -0
  115. package/llms/stitchkit.3.txt +22 -0
  116. package/llms/stitchkit.txt +3 -3
  117. package/llms/upgrading-0.37.0.txt +176 -0
  118. package/llms/{upgrading-0.37.0-0.58.0.txt → upgrading-0.44.0-0.60.0.txt} +229 -169
  119. package/llms/{upgrading-0.59.0-0.71.0.txt → upgrading-0.61.0-0.73.0.txt} +122 -229
  120. package/llms/{upgrading-0.72.0-0.99.0.txt → upgrading-0.74.0-0.100.0.txt} +67 -122
  121. package/llms/upgrading.txt +4 -3
  122. package/llms-full.txt +393 -6
  123. package/llms.txt +10 -9
  124. package/native/darwin-arm64.node +0 -0
  125. package/native/darwin-x64.node +0 -0
  126. package/package.json +1 -1
  127. package/dist/agent-runtime/run-mutation-queue.d.ts +0 -21
  128. package/dist/agent-runtime/run-mutation-queue.d.ts.map +0 -1
@@ -73,7 +73,7 @@ import {
73
73
  deltaWins,
74
74
  diff,
75
75
  watchKeyString
76
- } from "../index-wt743f7f.js";
76
+ } from "../index-k6gpggb9.js";
77
77
  import"../index-1j8pm5t0.js";
78
78
  import {
79
79
  BoundedChannelPolicySchema,
@@ -483,12 +483,317 @@ function createBoundedAdmission(config) {
483
483
  getSnapshot
484
484
  };
485
485
  }
486
- // src/internal/decision.ts
486
+ // src/application/change-subscription.ts
487
487
  import { z as z2 } from "zod";
488
- var PolicyDecisionSchema = z2.discriminatedUnion("outcome", [
489
- z2.object({ outcome: z2.literal("allow") }).strict(),
490
- z2.object({ outcome: z2.literal("deny"), reason: z2.string().min(1) }).strict(),
491
- z2.object({ outcome: z2.literal("defer") }).strict()
488
+
489
+ // src/internal/coalesced-task.ts
490
+ function createCoalescedTask(config) {
491
+ const backoff = createBackoff(config.backoff);
492
+ let running = false;
493
+ let dirty = false;
494
+ let closed = false;
495
+ let retry;
496
+ const pauseRetry = () => {
497
+ clearTimeout(retry);
498
+ retry = undefined;
499
+ };
500
+ const pump = async () => {
501
+ if (running || closed || !config.active() || retry)
502
+ return;
503
+ running = true;
504
+ try {
505
+ while (dirty && !closed && config.active()) {
506
+ dirty = false;
507
+ try {
508
+ await config.run();
509
+ backoff.reset();
510
+ } catch (error) {
511
+ dirty = true;
512
+ if (!closed && config.active()) {
513
+ retry = setTimeout(() => {
514
+ retry = undefined;
515
+ pump();
516
+ }, backoff.next());
517
+ retry.unref?.();
518
+ config.onError(error);
519
+ }
520
+ return;
521
+ }
522
+ }
523
+ } finally {
524
+ running = false;
525
+ }
526
+ };
527
+ return {
528
+ get running() {
529
+ return running;
530
+ },
531
+ trigger() {
532
+ if (closed)
533
+ return;
534
+ dirty = true;
535
+ pump();
536
+ },
537
+ pauseRetry,
538
+ close() {
539
+ closed = true;
540
+ dirty = false;
541
+ pauseRetry();
542
+ }
543
+ };
544
+ }
545
+
546
+ // src/application/change-subscription.ts
547
+ class ChangeSubscription {
548
+ config;
549
+ keys;
550
+ interval;
551
+ timeout;
552
+ backoff;
553
+ tasks;
554
+ failedKeys = new Set;
555
+ lifetime;
556
+ attempt;
557
+ connection;
558
+ opening;
559
+ closing;
560
+ retry;
561
+ periodic;
562
+ context;
563
+ removeAbort;
564
+ constructor(config) {
565
+ this.config = config;
566
+ this.keys = z2.array(z2.string().min(1).max(256)).min(1).max(128).parse(config.keys);
567
+ if (new Set(this.keys).size !== this.keys.length)
568
+ throw new Error("Subscription keys must be unique");
569
+ this.interval = z2.number().int().min(10).max(3600000).parse(config.reconcileIntervalMs);
570
+ this.timeout = z2.number().int().min(1).max(3600000).parse(config.timeoutMs ?? 30000);
571
+ const policy = config.backoff ?? { minDelayMs: 250, maxDelayMs: 30000, jitter: 0.2 };
572
+ this.backoff = createBackoff(policy);
573
+ this.tasks = new Map(this.keys.map((key) => [
574
+ key,
575
+ createCoalescedTask({
576
+ active: () => this.connection !== undefined && this.attempt?.signal.aborted === false,
577
+ run: () => this.read(key),
578
+ onError: (error) => {
579
+ this.failedKeys.add(key);
580
+ this.report(error);
581
+ },
582
+ backoff: policy
583
+ })
584
+ ]));
585
+ }
586
+ report(error) {
587
+ this.context?.reportHealth("degraded");
588
+ try {
589
+ this.config.onError(error);
590
+ } catch {}
591
+ }
592
+ async read(key) {
593
+ const connection = this.connection;
594
+ const attempt = this.attempt;
595
+ if (!connection || !attempt)
596
+ return;
597
+ const readAbort = new AbortController;
598
+ const signal = AbortSignal.any([attempt.signal, readAbort.signal]);
599
+ const work = this.config.reconcile(key, {
600
+ signal,
601
+ commit: (publish) => {
602
+ if (signal.aborted || this.connection !== connection)
603
+ return false;
604
+ publish();
605
+ return true;
606
+ }
607
+ });
608
+ const outcome = await withDeadline(work, this.timeout);
609
+ if (!outcome.settled) {
610
+ readAbort.abort();
611
+ this.report(new Error("Reconciliation deadline exceeded"));
612
+ await work;
613
+ throw new Error("Reconciliation deadline exceeded");
614
+ }
615
+ if (!signal.aborted && this.connection === connection) {
616
+ this.failedKeys.delete(key);
617
+ if (this.failedKeys.size === 0)
618
+ this.context?.reportHealth("healthy");
619
+ }
620
+ }
621
+ reconcile() {
622
+ for (const task of this.tasks.values())
623
+ task.trigger();
624
+ }
625
+ schedule() {
626
+ if (!this.lifetime || this.lifetime.signal.aborted || this.retry)
627
+ return;
628
+ this.retry = setTimeout(() => {
629
+ this.retry = undefined;
630
+ this.open();
631
+ }, this.backoff.next());
632
+ this.retry.unref?.();
633
+ }
634
+ async release(handle) {
635
+ const work = Promise.resolve().then(() => handle.close());
636
+ this.closing = work;
637
+ try {
638
+ const outcome = await withDeadline(work, this.timeout);
639
+ if (!outcome.settled) {
640
+ this.report(new Error("Subscription close deadline exceeded"));
641
+ await work;
642
+ }
643
+ } finally {
644
+ if (this.closing === work)
645
+ this.closing = undefined;
646
+ }
647
+ }
648
+ lost(handle, error) {
649
+ if (this.connection !== handle)
650
+ return;
651
+ this.connection = undefined;
652
+ this.attempt?.abort();
653
+ this.report(error);
654
+ this.release(handle).then(() => this.schedule(), (cause) => this.report(cause));
655
+ }
656
+ open() {
657
+ if (this.opening)
658
+ return this.opening;
659
+ const lifetime = this.lifetime;
660
+ if (!lifetime || lifetime.signal.aborted || this.closing)
661
+ return Promise.resolve();
662
+ this.opening = this.connect(lifetime).finally(() => {
663
+ this.opening = undefined;
664
+ });
665
+ return this.opening;
666
+ }
667
+ async connect(lifetime) {
668
+ const attempt = new AbortController;
669
+ this.attempt = attempt;
670
+ const onAbort = () => attempt.abort();
671
+ lifetime.signal.addEventListener("abort", onAbort, { once: true });
672
+ let handle;
673
+ let lost = false;
674
+ const work = (async () => {
675
+ handle = await this.config.connect(attempt.signal);
676
+ if (attempt.signal.aborted)
677
+ return;
678
+ const owned = handle;
679
+ await owned.subscribe({
680
+ signal: attempt.signal,
681
+ hint: (key) => {
682
+ if (attempt.signal.aborted || lost)
683
+ return;
684
+ const task = this.tasks.get(key);
685
+ if (task)
686
+ task.trigger();
687
+ else
688
+ this.report(new Error("Unknown subscription key"));
689
+ },
690
+ disconnected: (error) => {
691
+ if (attempt.signal.aborted || this.lifetime !== lifetime)
692
+ return;
693
+ lost = true;
694
+ if (this.connection === owned)
695
+ this.lost(owned, error);
696
+ else
697
+ this.report(error);
698
+ }
699
+ });
700
+ })();
701
+ try {
702
+ const outcome = await withDeadline(work, this.timeout);
703
+ if (!outcome.settled) {
704
+ attempt.abort();
705
+ this.report(new Error("Subscription connect deadline exceeded"));
706
+ await work;
707
+ }
708
+ if (attempt.signal.aborted || lifetime !== this.lifetime || lost) {
709
+ if (handle)
710
+ await this.release(handle);
711
+ this.schedule();
712
+ return;
713
+ }
714
+ this.connection = handle;
715
+ this.backoff.reset();
716
+ this.context?.reportHealth("healthy");
717
+ this.reconcile();
718
+ } catch (error) {
719
+ attempt.abort();
720
+ if (handle) {
721
+ try {
722
+ await this.release(handle);
723
+ } catch (cause) {
724
+ this.report(cause);
725
+ return;
726
+ }
727
+ }
728
+ if (!lifetime.signal.aborted) {
729
+ this.report(error);
730
+ this.schedule();
731
+ }
732
+ } finally {
733
+ lifetime.signal.removeEventListener("abort", onAbort);
734
+ }
735
+ }
736
+ start(context) {
737
+ if (this.lifetime && !this.lifetime.signal.aborted)
738
+ throw new Error("Subscription already started");
739
+ if (this.opening || this.closing || [...this.tasks.values()].some((task) => task.running))
740
+ throw new Error("Previous subscription work has not settled");
741
+ this.context = context;
742
+ this.lifetime = new AbortController;
743
+ const abort = () => {
744
+ this.close();
745
+ };
746
+ context.signal.addEventListener("abort", abort, { once: true });
747
+ this.removeAbort = () => context.signal.removeEventListener("abort", abort);
748
+ if (context.signal.aborted) {
749
+ this.close();
750
+ return {};
751
+ }
752
+ this.periodic = setInterval(() => this.reconcile(), this.interval);
753
+ this.periodic.unref?.();
754
+ const ready = (async () => {
755
+ const outcome = await withDeadline(this.open(), this.timeout);
756
+ if (!outcome.settled || !this.connection) {
757
+ this.close().catch((error) => this.report(error));
758
+ throw new Error("Subscription did not become ready before its deadline");
759
+ }
760
+ })();
761
+ return { ready };
762
+ }
763
+ async close() {
764
+ this.lifetime?.abort();
765
+ this.attempt?.abort();
766
+ this.removeAbort?.();
767
+ clearTimeout(this.retry);
768
+ this.retry = undefined;
769
+ clearInterval(this.periodic);
770
+ this.periodic = undefined;
771
+ for (const task of this.tasks.values())
772
+ task.pauseRetry();
773
+ const handle = this.connection;
774
+ this.connection = undefined;
775
+ if (handle) {
776
+ const outcome = await withDeadline(this.release(handle), this.timeout);
777
+ if (!outcome.settled)
778
+ this.report(new Error("Subscription shutdown deadline exceeded"));
779
+ }
780
+ }
781
+ }
782
+ function changeSubscriptionResource(config) {
783
+ const subscription = new ChangeSubscription(config);
784
+ return defineManagedResource({
785
+ id: config.id,
786
+ start: (context) => subscription.start(context),
787
+ close: () => subscription.close(),
788
+ force: () => subscription.close()
789
+ });
790
+ }
791
+ // src/internal/decision.ts
792
+ import { z as z3 } from "zod";
793
+ var PolicyDecisionSchema = z3.discriminatedUnion("outcome", [
794
+ z3.object({ outcome: z3.literal("allow") }).strict(),
795
+ z3.object({ outcome: z3.literal("deny"), reason: z3.string().min(1) }).strict(),
796
+ z3.object({ outcome: z3.literal("defer") }).strict()
492
797
  ]);
493
798
 
494
799
  // src/application/decisions.ts
@@ -562,17 +867,17 @@ function createDecisionPipeline(policies, config) {
562
867
  };
563
868
  }
564
869
  // src/application/events.ts
565
- import { z as z3 } from "zod";
566
- var ApplicationLifecycleEventSchema = z3.object({
567
- type: z3.literal("application-state"),
870
+ import { z as z4 } from "zod";
871
+ var ApplicationLifecycleEventSchema = z4.object({
872
+ type: z4.literal("application-state"),
568
873
  applicationId: ApplicationIdSchema,
569
- epoch: z3.string().uuid(),
570
- revision: z3.number().int().nonnegative(),
874
+ epoch: z4.string().uuid(),
875
+ revision: z4.number().int().nonnegative(),
571
876
  lifecycle: ApplicationLifecycleSchema,
572
877
  health: ApplicationHealthSchema,
573
- ready: z3.boolean(),
574
- capturedAt: z3.string().datetime({ offset: true }),
575
- resources: z3.array(ManagedResourceSnapshotSchema).readonly()
878
+ ready: z4.boolean(),
879
+ capturedAt: z4.string().datetime({ offset: true }),
880
+ resources: z4.array(ManagedResourceSnapshotSchema).readonly()
576
881
  }).strict().readonly();
577
882
  function applicationLifecycleEvent(snapshot) {
578
883
  return ApplicationLifecycleEventSchema.parse({
@@ -603,13 +908,13 @@ function createApplicationEventSink(config) {
603
908
  };
604
909
  }
605
910
  // src/application/health.ts
606
- import { z as z4 } from "zod";
607
- var ApplicationHealthHandlerOptionsSchema = z4.object({
608
- kind: z4.enum(["liveness", "readiness"]),
609
- retryAfterSeconds: z4.number().int().nonnegative().default(5)
911
+ import { z as z5 } from "zod";
912
+ var ApplicationHealthHandlerOptionsSchema = z5.object({
913
+ kind: z5.enum(["liveness", "readiness"]),
914
+ retryAfterSeconds: z5.number().int().nonnegative().default(5)
610
915
  });
611
- var ApplicationOperationalHandlersOptionsSchema = z4.object({
612
- retryAfterSeconds: z4.number().int().nonnegative().default(5)
916
+ var ApplicationOperationalHandlersOptionsSchema = z5.object({
917
+ retryAfterSeconds: z5.number().int().nonnegative().default(5)
613
918
  });
614
919
  function createApplicationHealthHandler(application, options) {
615
920
  const parsed = ApplicationHealthHandlerOptionsSchema.parse(options);
@@ -804,41 +1109,97 @@ function sqliteKeyspaceBackend(declaration, config) {
804
1109
  }
805
1110
  };
806
1111
  }
807
- // src/application/notification-outbox.ts
808
- import { z as z5 } from "zod";
809
- function notificationOutboxStateSchema(payload) {
810
- return z5.object({
811
- schemaVersion: z5.literal(1),
812
- queue: z5.array(z5.object({
813
- key: z5.string().min(1).max(512),
814
- payload,
815
- createdAt: z5.string().datetime({ offset: true }),
816
- attempts: z5.number().int().nonnegative(),
817
- nextAttemptAt: z5.string().datetime({ offset: true }),
818
- leaseOwner: z5.string().min(1).max(128).nullable(),
819
- leaseId: z5.string().min(1).max(128).nullable(),
820
- leaseUntil: z5.string().datetime({ offset: true }).nullable()
821
- }).strict()),
822
- receipts: z5.array(z5.object({
823
- key: z5.string().min(1).max(512),
824
- completedAt: z5.string().datetime({ offset: true })
825
- }).strict())
826
- }).strict();
1112
+ // src/application/notification-delivery.ts
1113
+ import { z as z6 } from "zod";
1114
+ var NotificationDeliveryStateSchema = z6.object({
1115
+ version: z6.string().min(1).max(128),
1116
+ actions: z6.array(z6.string().min(1).max(128)).min(1).max(128),
1117
+ receipts: z6.array(z6.object({ action: z6.string(), receipt: z6.json() }).strict()).max(128),
1118
+ projectedAt: z6.string().datetime({ offset: true }).optional()
1119
+ }).strict().refine((value) => new Set(value.actions).size === value.actions.length && new Set(value.receipts.map((receipt) => receipt.action)).size === value.receipts.length && value.receipts.every((receipt) => value.actions.includes(receipt.action)), "Invalid action plan or receipts");
1120
+ function notificationDeliveryPlan(config, payload) {
1121
+ return NotificationDeliveryStateSchema.parse({
1122
+ version: config.version,
1123
+ actions: [...config.actions(payload)],
1124
+ receipts: []
1125
+ });
827
1126
  }
828
- var emptyOutboxState = () => ({
829
- schemaVersion: 1,
830
- queue: [],
831
- receipts: []
832
- });
1127
+ async function deliverNotification(input) {
1128
+ const { item, config } = input;
1129
+ const plan = item.delivery;
1130
+ if (!plan || plan.version !== config.version)
1131
+ throw new Error("Notification action plan version does not match the executor");
1132
+ const owns = (current2) => current2.key === item.key && current2.leaseId === item.leaseId;
1133
+ const checkpoint = async (change) => input.store.update((state) => {
1134
+ if (!state)
1135
+ throw new Error("Outbox state disappeared");
1136
+ const live2 = state.queue.find(owns);
1137
+ if (!live2?.delivery)
1138
+ return { state, result: false };
1139
+ const delivery = NotificationDeliveryStateSchema.parse(change(live2.delivery));
1140
+ return {
1141
+ state: input.bounded({
1142
+ ...state,
1143
+ queue: state.queue.map((row) => row === live2 ? {
1144
+ ...row,
1145
+ delivery,
1146
+ leaseUntil: new Date(input.clock().getTime() + input.leaseMs).toISOString()
1147
+ } : row)
1148
+ }),
1149
+ result: true
1150
+ };
1151
+ });
1152
+ const notification = { key: item.key, payload: item.payload, attempt: item.attempts };
1153
+ for (const action of plan.actions) {
1154
+ if (!input.active())
1155
+ return false;
1156
+ if (plan.receipts.some((receipt2) => receipt2.action === action))
1157
+ continue;
1158
+ if (!await checkpoint((value) => value))
1159
+ return false;
1160
+ const result = await input.send({
1161
+ ...notification,
1162
+ action,
1163
+ idempotencyKey: JSON.stringify([item.key, plan.version, action])
1164
+ });
1165
+ const receipt = z6.json().parse(result ?? null);
1166
+ if (!await checkpoint((value) => ({
1167
+ ...value,
1168
+ receipts: [...value.receipts, { action, receipt }]
1169
+ })))
1170
+ return false;
1171
+ }
1172
+ if (!input.active())
1173
+ return false;
1174
+ const current = await input.store.read();
1175
+ const live = current?.queue.find(owns);
1176
+ if (!live?.delivery)
1177
+ return false;
1178
+ if (live.delivery.projectedAt === undefined) {
1179
+ await config.project(notification, live.delivery.receipts);
1180
+ if (!await checkpoint((value) => ({ ...value, projectedAt: input.clock().toISOString() })))
1181
+ return false;
1182
+ }
1183
+ return true;
1184
+ }
1185
+ // src/application/notification-outbox.ts
1186
+ import { z as z8 } from "zod";
1187
+
1188
+ // src/application/notification-outbox-state.ts
1189
+ import { z as z7 } from "zod";
833
1190
  var sizeOf = (value) => measureSize(value).responseBytes;
834
- var DEFAULT_OUTBOX_BACKOFF = { minDelayMs: 1000, maxDelayMs: 60000, jitter: 0 };
1191
+ var DEFAULT_OUTBOX_BACKOFF = {
1192
+ minDelayMs: 1000,
1193
+ maxDelayMs: 60000,
1194
+ jitter: 0
1195
+ };
835
1196
  function resolveOutboxLimits(config) {
836
- const pollIntervalMs = z5.number().int().min(10).parse(config.pollIntervalMs ?? 1000);
837
- const leaseMs = z5.number().int().min(100).parse(config.leaseMs ?? 30000);
838
- const maxAttempts = z5.number().int().min(1).max(1e4).parse(config.maxAttempts ?? 100);
839
- const maxQueue = z5.number().int().min(1).max(1e5).parse(config.maxQueue ?? 1000);
840
- const maxStateBytes = z5.number().int().min(1024).parse(config.maxStateBytes ?? 1024 * 1024);
841
- const retainReceipts = z5.number().int().min(0).max(1e5).parse(config.retainReceipts ?? 1000);
1197
+ const pollIntervalMs = z7.number().int().min(10).parse(config.pollIntervalMs ?? 1000);
1198
+ const leaseMs = z7.number().int().min(100).parse(config.leaseMs ?? 30000);
1199
+ const maxAttempts = z7.number().int().min(1).max(1e4).parse(config.maxAttempts ?? 100);
1200
+ const maxQueue = z7.number().int().min(1).max(1e5).parse(config.maxQueue ?? 1000);
1201
+ const maxStateBytes = z7.number().int().min(1024).parse(config.maxStateBytes ?? 1024 * 1024);
1202
+ const retainReceipts = z7.number().int().min(0).max(1e5).parse(config.retainReceipts ?? 1000);
842
1203
  return { pollIntervalMs, leaseMs, maxAttempts, maxQueue, maxStateBytes, retainReceipts };
843
1204
  }
844
1205
  function boundedOutboxState(parsed, maxQueue, retainReceipts, maxStateBytes) {
@@ -863,7 +1224,7 @@ function dropReason(classification, attempts, maxAttempts) {
863
1224
  return "terminal";
864
1225
  return attempts >= maxAttempts ? "attempt-limit" : null;
865
1226
  }
866
- var RetryDelaySchema = z5.number().nonnegative().max(24 * 60 * 60 * 1000);
1227
+ var RetryDelaySchema = z7.number().nonnegative().max(24 * 60 * 60 * 1000);
867
1228
  var NO_LEASE = { leaseOwner: null, leaseId: null, leaseUntil: null };
868
1229
  function claimDue(queue, now, ownerId, leaseMs) {
869
1230
  const candidate = [...queue].sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt)).find((item) => Date.parse(item.nextAttemptAt) <= now.getTime() && (item.leaseUntil === null || Date.parse(item.leaseUntil) <= now.getTime()));
@@ -877,16 +1238,80 @@ function claimDue(queue, now, ownerId, leaseMs) {
877
1238
  leaseUntil: new Date(now.getTime() + leaseMs).toISOString()
878
1239
  };
879
1240
  }
1241
+
1242
+ // src/application/notification-outbox.ts
1243
+ function notificationOutboxStateSchema(payload) {
1244
+ return z8.object({
1245
+ schemaVersion: z8.literal(1),
1246
+ queue: z8.array(z8.object({
1247
+ key: z8.string().min(1).max(512),
1248
+ payload,
1249
+ delivery: NotificationDeliveryStateSchema.optional(),
1250
+ createdAt: z8.string().datetime({ offset: true }),
1251
+ attempts: z8.number().int().nonnegative(),
1252
+ nextAttemptAt: z8.string().datetime({ offset: true }),
1253
+ leaseOwner: z8.string().min(1).max(128).nullable(),
1254
+ leaseId: z8.string().min(1).max(128).nullable(),
1255
+ leaseUntil: z8.string().datetime({ offset: true }).nullable()
1256
+ }).strict()),
1257
+ receipts: z8.array(z8.object({
1258
+ key: z8.string().min(1).max(512),
1259
+ completedAt: z8.string().datetime({ offset: true }),
1260
+ delivery: NotificationDeliveryStateSchema.optional()
1261
+ }).strict())
1262
+ }).strict();
1263
+ }
1264
+ var emptyOutboxState = () => ({
1265
+ schemaVersion: 1,
1266
+ queue: [],
1267
+ receipts: []
1268
+ });
880
1269
  async function reportOutboxError(config, error) {
881
1270
  try {
882
1271
  await config.onError?.(error);
883
1272
  } catch {}
884
1273
  }
1274
+ function notificationRejection(config, parse, bounded, clock, backoffMs) {
1275
+ const maxAttempts = config.maxAttempts ?? 100;
1276
+ return async (claimed, error) => {
1277
+ const classification = await config.classify(error);
1278
+ return config.store.update((current) => {
1279
+ const state = parse(current);
1280
+ const live = state.queue.find((item) => item.key === claimed.key && item.leaseId === claimed.leaseId);
1281
+ if (!live)
1282
+ return { state, result: null };
1283
+ const attempts = live.attempts;
1284
+ const reason = dropReason(classification, attempts, maxAttempts);
1285
+ if (reason) {
1286
+ return {
1287
+ state: bounded({
1288
+ ...state,
1289
+ queue: state.queue.filter((item) => item.key !== live.key)
1290
+ }),
1291
+ result: { item: { ...live, attempts }, error, reason }
1292
+ };
1293
+ }
1294
+ const wait = RetryDelaySchema.parse(backoffMs(attempts));
1295
+ const retry = {
1296
+ ...live,
1297
+ nextAttemptAt: new Date(clock().getTime() + wait).toISOString(),
1298
+ ...NO_LEASE
1299
+ };
1300
+ return {
1301
+ state: bounded({
1302
+ ...state,
1303
+ queue: state.queue.map((item) => item.key === live.key ? retry : item)
1304
+ }),
1305
+ result: null
1306
+ };
1307
+ });
1308
+ };
1309
+ }
885
1310
  function createNotificationOutbox(config) {
886
1311
  const schema = notificationOutboxStateSchema(config.payloadSchema);
887
1312
  const clock = config.clock ?? (() => new Date);
888
1313
  const ownerId = config.ownerId ?? crypto.randomUUID();
889
- const { pollIntervalMs, leaseMs, maxAttempts, maxQueue, maxStateBytes, retainReceipts } = resolveOutboxLimits(config);
1314
+ const { pollIntervalMs, leaseMs, maxQueue, maxStateBytes, retainReceipts } = resolveOutboxLimits(config);
890
1315
  const backoffMs = config.backoffMs ?? ((attempt) => backoffDelay(DEFAULT_OUTBOX_BACKOFF, attempt));
891
1316
  let timer;
892
1317
  let running = false;
@@ -895,7 +1320,7 @@ function createNotificationOutbox(config) {
895
1320
  const bounded = (input) => boundedOutboxState(schema.parse(input), maxQueue, retainReceipts, maxStateBytes);
896
1321
  const parse = (state) => schema.parse(state ?? emptyOutboxState());
897
1322
  const enqueue = async (input) => {
898
- const key = z5.string().min(1).max(512).parse(input.key);
1323
+ const key = z8.string().min(1).max(512).parse(input.key);
899
1324
  const payload = config.payloadSchema.parse(input.payload);
900
1325
  const supersedes = new Set(input.supersedes ?? []);
901
1326
  return config.store.update((current) => {
@@ -904,7 +1329,16 @@ function createNotificationOutbox(config) {
904
1329
  return { state, result: false };
905
1330
  }
906
1331
  const now = clock().toISOString();
907
- const fresh = { key, payload, createdAt: now, attempts: 0, nextAttemptAt: now };
1332
+ const fresh = {
1333
+ key,
1334
+ payload,
1335
+ createdAt: now,
1336
+ attempts: 0,
1337
+ nextAttemptAt: now,
1338
+ ...config.delivery && {
1339
+ delivery: notificationDeliveryPlan(config.delivery, payload)
1340
+ }
1341
+ };
908
1342
  const queue = [
909
1343
  ...state.queue.filter((item) => !supersedes.has(item.key)),
910
1344
  { ...fresh, ...NO_LEASE }
@@ -932,11 +1366,15 @@ function createNotificationOutbox(config) {
932
1366
  const acknowledge = async (claimed) => {
933
1367
  await config.store.update((current) => {
934
1368
  const state = parse(current);
935
- const owns = state.queue.some((item) => item.key === claimed.key && item.leaseId === claimed.leaseId);
936
- if (!owns)
1369
+ const live = state.queue.find((item) => item.key === claimed.key && item.leaseId === claimed.leaseId);
1370
+ if (!live)
937
1371
  return { state, result: undefined };
938
1372
  const receipts = [
939
- { key: claimed.key, completedAt: clock().toISOString() },
1373
+ {
1374
+ key: claimed.key,
1375
+ completedAt: clock().toISOString(),
1376
+ ...live.delivery && { delivery: live.delivery }
1377
+ },
940
1378
  ...state.receipts.filter((receipt) => receipt.key !== claimed.key)
941
1379
  ].slice(0, retainReceipts);
942
1380
  return {
@@ -949,39 +1387,7 @@ function createNotificationOutbox(config) {
949
1387
  };
950
1388
  });
951
1389
  };
952
- const reject = async (claimed, error) => {
953
- const classification = await config.classify(error);
954
- return config.store.update((current) => {
955
- const state = parse(current);
956
- const live = state.queue.find((item) => item.key === claimed.key && item.leaseId === claimed.leaseId);
957
- if (!live)
958
- return { state, result: null };
959
- const attempts = live.attempts;
960
- const reason = dropReason(classification, attempts, maxAttempts);
961
- if (reason) {
962
- return {
963
- state: bounded({
964
- ...state,
965
- queue: state.queue.filter((item) => item.key !== live.key)
966
- }),
967
- result: { item: { ...live, attempts }, error, reason }
968
- };
969
- }
970
- const wait = RetryDelaySchema.parse(backoffMs(attempts));
971
- const retry = {
972
- ...live,
973
- nextAttemptAt: new Date(clock().getTime() + wait).toISOString(),
974
- ...NO_LEASE
975
- };
976
- return {
977
- state: bounded({
978
- ...state,
979
- queue: state.queue.map((item) => item.key === live.key ? retry : item)
980
- }),
981
- result: null
982
- };
983
- });
984
- };
1390
+ const reject = notificationRejection(config, parse, bounded, clock, backoffMs);
985
1391
  const flushPass = async () => {
986
1392
  let delivered = 0;
987
1393
  while (!interrupted) {
@@ -989,11 +1395,28 @@ function createNotificationOutbox(config) {
989
1395
  if (!claimed)
990
1396
  return delivered;
991
1397
  try {
992
- await config.send({
993
- key: claimed.key,
994
- payload: claimed.payload,
995
- attempt: claimed.attempts
996
- });
1398
+ if (config.delivery) {
1399
+ const finished = await deliverNotification({
1400
+ item: claimed,
1401
+ store: config.store,
1402
+ config: config.delivery,
1403
+ send: config.send,
1404
+ clock,
1405
+ bounded,
1406
+ leaseMs,
1407
+ active: () => !interrupted
1408
+ });
1409
+ if (!finished)
1410
+ return delivered;
1411
+ } else {
1412
+ if (claimed.delivery)
1413
+ throw new Error("An action-plan notification requires its delivery executor");
1414
+ await config.send({
1415
+ key: claimed.key,
1416
+ payload: claimed.payload,
1417
+ attempt: claimed.attempts
1418
+ });
1419
+ }
997
1420
  await acknowledge(claimed);
998
1421
  delivered += 1;
999
1422
  } catch (error) {
@@ -1072,11 +1495,11 @@ function notificationOutboxResource(outbox, config = {}) {
1072
1495
  });
1073
1496
  }
1074
1497
  // src/application/process-lifecycle.ts
1075
- import { z as z7 } from "zod";
1498
+ import { z as z10 } from "zod";
1076
1499
 
1077
1500
  // src/application/process-lifecycle-transitions.ts
1078
- import { z as z6 } from "zod";
1079
- var LifecycleTerminationSchema = z6.enum([
1501
+ import { z as z9 } from "zod";
1502
+ var LifecycleTerminationSchema = z9.enum([
1080
1503
  "active",
1081
1504
  "draining",
1082
1505
  "clean",
@@ -1085,22 +1508,22 @@ var LifecycleTerminationSchema = z6.enum([
1085
1508
  "hot-reload",
1086
1509
  "abnormal"
1087
1510
  ]);
1088
- var timestamp = z6.string().datetime({ offset: true });
1089
- var LifecycleRunSchema = z6.object({
1090
- runId: z6.string().min(1).max(128),
1091
- pid: z6.number().int().positive(),
1092
- version: z6.string().min(1).max(256),
1511
+ var timestamp = z9.string().datetime({ offset: true });
1512
+ var LifecycleRunSchema = z9.object({
1513
+ runId: z9.string().min(1).max(128),
1514
+ pid: z9.number().int().positive(),
1515
+ version: z9.string().min(1).max(256),
1093
1516
  startedAt: timestamp,
1094
1517
  readyAt: timestamp.nullable(),
1095
1518
  unavailableAt: timestamp.nullable().default(null),
1096
1519
  stoppedAt: timestamp.nullable(),
1097
1520
  termination: LifecycleTerminationSchema
1098
1521
  }).strict();
1099
- var LifecycleStateSchema = z6.object({
1100
- schemaVersion: z6.literal(1),
1101
- runs: z6.array(LifecycleRunSchema)
1522
+ var LifecycleStateSchema = z9.object({
1523
+ schemaVersion: z9.literal(1),
1524
+ runs: z9.array(LifecycleRunSchema)
1102
1525
  }).strict();
1103
- var PreviousExitSchema = z6.enum([
1526
+ var PreviousExitSchema = z9.enum([
1104
1527
  "first-boot",
1105
1528
  "hot-reload",
1106
1529
  "clean",
@@ -1127,7 +1550,7 @@ var UNKNOWN_VERSION = "unknown";
1127
1550
  function isVersionChange(previous, next) {
1128
1551
  return previous !== UNKNOWN_VERSION && next !== UNKNOWN_VERSION && previous !== next;
1129
1552
  }
1130
- var RetainSchema = z6.number().int().min(1).max(1000);
1553
+ var RetainSchema = z9.number().int().min(1).max(1000);
1131
1554
  function previousExitOf(previous, run, overlap) {
1132
1555
  if (!previous)
1133
1556
  return "first-boot";
@@ -1315,8 +1738,8 @@ function transitionProcessShutdown(source, input) {
1315
1738
  // src/application/process-lifecycle.ts
1316
1739
  function createProcessLifecycleLedger(config) {
1317
1740
  const clock = config.clock ?? (() => new Date);
1318
- const pid = z7.number().int().positive().parse(config.pid ?? process.pid);
1319
- const retain = z7.number().int().min(1).max(1000).parse(config.retain ?? 20);
1741
+ const pid = z10.number().int().positive().parse(config.pid ?? process.pid);
1742
+ const retain = z10.number().int().min(1).max(1000).parse(config.retain ?? 20);
1320
1743
  const configuredRunId = config.runId;
1321
1744
  const nextRunId = typeof configuredRunId === "function" ? configuredRunId : configuredRunId === undefined ? () => crypto.randomUUID() : () => configuredRunId;
1322
1745
  let runId = nextRunId();
@@ -1441,27 +1864,27 @@ function lifecycleLedgerResource(ledger, config) {
1441
1864
  });
1442
1865
  }
1443
1866
  // src/application/revision-signal.ts
1444
- import { z as z8 } from "zod";
1445
- var NonNegativeSafeIntegerSchema = z8.number().int().nonnegative().safe();
1446
- var PositiveSafeIntegerSchema2 = z8.number().int().positive().safe();
1447
- var TimerDelaySchema = z8.number().int().positive().max(2147483647);
1448
- var RevisionSignalStateSchema = z8.enum(["open", "closed"]);
1449
- var RevisionSignalWaitOutcomeSchema = z8.enum([
1867
+ import { z as z11 } from "zod";
1868
+ var NonNegativeSafeIntegerSchema = z11.number().int().nonnegative().safe();
1869
+ var PositiveSafeIntegerSchema2 = z11.number().int().positive().safe();
1870
+ var TimerDelaySchema = z11.number().int().positive().max(2147483647);
1871
+ var RevisionSignalStateSchema = z11.enum(["open", "closed"]);
1872
+ var RevisionSignalWaitOutcomeSchema = z11.enum([
1450
1873
  "changed",
1451
1874
  "timed-out",
1452
1875
  "aborted",
1453
1876
  "closed",
1454
1877
  "capacity"
1455
1878
  ]);
1456
- var RevisionSignalWaitResultSchema = z8.object({
1879
+ var RevisionSignalWaitResultSchema = z11.object({
1457
1880
  outcome: RevisionSignalWaitOutcomeSchema,
1458
1881
  revision: NonNegativeSafeIntegerSchema
1459
1882
  }).strict().readonly();
1460
- var RevisionSignalAdvanceResultSchema = z8.object({
1461
- outcome: z8.enum(["advanced", "closed"]),
1883
+ var RevisionSignalAdvanceResultSchema = z11.object({
1884
+ outcome: z11.enum(["advanced", "closed"]),
1462
1885
  revision: NonNegativeSafeIntegerSchema
1463
1886
  }).strict().readonly();
1464
- var RevisionSignalSnapshotSchema = z8.object({
1887
+ var RevisionSignalSnapshotSchema = z11.object({
1465
1888
  state: RevisionSignalStateSchema,
1466
1889
  revision: NonNegativeSafeIntegerSchema,
1467
1890
  pending: NonNegativeSafeIntegerSchema,
@@ -1678,9 +2101,23 @@ class WatchHubCore {
1678
2101
  same;
1679
2102
  reads = 0;
1680
2103
  closed = false;
2104
+ periodic;
1681
2105
  constructor(config) {
1682
2106
  this.config = config;
1683
2107
  this.maxWatches = config.maxWatchesPerSubscriber ?? 64;
2108
+ for (const bound of [config.maxSources ?? 1024, config.maxSubscribers ?? 1024]) {
2109
+ if (!Number.isInteger(bound) || bound < 1)
2110
+ throw new RangeError("Watch capacity must be positive");
2111
+ }
2112
+ if (config.reconcileIntervalMs !== undefined) {
2113
+ if (!Number.isInteger(config.reconcileIntervalMs) || config.reconcileIntervalMs < 10 || config.reconcileIntervalMs > 3600000)
2114
+ throw new RangeError("Invalid reconciliation interval");
2115
+ this.periodic = setInterval(() => {
2116
+ for (const source of this.sources.values())
2117
+ source.task.trigger();
2118
+ }, config.reconcileIntervalMs);
2119
+ this.periodic.unref?.();
2120
+ }
1684
2121
  this.holdMs = config.holdMs ?? 0;
1685
2122
  this.deltaMemoryBytes = config.deltaMemoryBytes ?? 262144;
1686
2123
  this.same = config.same ?? ((previous, next) => serializeCanonicalJson(previous) === serializeCanonicalJson(next));
@@ -1783,58 +2220,35 @@ class WatchHubCore {
1783
2220
  for (const subscriber of source.subscribers)
1784
2221
  this.deliver(source, subscriber);
1785
2222
  }
1786
- async pump(source) {
1787
- if (source.reading || this.closed)
2223
+ async readSource(source) {
2224
+ this.reads += 1;
2225
+ const value = await this.config.read(source.operation, source.args, source.scope);
2226
+ if (this.closed || this.sources.get(source.id) !== source)
1788
2227
  return;
1789
- source.reading = true;
1790
- try {
1791
- while (source.dirty && !this.closed) {
1792
- source.dirty = false;
1793
- this.reads += 1;
1794
- try {
1795
- const value = await this.config.read(source.operation, source.args);
1796
- source.backoff.reset();
1797
- const signature = serializeCanonicalJson(value);
1798
- const unchanged = source.signature !== undefined && this.same(source.value, value);
1799
- source.signature = signature;
1800
- if (!unchanged)
1801
- this.publish(source, value, valueFingerprint(value));
1802
- if (source.state.phase !== "live") {
1803
- this.announceState(source, { key: source.key, phase: "live" });
1804
- }
1805
- } catch (error) {
1806
- const code = readErrorCode(error);
1807
- this.announceState(source, {
1808
- key: source.key,
1809
- phase: "unavailable",
1810
- reason: "source-error",
1811
- ...code !== undefined && { code },
1812
- message: error instanceof Error ? error.message : String(error)
1813
- });
1814
- this.config.logger?.warn?.("[stitchkit] watched read failed", {
1815
- key: watchKeyString(source.key),
1816
- error
1817
- });
1818
- this.scheduleRetry(source);
1819
- return;
1820
- }
1821
- }
1822
- } finally {
1823
- source.reading = false;
1824
- }
2228
+ const signature = serializeCanonicalJson(value);
2229
+ const unchanged = source.signature !== undefined && this.same(source.value, value);
2230
+ source.signature = signature;
2231
+ if (!unchanged)
2232
+ this.publish(source, value, valueFingerprint(value));
2233
+ if (source.state.phase !== "live")
2234
+ this.announceState(source, { key: source.key, phase: "live" });
1825
2235
  }
1826
- scheduleRetry(source) {
1827
- if (this.closed || source.subscribers.size === 0)
1828
- return;
1829
- clearTimeout(source.retry);
1830
- source.retry = setTimeout(() => {
1831
- source.dirty = true;
1832
- this.pump(source);
1833
- }, source.backoff.next());
1834
- source.retry.unref?.();
2236
+ readFailed(source, error) {
2237
+ const code = readErrorCode(error);
2238
+ this.announceState(source, {
2239
+ key: source.key,
2240
+ phase: "unavailable",
2241
+ reason: "source-error",
2242
+ ...code !== undefined && { code },
2243
+ message: error instanceof Error ? error.message : String(error)
2244
+ });
2245
+ this.config.logger?.warn?.("[stitchkit] watched read failed", {
2246
+ key: watchKeyString(source.key),
2247
+ error
2248
+ });
1835
2249
  }
1836
- acquire(operation, key, args) {
1837
- const id = watchKeyString(key);
2250
+ acquire(operation, key, args, scope) {
2251
+ const id = JSON.stringify([scope ?? null, watchKeyString(key)]);
1838
2252
  const existing = this.sources.get(id);
1839
2253
  if (existing) {
1840
2254
  clearTimeout(existing.release);
@@ -1842,6 +2256,8 @@ class WatchHubCore {
1842
2256
  return existing;
1843
2257
  }
1844
2258
  const source = {
2259
+ id,
2260
+ scope,
1845
2261
  key,
1846
2262
  operation,
1847
2263
  args,
@@ -1851,15 +2267,17 @@ class WatchHubCore {
1851
2267
  history: new Map,
1852
2268
  historyBytes: 0,
1853
2269
  baselines: new Map,
1854
- reading: false,
1855
- dirty: true,
1856
- state: { key, phase: "opening" },
1857
- backoff: createBackoff(this.config.backoff ?? DEFAULT_BACKOFF)
2270
+ task: createCoalescedTask({
2271
+ run: () => this.readSource(source),
2272
+ active: () => !this.closed && source.subscribers.size > 0,
2273
+ onError: (error) => this.readFailed(source, error),
2274
+ backoff: this.config.backoff ?? DEFAULT_BACKOFF
2275
+ }),
2276
+ state: { key, phase: "opening" }
1858
2277
  };
1859
2278
  for (const topic of this.config.invalidatedBy(operation, args)) {
1860
2279
  source.unsubscribes.push(this.config.subscribe(topic, () => {
1861
- source.dirty = true;
1862
- this.pump(source);
2280
+ source.task.trigger();
1863
2281
  }));
1864
2282
  }
1865
2283
  this.sources.set(id, source);
@@ -1868,17 +2286,17 @@ class WatchHubCore {
1868
2286
  release(source) {
1869
2287
  if (source.subscribers.size > 0)
1870
2288
  return;
1871
- clearTimeout(source.retry);
1872
- source.retry = undefined;
2289
+ source.task.pauseRetry();
1873
2290
  const drop = () => {
1874
- if (source.reading) {
2291
+ if (source.task.running) {
1875
2292
  source.release = setTimeout(drop, 10);
1876
2293
  source.release.unref?.();
1877
2294
  return;
1878
2295
  }
2296
+ source.task.close();
1879
2297
  for (const unsubscribe of source.unsubscribes)
1880
2298
  unsubscribe();
1881
- this.sources.delete(watchKeyString(source.key));
2299
+ this.sources.delete(source.id);
1882
2300
  };
1883
2301
  if (this.holdMs === 0) {
1884
2302
  drop();
@@ -1887,11 +2305,18 @@ class WatchHubCore {
1887
2305
  source.release = setTimeout(drop, this.holdMs);
1888
2306
  source.release.unref?.();
1889
2307
  }
1890
- attach(subscriber) {
2308
+ attach(subscriber, scope) {
2309
+ if (this.held.size >= (this.config.maxSubscribers ?? 1024))
2310
+ throw new Error("Watch subscriber capacity exceeded");
2311
+ if (scope && (!scope.key || scope.key.length > 512))
2312
+ throw new Error("Invalid watch admission scope");
1891
2313
  const keys = new Set;
1892
2314
  this.held.set(subscriber, keys);
1893
- return {
2315
+ let detached = false;
2316
+ const attached = {
1894
2317
  open: (key, args, have) => {
2318
+ if (detached || scope?.signal.aborted)
2319
+ return { accepted: false, reason: "Watch scope ended" };
1895
2320
  if (this.closed)
1896
2321
  return { accepted: false, reason: "the watch hub is closed" };
1897
2322
  const operation = { service: key.service, action: key.action };
@@ -1901,7 +2326,7 @@ class WatchHubCore {
1901
2326
  reason: `${key.service}.${key.action} is not watchable`
1902
2327
  };
1903
2328
  }
1904
- const id = watchKeyString(key);
2329
+ const id = JSON.stringify([scope?.key ?? null, watchKeyString(key)]);
1905
2330
  if (keys.has(id))
1906
2331
  return { accepted: true };
1907
2332
  if (keys.size >= this.maxWatches) {
@@ -1910,7 +2335,9 @@ class WatchHubCore {
1910
2335
  reason: `this connection is already watching ${keys.size} reads (limit ${this.maxWatches})`
1911
2336
  };
1912
2337
  }
1913
- const source = this.acquire(operation, key, args);
2338
+ if (!this.sources.has(id) && this.sources.size >= (this.config.maxSources ?? 1024))
2339
+ return { accepted: false, reason: "Watch source capacity exceeded" };
2340
+ const source = this.acquire(operation, key, args, scope?.key);
1914
2341
  source.subscribers.add(subscriber);
1915
2342
  keys.add(id);
1916
2343
  if (have)
@@ -1932,11 +2359,12 @@ class WatchHubCore {
1932
2359
  reason: error instanceof Error ? error.message : String(error)
1933
2360
  };
1934
2361
  }
1935
- this.pump(source);
2362
+ if (!source.task.running && (source.signature === undefined || source.state.phase !== "live"))
2363
+ source.task.trigger();
1936
2364
  return { accepted: true };
1937
2365
  },
1938
2366
  close: (key) => {
1939
- const id = watchKeyString(key);
2367
+ const id = JSON.stringify([scope?.key ?? null, watchKeyString(key)]);
1940
2368
  if (!keys.delete(id))
1941
2369
  return;
1942
2370
  const source = this.sources.get(id);
@@ -1947,6 +2375,8 @@ class WatchHubCore {
1947
2375
  this.release(source);
1948
2376
  },
1949
2377
  detach: () => {
2378
+ detached = true;
2379
+ scope?.signal.removeEventListener("abort", onAbort);
1950
2380
  for (const id of keys) {
1951
2381
  const source = this.sources.get(id);
1952
2382
  if (!source)
@@ -1959,20 +2389,28 @@ class WatchHubCore {
1959
2389
  this.held.delete(subscriber);
1960
2390
  }
1961
2391
  };
2392
+ const onAbort = () => attached.detach();
2393
+ if (scope?.signal.aborted)
2394
+ attached.detach();
2395
+ else
2396
+ scope?.signal.addEventListener("abort", onAbort, { once: true });
2397
+ return attached;
1962
2398
  }
1963
2399
  close() {
1964
2400
  this.closed = true;
2401
+ clearInterval(this.periodic);
1965
2402
  const teardown = [...this.sources.values()];
1966
2403
  this.sources.clear();
1967
2404
  this.held.clear();
1968
2405
  for (const source of teardown) {
1969
- clearTimeout(source.retry);
2406
+ source.task.close();
1970
2407
  clearTimeout(source.release);
1971
2408
  this.announceState(source, {
1972
2409
  key: source.key,
1973
2410
  phase: "unavailable",
1974
2411
  reason: "source-unavailable"
1975
2412
  });
2413
+ source.task.close();
1976
2414
  for (const unsubscribe of source.unsubscribes)
1977
2415
  unsubscribe();
1978
2416
  }
@@ -2000,7 +2438,7 @@ function watchKey(operation, args) {
2000
2438
  function createWatchHub(config) {
2001
2439
  const hub = new WatchHubCore(config);
2002
2440
  return {
2003
- attach: (subscriber) => hub.attach(subscriber),
2441
+ attach: (subscriber, scope) => hub.attach(subscriber, scope),
2004
2442
  readCount: () => hub.reads,
2005
2443
  size: () => hub.sources.size,
2006
2444
  close: () => hub.close()
@@ -2040,6 +2478,7 @@ export {
2040
2478
  createApplicationEventSink,
2041
2479
  createApplication,
2042
2480
  createActivityProjection,
2481
+ changeSubscriptionResource,
2043
2482
  applicationLifecycleEvent,
2044
2483
  RevisionSignalWaitResultSchema,
2045
2484
  RevisionSignalWaitOutcomeSchema,
@@ -2048,6 +2487,7 @@ export {
2048
2487
  RevisionSignalAdvanceResultSchema,
2049
2488
  PreviousExitSchema,
2050
2489
  PolicyDecisionSchema,
2490
+ NotificationDeliveryStateSchema,
2051
2491
  ManagedScheduleStatusSchema,
2052
2492
  ManagedScheduleOverlapSchema,
2053
2493
  ManagedScheduleErrorPolicySchema,