@smthrs/notifications 0.0.0-stage → 1.0.0-rc.12

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 (146) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/LICENSE +21 -0
  3. package/README.md +93 -2
  4. package/dist/cjs/AlertPolicy.d.ts +284 -0
  5. package/dist/cjs/AlertPolicy.d.ts.map +1 -0
  6. package/dist/cjs/AlertPolicy.js +124 -0
  7. package/dist/cjs/AlertPolicy.js.map +7 -0
  8. package/dist/cjs/AlertRuntime.d.ts +90 -0
  9. package/dist/cjs/AlertRuntime.d.ts.map +1 -0
  10. package/dist/cjs/AlertRuntime.js +166 -0
  11. package/dist/cjs/AlertRuntime.js.map +7 -0
  12. package/dist/cjs/AlertSink.d.ts +101 -0
  13. package/dist/cjs/AlertSink.d.ts.map +1 -0
  14. package/dist/cjs/AlertSink.js +44 -0
  15. package/dist/cjs/AlertSink.js.map +7 -0
  16. package/dist/cjs/Alerts.d.ts +57 -0
  17. package/dist/cjs/Alerts.d.ts.map +1 -0
  18. package/dist/cjs/Alerts.js +46 -0
  19. package/dist/cjs/Alerts.js.map +7 -0
  20. package/dist/cjs/Notification.d.ts +214 -0
  21. package/dist/cjs/Notification.d.ts.map +1 -0
  22. package/dist/cjs/Notification.js +62 -0
  23. package/dist/cjs/Notification.js.map +7 -0
  24. package/dist/cjs/NotificationEvent.d.ts +214 -0
  25. package/dist/cjs/NotificationEvent.d.ts.map +1 -0
  26. package/dist/cjs/NotificationEvent.js +84 -0
  27. package/dist/cjs/NotificationEvent.js.map +7 -0
  28. package/dist/cjs/NotificationQueue.d.ts +194 -0
  29. package/dist/cjs/NotificationQueue.d.ts.map +1 -0
  30. package/dist/cjs/NotificationQueue.js +477 -0
  31. package/dist/cjs/NotificationQueue.js.map +7 -0
  32. package/dist/cjs/NotificationState.d.ts +169 -0
  33. package/dist/cjs/NotificationState.d.ts.map +1 -0
  34. package/dist/cjs/NotificationState.js +110 -0
  35. package/dist/cjs/NotificationState.js.map +7 -0
  36. package/dist/cjs/Projection.d.ts +30 -0
  37. package/dist/cjs/Projection.d.ts.map +1 -0
  38. package/dist/cjs/Projection.js +53 -0
  39. package/dist/cjs/Projection.js.map +7 -0
  40. package/dist/cjs/SteerPayload.d.ts +177 -0
  41. package/dist/cjs/SteerPayload.d.ts.map +1 -0
  42. package/dist/cjs/SteerPayload.js +61 -0
  43. package/dist/cjs/SteerPayload.js.map +7 -0
  44. package/dist/cjs/index.d.ts +41 -0
  45. package/dist/cjs/index.d.ts.map +1 -0
  46. package/dist/cjs/index.js +47 -0
  47. package/dist/cjs/index.js.map +7 -0
  48. package/dist/cjs/internal/foldCache.d.ts +36 -0
  49. package/dist/cjs/internal/foldCache.d.ts.map +1 -0
  50. package/dist/cjs/internal/foldCache.js +54 -0
  51. package/dist/cjs/internal/foldCache.js.map +7 -0
  52. package/dist/cjs/internal/replay.d.ts +22 -0
  53. package/dist/cjs/internal/replay.d.ts.map +1 -0
  54. package/dist/cjs/internal/replay.js +46 -0
  55. package/dist/cjs/internal/replay.js.map +7 -0
  56. package/dist/cjs/layerWebhook.d.ts +60 -0
  57. package/dist/cjs/layerWebhook.d.ts.map +1 -0
  58. package/dist/cjs/layerWebhook.js +145 -0
  59. package/dist/cjs/layerWebhook.js.map +7 -0
  60. package/dist/cjs/package.json +1 -0
  61. package/dist/esm/AlertPolicy.d.ts +284 -0
  62. package/dist/esm/AlertPolicy.d.ts.map +1 -0
  63. package/dist/esm/AlertPolicy.js +284 -0
  64. package/dist/esm/AlertPolicy.js.map +1 -0
  65. package/dist/esm/AlertRuntime.d.ts +90 -0
  66. package/dist/esm/AlertRuntime.d.ts.map +1 -0
  67. package/dist/esm/AlertRuntime.js +191 -0
  68. package/dist/esm/AlertRuntime.js.map +1 -0
  69. package/dist/esm/AlertSink.d.ts +101 -0
  70. package/dist/esm/AlertSink.d.ts.map +1 -0
  71. package/dist/esm/AlertSink.js +63 -0
  72. package/dist/esm/AlertSink.js.map +1 -0
  73. package/dist/esm/Alerts.d.ts +57 -0
  74. package/dist/esm/Alerts.d.ts.map +1 -0
  75. package/dist/esm/Alerts.js +57 -0
  76. package/dist/esm/Alerts.js.map +1 -0
  77. package/dist/esm/Notification.d.ts +214 -0
  78. package/dist/esm/Notification.d.ts.map +1 -0
  79. package/dist/esm/Notification.js +121 -0
  80. package/dist/esm/Notification.js.map +1 -0
  81. package/dist/esm/NotificationEvent.d.ts +214 -0
  82. package/dist/esm/NotificationEvent.d.ts.map +1 -0
  83. package/dist/esm/NotificationEvent.js +158 -0
  84. package/dist/esm/NotificationEvent.js.map +1 -0
  85. package/dist/esm/NotificationQueue.d.ts +194 -0
  86. package/dist/esm/NotificationQueue.d.ts.map +1 -0
  87. package/dist/esm/NotificationQueue.js +625 -0
  88. package/dist/esm/NotificationQueue.js.map +1 -0
  89. package/dist/esm/NotificationState.d.ts +169 -0
  90. package/dist/esm/NotificationState.d.ts.map +1 -0
  91. package/dist/esm/NotificationState.js +191 -0
  92. package/dist/esm/NotificationState.js.map +1 -0
  93. package/dist/esm/Projection.d.ts +30 -0
  94. package/dist/esm/Projection.d.ts.map +1 -0
  95. package/dist/esm/Projection.js +47 -0
  96. package/dist/esm/Projection.js.map +1 -0
  97. package/dist/esm/SteerPayload.d.ts +177 -0
  98. package/dist/esm/SteerPayload.d.ts.map +1 -0
  99. package/dist/esm/SteerPayload.js +139 -0
  100. package/dist/esm/SteerPayload.js.map +1 -0
  101. package/dist/esm/index.d.ts +41 -0
  102. package/dist/esm/index.d.ts.map +1 -0
  103. package/dist/esm/index.js +41 -0
  104. package/dist/esm/index.js.map +1 -0
  105. package/dist/esm/internal/foldCache.d.ts +36 -0
  106. package/dist/esm/internal/foldCache.d.ts.map +1 -0
  107. package/dist/esm/internal/foldCache.js +58 -0
  108. package/dist/esm/internal/foldCache.js.map +1 -0
  109. package/dist/esm/internal/replay.d.ts +22 -0
  110. package/dist/esm/internal/replay.d.ts.map +1 -0
  111. package/dist/esm/internal/replay.js +31 -0
  112. package/dist/esm/internal/replay.js.map +1 -0
  113. package/dist/esm/layerWebhook.d.ts +60 -0
  114. package/dist/esm/layerWebhook.d.ts.map +1 -0
  115. package/dist/esm/layerWebhook.js +186 -0
  116. package/dist/esm/layerWebhook.js.map +1 -0
  117. package/docs/README.md +146 -0
  118. package/docs/api.md +443 -0
  119. package/docs/concepts/admission-and-promotion.md +144 -0
  120. package/docs/concepts/alerting.md +138 -0
  121. package/docs/concepts/journal-records.md +132 -0
  122. package/docs/guides/alert-on-a-stuck-run.md +140 -0
  123. package/docs/guides/drain-at-a-turn-boundary.md +95 -0
  124. package/docs/guides/handle-a-full-queue.md +102 -0
  125. package/docs/guides/report-pending-notifications.md +92 -0
  126. package/docs/guides/send-alerts-to-a-webhook.md +155 -0
  127. package/docs/guides/steer-a-run.md +127 -0
  128. package/docs/guides/testing.md +161 -0
  129. package/docs/installation.md +123 -0
  130. package/docs/quickstart.md +159 -0
  131. package/docs/troubleshooting.md +254 -0
  132. package/package.json +182 -3
  133. package/src/AlertPolicy.ts +377 -0
  134. package/src/AlertRuntime.ts +280 -0
  135. package/src/AlertSink.ts +105 -0
  136. package/src/Alerts.ts +71 -0
  137. package/src/Notification.ts +171 -0
  138. package/src/NotificationEvent.ts +213 -0
  139. package/src/NotificationQueue.ts +856 -0
  140. package/src/NotificationState.ts +295 -0
  141. package/src/Projection.ts +48 -0
  142. package/src/SteerPayload.ts +198 -0
  143. package/src/index.ts +47 -0
  144. package/src/internal/foldCache.ts +72 -0
  145. package/src/internal/replay.ts +39 -0
  146. package/src/layerWebhook.ts +239 -0
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The alert sink port: where a raised alert is sent, and why a send failed.
3
+ *
4
+ * The port is a tag and an error so a deployment supplies the transport and
5
+ * the policy needs none. `./layerWebhook.ts` is the transport this package
6
+ * ships, and {@link layerNoop} is the one a test composes.
7
+ *
8
+ * @since 1.0.0
9
+ */
10
+ import { Context, Effect, Layer, Schema } from "effect";
11
+ import type { Alert } from "./AlertPolicy.ts";
12
+ /**
13
+ * Why a sink did not take an alert.
14
+ *
15
+ * `sink_rejected` is an answer that refused the page, `sink_unreachable` a
16
+ * request that never got one, and `sink_timeout` one that got no answer inside
17
+ * the sink's bound. `sink_misconfigured` is an endpoint the sink may not POST
18
+ * to at all; it is raised when the layer is built, before any alert exists,
19
+ * so it never reaches a journal.
20
+ *
21
+ * @category models
22
+ * @since 1.0.0
23
+ */
24
+ export declare const FailureCode: Schema.Literals<readonly ["sink_rejected", "sink_unreachable", "sink_timeout", "sink_misconfigured"]>;
25
+ /**
26
+ * Why a sink did not take an alert.
27
+ *
28
+ * @category models
29
+ * @since 1.0.0
30
+ */
31
+ export type FailureCode = typeof FailureCode.Type;
32
+ declare const AlertError_base: Schema.Class<AlertError, Schema.TaggedStruct<"/notifications/AlertError", {
33
+ readonly code: Schema.withConstructorDefault<Schema.Literals<readonly ["sink_rejected", "sink_unreachable", "sink_timeout", "sink_misconfigured"]>>;
34
+ readonly message: Schema.String;
35
+ /** The HTTP status that refused the page, when the sink speaks HTTP. */
36
+ readonly status: Schema.optional<Schema.Int>;
37
+ /** A short, credential-free description of the transport failure. */
38
+ readonly reason: Schema.optional<Schema.String>;
39
+ }>, import("effect/Cause").YieldableError>;
40
+ /**
41
+ * A sink refused or could not take an alert.
42
+ *
43
+ * The error carries a code, the answering status when there was one, and a
44
+ * short reason. It deliberately holds no request: a webhook request carries
45
+ * the credential the deployment handed {@link layerWebhook}, and an error is
46
+ * logged, encoded, and journaled in places a credential must never reach.
47
+ *
48
+ * @category errors
49
+ * @since 0.1.0
50
+ */
51
+ export declare class AlertError extends AlertError_base {
52
+ }
53
+ /**
54
+ * Where a raised alert is sent.
55
+ *
56
+ * Injected rather than fixed, because who gets paged is a deployment's
57
+ * decision and the policy is not: the same rules feed a webhook in production
58
+ * and nothing at all in a test.
59
+ *
60
+ * @category services
61
+ * @since 0.1.0
62
+ */
63
+ export interface SinkService {
64
+ /**
65
+ * Sends one alert, and does so idempotently on {@link alertId}.
66
+ *
67
+ * Delivery is at-least-once: the `flows.alerts.delivered` record is written
68
+ * after this effect succeeds, so a process that dies in between pages again
69
+ * on the next tick. Every field of the alert, {@link Alert.firedAt}
70
+ * included, is derived from the journal, so the same alert is byte-identical
71
+ * on every attempt and `alertId(alert)` is the deduplication key a receiving
72
+ * system should key on. A sink that cannot dedupe is a sink that will
73
+ * occasionally page twice about one condition.
74
+ *
75
+ * A failure means the page did not go out. Succeeding on a page that was
76
+ * dropped is the one thing this port must never do.
77
+ */
78
+ readonly deliver: (alert: Alert) => Effect.Effect<void, AlertError>;
79
+ }
80
+ declare const Sink_base: Context.ServiceClass<Sink, "/notifications/AlertSink", SinkService>;
81
+ /**
82
+ * The {@link SinkService} tag.
83
+ *
84
+ * @category services
85
+ * @since 0.1.0
86
+ */
87
+ export declare class Sink extends Sink_base {
88
+ }
89
+ /**
90
+ * A sink that accepts every alert and sends it nowhere.
91
+ *
92
+ * The admission and the delivery record still happen, so a composition without
93
+ * an outbound channel still has the durable evidence of what it would have
94
+ * paged about.
95
+ *
96
+ * @category layers
97
+ * @since 0.1.0
98
+ */
99
+ export declare const layerNoop: Layer.Layer<Sink>;
100
+ export {};
101
+ //# sourceMappingURL=AlertSink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AlertSink.d.ts","sourceRoot":"","sources":["../../src/AlertSink.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACvD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAE7C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,WAAW,uGAA+F,CAAA;AAEvH;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,WAAW,CAAC,IAAI,CAAA;;;;IAkB7C,wEAAwE;;IAExE,qEAAqE;;;AAlBzE;;;;;;;;;;GAUG;AACH,qBAAa,UAAW,SAAQ,eAU/B;CAAG;AAEJ;;;;;;;;;GASG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;CACpE;;AAED;;;;;GAKG;AACH,qBAAa,IAAK,SAAQ,SAAgE;CAAG;AAE7F;;;;;;;;;GASG;AACH,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,IAAI,CAAuD,CAAA"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The alert sink port: where a raised alert is sent, and why a send failed.
3
+ *
4
+ * The port is a tag and an error so a deployment supplies the transport and
5
+ * the policy needs none. `./layerWebhook.ts` is the transport this package
6
+ * ships, and {@link layerNoop} is the one a test composes.
7
+ *
8
+ * @since 1.0.0
9
+ */
10
+ import { Context, Effect, Layer, Schema } from "effect";
11
+ /**
12
+ * Why a sink did not take an alert.
13
+ *
14
+ * `sink_rejected` is an answer that refused the page, `sink_unreachable` a
15
+ * request that never got one, and `sink_timeout` one that got no answer inside
16
+ * the sink's bound. `sink_misconfigured` is an endpoint the sink may not POST
17
+ * to at all; it is raised when the layer is built, before any alert exists,
18
+ * so it never reaches a journal.
19
+ *
20
+ * @category models
21
+ * @since 1.0.0
22
+ */
23
+ export const FailureCode = Schema.Literals(["sink_rejected", "sink_unreachable", "sink_timeout", "sink_misconfigured"]);
24
+ /**
25
+ * A sink refused or could not take an alert.
26
+ *
27
+ * The error carries a code, the answering status when there was one, and a
28
+ * short reason. It deliberately holds no request: a webhook request carries
29
+ * the credential the deployment handed {@link layerWebhook}, and an error is
30
+ * logged, encoded, and journaled in places a credential must never reach.
31
+ *
32
+ * @category errors
33
+ * @since 0.1.0
34
+ */
35
+ export class AlertError extends Schema.TaggedError()("/notifications/AlertError", {
36
+ code: FailureCode.pipe(Schema.withConstructorDefault(Effect.succeed("sink_rejected"))),
37
+ message: Schema.String,
38
+ /** The HTTP status that refused the page, when the sink speaks HTTP. */
39
+ status: Schema.optional(Schema.Int),
40
+ /** A short, credential-free description of the transport failure. */
41
+ reason: Schema.optional(Schema.String)
42
+ }) {
43
+ }
44
+ /**
45
+ * The {@link SinkService} tag.
46
+ *
47
+ * @category services
48
+ * @since 0.1.0
49
+ */
50
+ export class Sink extends Context.Service()("/notifications/AlertSink") {
51
+ }
52
+ /**
53
+ * A sink that accepts every alert and sends it nowhere.
54
+ *
55
+ * The admission and the delivery record still happen, so a composition without
56
+ * an outbound channel still has the durable evidence of what it would have
57
+ * paged about.
58
+ *
59
+ * @category layers
60
+ * @since 0.1.0
61
+ */
62
+ export const layerNoop = Layer.succeed(Sink)({ deliver: () => Effect.void });
63
+ //# sourceMappingURL=AlertSink.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AlertSink.js","sourceRoot":"","sources":["../../src/AlertSink.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAGvD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,eAAe,EAAE,kBAAkB,EAAE,cAAc,EAAE,oBAAoB,CAAC,CAAC,CAAA;AAUvH;;;;;;;;;;GAUG;AACH,MAAM,OAAO,UAAW,SAAQ,MAAM,CAAC,WAAW,EAAc,CAC9D,2BAA2B,EAC3B;IACE,IAAI,EAAE,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,MAAM,CAAC,OAAO,CAAC,eAAwB,CAAC,CAAC,CAAC;IAC/F,OAAO,EAAE,MAAM,CAAC,MAAM;IACtB,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC;IACnC,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;CACvC,CACF;CAAG;AA8BJ;;;;;GAKG;AACH,MAAM,OAAO,IAAK,SAAQ,OAAO,CAAC,OAAO,EAAqB,CAAC,0BAA0B,CAAC;CAAG;AAE7F;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,SAAS,GAAsB,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAA","sourcesContent":["/**\n * The alert sink port: where a raised alert is sent, and why a send failed.\n *\n * The port is a tag and an error so a deployment supplies the transport and\n * the policy needs none. `./layerWebhook.ts` is the transport this package\n * ships, and {@link layerNoop} is the one a test composes.\n *\n * @since 1.0.0\n */\n\nimport { Context, Effect, Layer, Schema } from \"effect\"\nimport type { Alert } from \"./AlertPolicy.ts\"\n\n/**\n * Why a sink did not take an alert.\n *\n * `sink_rejected` is an answer that refused the page, `sink_unreachable` a\n * request that never got one, and `sink_timeout` one that got no answer inside\n * the sink's bound. `sink_misconfigured` is an endpoint the sink may not POST\n * to at all; it is raised when the layer is built, before any alert exists,\n * so it never reaches a journal.\n *\n * @category models\n * @since 1.0.0\n */\nexport const FailureCode = Schema.Literals([\"sink_rejected\", \"sink_unreachable\", \"sink_timeout\", \"sink_misconfigured\"])\n\n/**\n * Why a sink did not take an alert.\n *\n * @category models\n * @since 1.0.0\n */\nexport type FailureCode = typeof FailureCode.Type\n\n/**\n * A sink refused or could not take an alert.\n *\n * The error carries a code, the answering status when there was one, and a\n * short reason. It deliberately holds no request: a webhook request carries\n * the credential the deployment handed {@link layerWebhook}, and an error is\n * logged, encoded, and journaled in places a credential must never reach.\n *\n * @category errors\n * @since 0.1.0\n */\nexport class AlertError extends Schema.TaggedError<AlertError>()(\n \"/notifications/AlertError\",\n {\n code: FailureCode.pipe(Schema.withConstructorDefault(Effect.succeed(\"sink_rejected\" as const))),\n message: Schema.String,\n /** The HTTP status that refused the page, when the sink speaks HTTP. */\n status: Schema.optional(Schema.Int),\n /** A short, credential-free description of the transport failure. */\n reason: Schema.optional(Schema.String)\n }\n) {}\n\n/**\n * Where a raised alert is sent.\n *\n * Injected rather than fixed, because who gets paged is a deployment's\n * decision and the policy is not: the same rules feed a webhook in production\n * and nothing at all in a test.\n *\n * @category services\n * @since 0.1.0\n */\nexport interface SinkService {\n /**\n * Sends one alert, and does so idempotently on {@link alertId}.\n *\n * Delivery is at-least-once: the `flows.alerts.delivered` record is written\n * after this effect succeeds, so a process that dies in between pages again\n * on the next tick. Every field of the alert, {@link Alert.firedAt}\n * included, is derived from the journal, so the same alert is byte-identical\n * on every attempt and `alertId(alert)` is the deduplication key a receiving\n * system should key on. A sink that cannot dedupe is a sink that will\n * occasionally page twice about one condition.\n *\n * A failure means the page did not go out. Succeeding on a page that was\n * dropped is the one thing this port must never do.\n */\n readonly deliver: (alert: Alert) => Effect.Effect<void, AlertError>\n}\n\n/**\n * The {@link SinkService} tag.\n *\n * @category services\n * @since 0.1.0\n */\nexport class Sink extends Context.Service<Sink, SinkService>()(\"/notifications/AlertSink\") {}\n\n/**\n * A sink that accepts every alert and sends it nowhere.\n *\n * The admission and the delivery record still happen, so a composition without\n * an outbound channel still has the durable evidence of what it would have\n * paged about.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerNoop: Layer.Layer<Sink> = Layer.succeed(Sink)({ deliver: () => Effect.void })\n"]}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Alert policy: run conditions that have lasted too long, turned into durable,
3
+ * coalesced, delivered-once notifications.
4
+ *
5
+ * A notification queue answers "tell this run something". An alert answers the
6
+ * question nobody is around to ask: a run has been waiting for an approval for
7
+ * an hour, and the person who could grant it does not know. The two are the
8
+ * same machinery, since an alert is admitted as a coalesced system event, and
9
+ * they differ only in who decides to write one.
10
+ *
11
+ * ## Journal time, not wall time
12
+ *
13
+ * A condition's clock starts at the journal entry that opened it, which makes
14
+ * the decision replayable: the same journal produces the same alerts, whatever
15
+ * process reads it and whenever. A restart re-derives every open condition
16
+ * from the entries rather than from a timer it lost.
17
+ *
18
+ * ## Delivered at least once
19
+ *
20
+ * The admission is idempotent on the notification id, so re-admitting the same
21
+ * alert writes nothing new, and `flows.alerts.delivered` is journaled AFTER
22
+ * the sink accepted the page and is checked before the sink is called again.
23
+ * That ordering is deliberate and it is the reason the guarantee is
24
+ * at-least-once rather than exactly-once: a process that dies between the
25
+ * accepted send and the delivery record has paged, left no evidence of it, and
26
+ * will page again on the next tick. Recording the delivery first turns the
27
+ * same crash into a page nobody ever receives, which for an alert is the worse
28
+ * failure.
29
+ *
30
+ * So the sink owns the last mile: {@link SinkService.deliver} MUST be
31
+ * idempotent on {@link alertId}, which is stable for the life of one condition
32
+ * and reaches the sink on every alert it is handed. A sink that fails journals
33
+ * `flows.alerts.failed` once per failure code and the alert is retried on the
34
+ * next tick, because a refused page is not a delivered page.
35
+ *
36
+ * ## What a refusal costs
37
+ *
38
+ * The queue can refuse the admission when a run already holds its capacity of
39
+ * pending notifications. A tick that is refused pages nobody: the alert comes
40
+ * back in {@link Tick.refused}, the refusal is journaled, and the next tick
41
+ * tries again once a boundary has drained. Calling the sink anyway would page
42
+ * about an alert the run will never see.
43
+ *
44
+ * ## Conditions are data
45
+ *
46
+ * A condition is a payload field with a value: `status` is `waiting-approval`,
47
+ * `health` is `stalled`. Those are the fields control-plane entries and monitor
48
+ * beats already carry, so a deployment that journals a different vocabulary
49
+ * supplies its own detectors instead of a fork of this module.
50
+ *
51
+ * @since 0.1.0
52
+ */
53
+ export { type Alert, alertId, coalescingKey, conditions, decide, defaultDetectors, deliveredEventType, Detector, failedEventType, type Open, Policy, Rule, Severity } from "./AlertPolicy.ts";
54
+ export { AlertRuntime, layer, type RuntimeService, type Tick } from "./AlertRuntime.ts";
55
+ export { AlertError, FailureCode, layerNoop, Sink, type SinkService } from "./AlertSink.ts";
56
+ export { defaultWebhookTimeout, layerWebhook } from "./layerWebhook.ts";
57
+ //# sourceMappingURL=Alerts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Alerts.d.ts","sourceRoot":"","sources":["../../src/Alerts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,OAAO,EACL,KAAK,KAAK,EACV,OAAO,EACP,aAAa,EACb,UAAU,EACV,MAAM,EACN,gBAAgB,EAChB,kBAAkB,EAClB,QAAQ,EACR,eAAe,EACf,KAAK,IAAI,EACT,MAAM,EACN,IAAI,EACJ,QAAQ,EACT,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,KAAK,cAAc,EAAE,KAAK,IAAI,EAAE,MAAM,mBAAmB,CAAA;AACvF,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC3F,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Alert policy: run conditions that have lasted too long, turned into durable,
3
+ * coalesced, delivered-once notifications.
4
+ *
5
+ * A notification queue answers "tell this run something". An alert answers the
6
+ * question nobody is around to ask: a run has been waiting for an approval for
7
+ * an hour, and the person who could grant it does not know. The two are the
8
+ * same machinery, since an alert is admitted as a coalesced system event, and
9
+ * they differ only in who decides to write one.
10
+ *
11
+ * ## Journal time, not wall time
12
+ *
13
+ * A condition's clock starts at the journal entry that opened it, which makes
14
+ * the decision replayable: the same journal produces the same alerts, whatever
15
+ * process reads it and whenever. A restart re-derives every open condition
16
+ * from the entries rather than from a timer it lost.
17
+ *
18
+ * ## Delivered at least once
19
+ *
20
+ * The admission is idempotent on the notification id, so re-admitting the same
21
+ * alert writes nothing new, and `flows.alerts.delivered` is journaled AFTER
22
+ * the sink accepted the page and is checked before the sink is called again.
23
+ * That ordering is deliberate and it is the reason the guarantee is
24
+ * at-least-once rather than exactly-once: a process that dies between the
25
+ * accepted send and the delivery record has paged, left no evidence of it, and
26
+ * will page again on the next tick. Recording the delivery first turns the
27
+ * same crash into a page nobody ever receives, which for an alert is the worse
28
+ * failure.
29
+ *
30
+ * So the sink owns the last mile: {@link SinkService.deliver} MUST be
31
+ * idempotent on {@link alertId}, which is stable for the life of one condition
32
+ * and reaches the sink on every alert it is handed. A sink that fails journals
33
+ * `flows.alerts.failed` once per failure code and the alert is retried on the
34
+ * next tick, because a refused page is not a delivered page.
35
+ *
36
+ * ## What a refusal costs
37
+ *
38
+ * The queue can refuse the admission when a run already holds its capacity of
39
+ * pending notifications. A tick that is refused pages nobody: the alert comes
40
+ * back in {@link Tick.refused}, the refusal is journaled, and the next tick
41
+ * tries again once a boundary has drained. Calling the sink anyway would page
42
+ * about an alert the run will never see.
43
+ *
44
+ * ## Conditions are data
45
+ *
46
+ * A condition is a payload field with a value: `status` is `waiting-approval`,
47
+ * `health` is `stalled`. Those are the fields control-plane entries and monitor
48
+ * beats already carry, so a deployment that journals a different vocabulary
49
+ * supplies its own detectors instead of a fork of this module.
50
+ *
51
+ * @since 0.1.0
52
+ */
53
+ export { alertId, coalescingKey, conditions, decide, defaultDetectors, deliveredEventType, Detector, failedEventType, Policy, Rule, Severity } from "./AlertPolicy.js";
54
+ export { AlertRuntime, layer } from "./AlertRuntime.js";
55
+ export { AlertError, FailureCode, layerNoop, Sink } from "./AlertSink.js";
56
+ export { defaultWebhookTimeout, layerWebhook } from "./layerWebhook.js";
57
+ //# sourceMappingURL=Alerts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Alerts.js","sourceRoot":"","sources":["../../src/Alerts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,OAAO,EAEL,OAAO,EACP,aAAa,EACb,UAAU,EACV,MAAM,EACN,gBAAgB,EAChB,kBAAkB,EAClB,QAAQ,EACR,eAAe,EAEf,MAAM,EACN,IAAI,EACJ,QAAQ,EACT,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,YAAY,EAAE,KAAK,EAAkC,MAAM,mBAAmB,CAAA;AACvF,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAoB,MAAM,gBAAgB,CAAA;AAC3F,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA","sourcesContent":["/**\n * Alert policy: run conditions that have lasted too long, turned into durable,\n * coalesced, delivered-once notifications.\n *\n * A notification queue answers \"tell this run something\". An alert answers the\n * question nobody is around to ask: a run has been waiting for an approval for\n * an hour, and the person who could grant it does not know. The two are the\n * same machinery, since an alert is admitted as a coalesced system event, and\n * they differ only in who decides to write one.\n *\n * ## Journal time, not wall time\n *\n * A condition's clock starts at the journal entry that opened it, which makes\n * the decision replayable: the same journal produces the same alerts, whatever\n * process reads it and whenever. A restart re-derives every open condition\n * from the entries rather than from a timer it lost.\n *\n * ## Delivered at least once\n *\n * The admission is idempotent on the notification id, so re-admitting the same\n * alert writes nothing new, and `flows.alerts.delivered` is journaled AFTER\n * the sink accepted the page and is checked before the sink is called again.\n * That ordering is deliberate and it is the reason the guarantee is\n * at-least-once rather than exactly-once: a process that dies between the\n * accepted send and the delivery record has paged, left no evidence of it, and\n * will page again on the next tick. Recording the delivery first turns the\n * same crash into a page nobody ever receives, which for an alert is the worse\n * failure.\n *\n * So the sink owns the last mile: {@link SinkService.deliver} MUST be\n * idempotent on {@link alertId}, which is stable for the life of one condition\n * and reaches the sink on every alert it is handed. A sink that fails journals\n * `flows.alerts.failed` once per failure code and the alert is retried on the\n * next tick, because a refused page is not a delivered page.\n *\n * ## What a refusal costs\n *\n * The queue can refuse the admission when a run already holds its capacity of\n * pending notifications. A tick that is refused pages nobody: the alert comes\n * back in {@link Tick.refused}, the refusal is journaled, and the next tick\n * tries again once a boundary has drained. Calling the sink anyway would page\n * about an alert the run will never see.\n *\n * ## Conditions are data\n *\n * A condition is a payload field with a value: `status` is `waiting-approval`,\n * `health` is `stalled`. Those are the fields control-plane entries and monitor\n * beats already carry, so a deployment that journals a different vocabulary\n * supplies its own detectors instead of a fork of this module.\n *\n * @since 0.1.0\n */\n\nexport {\n type Alert,\n alertId,\n coalescingKey,\n conditions,\n decide,\n defaultDetectors,\n deliveredEventType,\n Detector,\n failedEventType,\n type Open,\n Policy,\n Rule,\n Severity\n} from \"./AlertPolicy.ts\"\nexport { AlertRuntime, layer, type RuntimeService, type Tick } from \"./AlertRuntime.ts\"\nexport { AlertError, FailureCode, layerNoop, Sink, type SinkService } from \"./AlertSink.ts\"\nexport { defaultWebhookTimeout, layerWebhook } from \"./layerWebhook.ts\"\n"]}
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Durable notification payloads and their admission classification.
3
+ *
4
+ * @since 0.1.0
5
+ */
6
+ import { Schema } from "effect";
7
+ /**
8
+ * Producer-supplied attribution carried as data alongside the authenticated
9
+ * source actor. These labels never authorize a notification or a model action.
10
+ * They pass through the journal's ordinary redaction policy.
11
+ *
12
+ * @category models
13
+ * @since 1.0.0
14
+ */
15
+ export declare const Attribution: Schema.$Record<Schema.String, Schema.String>;
16
+ /**
17
+ * Durable origin of a notification: who said it, from where, and at which
18
+ * turn.
19
+ *
20
+ * Provenance travels with the notification because the run that receives one
21
+ * is not the run that wrote it. An operator steer, a parent run's event, and a
22
+ * webhook all arrive on the same queue, and only these fields say which is
23
+ * which after the fact.
24
+ *
25
+ * @category models
26
+ * @since 0.1.0
27
+ */
28
+ export declare const Provenance: Schema.Struct<{
29
+ readonly sourceRunId: Schema.String;
30
+ readonly sourceLineageId: Schema.String;
31
+ readonly sourceTurn: Schema.Int;
32
+ readonly sourceActor: Schema.String;
33
+ readonly attribution: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
34
+ }>;
35
+ /**
36
+ * The decoded form of {@link Provenance}.
37
+ *
38
+ * @category models
39
+ * @since 0.1.0
40
+ */
41
+ export type Provenance = typeof Provenance.Type;
42
+ /**
43
+ * A human message that must reach the model before the current turn closes.
44
+ *
45
+ * `id` is the caller's idempotency key: admitting it twice with the same
46
+ * content is one notification, and admitting it with different content is a
47
+ * producer bug the queue refuses. New admissions fingerprint the validated
48
+ * content before journal redaction, so redaction does not change retry identity.
49
+ *
50
+ * @category models
51
+ * @since 0.1.0
52
+ */
53
+ export declare const HumanSteer: Schema.TaggedStruct<"human-steer", {
54
+ readonly delivery: Schema.Literal<"steer">;
55
+ readonly id: Schema.NonEmptyString;
56
+ readonly targetLineageId: Schema.NonEmptyString;
57
+ readonly provenance: Schema.Struct<{
58
+ readonly sourceRunId: Schema.String;
59
+ readonly sourceLineageId: Schema.String;
60
+ readonly sourceTurn: Schema.Int;
61
+ readonly sourceActor: Schema.String;
62
+ readonly attribution: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
63
+ }>;
64
+ readonly payload: Schema.Codec<Schema.Json, Schema.Json, never, never>;
65
+ }>;
66
+ /**
67
+ * The decoded form of {@link HumanSteer}.
68
+ *
69
+ * @category models
70
+ * @since 0.1.0
71
+ */
72
+ export type HumanSteer = typeof HumanSteer.Type;
73
+ /**
74
+ * A human message that waits for the run to become idle.
75
+ *
76
+ * The delivery class is the whole difference from {@link HumanSteer}: a
77
+ * follow-up is not urgent enough to interrupt the turn in flight, so it is
78
+ * promoted one at a time when the run would otherwise have nothing to do.
79
+ *
80
+ * @category models
81
+ * @since 0.1.0
82
+ */
83
+ export declare const HumanFollowup: Schema.TaggedStruct<"human-followup", {
84
+ readonly delivery: Schema.Literal<"queue">;
85
+ readonly id: Schema.NonEmptyString;
86
+ readonly targetLineageId: Schema.NonEmptyString;
87
+ readonly provenance: Schema.Struct<{
88
+ readonly sourceRunId: Schema.String;
89
+ readonly sourceLineageId: Schema.String;
90
+ readonly sourceTurn: Schema.Int;
91
+ readonly sourceActor: Schema.String;
92
+ readonly attribution: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
93
+ }>;
94
+ readonly payload: Schema.Codec<Schema.Json, Schema.Json, never, never>;
95
+ }>;
96
+ /**
97
+ * The decoded form of {@link HumanFollowup}.
98
+ *
99
+ * @category models
100
+ * @since 0.1.0
101
+ */
102
+ export type HumanFollowup = typeof HumanFollowup.Type;
103
+ /**
104
+ * A machine-originated event. Events sharing a coalescing key and a target
105
+ * lineage collapse to the most recent one while pending.
106
+ *
107
+ * Coalescing is what keeps a chatty producer from filling the queue: ten
108
+ * updates about one condition are one pending notification carrying the latest
109
+ * of them. The key is scoped to `targetLineageId`, so two branches reporting
110
+ * one condition under the same ordinary key each keep their own event. An
111
+ * event with no key never coalesces.
112
+ *
113
+ * @category models
114
+ * @since 0.1.0
115
+ */
116
+ export declare const SystemEvent: Schema.TaggedStruct<"system-event", {
117
+ readonly delivery: Schema.Literal<"queue">;
118
+ readonly coalescingKey: Schema.optional<Schema.String>;
119
+ readonly id: Schema.NonEmptyString;
120
+ readonly targetLineageId: Schema.NonEmptyString;
121
+ readonly provenance: Schema.Struct<{
122
+ readonly sourceRunId: Schema.String;
123
+ readonly sourceLineageId: Schema.String;
124
+ readonly sourceTurn: Schema.Int;
125
+ readonly sourceActor: Schema.String;
126
+ readonly attribution: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
127
+ }>;
128
+ readonly payload: Schema.Codec<Schema.Json, Schema.Json, never, never>;
129
+ }>;
130
+ /**
131
+ * The decoded form of {@link SystemEvent}.
132
+ *
133
+ * @category models
134
+ * @since 0.1.0
135
+ */
136
+ export type SystemEvent = typeof SystemEvent.Type;
137
+ /**
138
+ * Any durable notification retained by the pending queue.
139
+ *
140
+ * `NotificationQueue.admit` decodes its argument against this union before it
141
+ * journals anything, so a value that does not match it is refused rather than
142
+ * acknowledged and then skipped by every replay. Journal-backed queues deeply
143
+ * freeze decoded notifications, including payloads, before returning them from
144
+ * `pending` or `drain`.
145
+ *
146
+ * @category models
147
+ * @since 0.1.0
148
+ */
149
+ export declare const Notification: Schema.Union<readonly [Schema.TaggedStruct<"human-steer", {
150
+ readonly delivery: Schema.Literal<"steer">;
151
+ readonly id: Schema.NonEmptyString;
152
+ readonly targetLineageId: Schema.NonEmptyString;
153
+ readonly provenance: Schema.Struct<{
154
+ readonly sourceRunId: Schema.String;
155
+ readonly sourceLineageId: Schema.String;
156
+ readonly sourceTurn: Schema.Int;
157
+ readonly sourceActor: Schema.String;
158
+ readonly attribution: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
159
+ }>;
160
+ readonly payload: Schema.Codec<Schema.Json, Schema.Json, never, never>;
161
+ }>, Schema.TaggedStruct<"human-followup", {
162
+ readonly delivery: Schema.Literal<"queue">;
163
+ readonly id: Schema.NonEmptyString;
164
+ readonly targetLineageId: Schema.NonEmptyString;
165
+ readonly provenance: Schema.Struct<{
166
+ readonly sourceRunId: Schema.String;
167
+ readonly sourceLineageId: Schema.String;
168
+ readonly sourceTurn: Schema.Int;
169
+ readonly sourceActor: Schema.String;
170
+ readonly attribution: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
171
+ }>;
172
+ readonly payload: Schema.Codec<Schema.Json, Schema.Json, never, never>;
173
+ }>, Schema.TaggedStruct<"system-event", {
174
+ readonly delivery: Schema.Literal<"queue">;
175
+ readonly coalescingKey: Schema.optional<Schema.String>;
176
+ readonly id: Schema.NonEmptyString;
177
+ readonly targetLineageId: Schema.NonEmptyString;
178
+ readonly provenance: Schema.Struct<{
179
+ readonly sourceRunId: Schema.String;
180
+ readonly sourceLineageId: Schema.String;
181
+ readonly sourceTurn: Schema.Int;
182
+ readonly sourceActor: Schema.String;
183
+ readonly attribution: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
184
+ }>;
185
+ readonly payload: Schema.Codec<Schema.Json, Schema.Json, never, never>;
186
+ }>]>;
187
+ /**
188
+ * Any durable notification retained by the pending queue.
189
+ *
190
+ * @category models
191
+ * @since 0.1.0
192
+ */
193
+ export type Notification = typeof Notification.Type;
194
+ /**
195
+ * How a notification is allowed to reach the model.
196
+ *
197
+ * `steer` notifications are promoted in a batch at turn close; `queue`
198
+ * notifications are promoted one at a time when the run would otherwise idle.
199
+ *
200
+ * @param notification the notification to classify
201
+ * @category getters
202
+ * @since 0.1.0
203
+ */
204
+ export declare const admissionClass: (notification: Notification) => "steer" | "queue";
205
+ /**
206
+ * The key a pending notification coalesces on, or `null` when it must never
207
+ * be coalesced. Only system events coalesce, and only when they declare a key.
208
+ *
209
+ * @param notification the notification to key
210
+ * @category getters
211
+ * @since 0.1.0
212
+ */
213
+ export declare const coalesceKey: (notification: Notification) => string | null;
214
+ //# sourceMappingURL=Notification.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Notification.d.ts","sourceRoot":"","sources":["../../src/Notification.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE/B;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,8CAA8C,CAAA;AAEtE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,UAAU;;;;;;EAMrB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC,IAAI,CAAA;AAS/C;;;;;;;;;;GAUG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;EAGrB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC,IAAI,CAAA;AAE/C;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;EAGxB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,aAAa,CAAC,IAAI,CAAA;AAErD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;EAItB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,WAAW,CAAC,IAAI,CAAA;AAEjD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAAyD,CAAA;AAElF;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,OAAO,YAAY,CAAC,IAAI,CAAA;AAEnD;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAAI,cAAc,YAAY,KAAG,OAAO,GAAG,OAAgC,CAAA;AAEtG;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,GAAI,cAAc,YAAY,KAAG,MAAM,GAAG,IACgB,CAAA"}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Durable notification payloads and their admission classification.
3
+ *
4
+ * @since 0.1.0
5
+ */
6
+ import { Schema } from "effect";
7
+ /**
8
+ * Producer-supplied attribution carried as data alongside the authenticated
9
+ * source actor. These labels never authorize a notification or a model action.
10
+ * They pass through the journal's ordinary redaction policy.
11
+ *
12
+ * @category models
13
+ * @since 1.0.0
14
+ */
15
+ export const Attribution = Schema.Record(Schema.String, Schema.String);
16
+ /**
17
+ * Durable origin of a notification: who said it, from where, and at which
18
+ * turn.
19
+ *
20
+ * Provenance travels with the notification because the run that receives one
21
+ * is not the run that wrote it. An operator steer, a parent run's event, and a
22
+ * webhook all arrive on the same queue, and only these fields say which is
23
+ * which after the fact.
24
+ *
25
+ * @category models
26
+ * @since 0.1.0
27
+ */
28
+ export const Provenance = Schema.Struct({
29
+ sourceRunId: Schema.String,
30
+ sourceLineageId: Schema.String,
31
+ sourceTurn: Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0))),
32
+ sourceActor: Schema.String,
33
+ attribution: Schema.optional(Attribution)
34
+ });
35
+ const common = {
36
+ id: Schema.NonEmptyString,
37
+ targetLineageId: Schema.NonEmptyString,
38
+ provenance: Provenance,
39
+ payload: Schema.Json
40
+ };
41
+ /**
42
+ * A human message that must reach the model before the current turn closes.
43
+ *
44
+ * `id` is the caller's idempotency key: admitting it twice with the same
45
+ * content is one notification, and admitting it with different content is a
46
+ * producer bug the queue refuses. New admissions fingerprint the validated
47
+ * content before journal redaction, so redaction does not change retry identity.
48
+ *
49
+ * @category models
50
+ * @since 0.1.0
51
+ */
52
+ export const HumanSteer = Schema.TaggedStruct("human-steer", {
53
+ ...common,
54
+ delivery: Schema.Literal("steer")
55
+ });
56
+ /**
57
+ * A human message that waits for the run to become idle.
58
+ *
59
+ * The delivery class is the whole difference from {@link HumanSteer}: a
60
+ * follow-up is not urgent enough to interrupt the turn in flight, so it is
61
+ * promoted one at a time when the run would otherwise have nothing to do.
62
+ *
63
+ * @category models
64
+ * @since 0.1.0
65
+ */
66
+ export const HumanFollowup = Schema.TaggedStruct("human-followup", {
67
+ ...common,
68
+ delivery: Schema.Literal("queue")
69
+ });
70
+ /**
71
+ * A machine-originated event. Events sharing a coalescing key and a target
72
+ * lineage collapse to the most recent one while pending.
73
+ *
74
+ * Coalescing is what keeps a chatty producer from filling the queue: ten
75
+ * updates about one condition are one pending notification carrying the latest
76
+ * of them. The key is scoped to `targetLineageId`, so two branches reporting
77
+ * one condition under the same ordinary key each keep their own event. An
78
+ * event with no key never coalesces.
79
+ *
80
+ * @category models
81
+ * @since 0.1.0
82
+ */
83
+ export const SystemEvent = Schema.TaggedStruct("system-event", {
84
+ ...common,
85
+ delivery: Schema.Literal("queue"),
86
+ coalescingKey: Schema.optional(Schema.String)
87
+ });
88
+ /**
89
+ * Any durable notification retained by the pending queue.
90
+ *
91
+ * `NotificationQueue.admit` decodes its argument against this union before it
92
+ * journals anything, so a value that does not match it is refused rather than
93
+ * acknowledged and then skipped by every replay. Journal-backed queues deeply
94
+ * freeze decoded notifications, including payloads, before returning them from
95
+ * `pending` or `drain`.
96
+ *
97
+ * @category models
98
+ * @since 0.1.0
99
+ */
100
+ export const Notification = Schema.Union([HumanSteer, HumanFollowup, SystemEvent]);
101
+ /**
102
+ * How a notification is allowed to reach the model.
103
+ *
104
+ * `steer` notifications are promoted in a batch at turn close; `queue`
105
+ * notifications are promoted one at a time when the run would otherwise idle.
106
+ *
107
+ * @param notification the notification to classify
108
+ * @category getters
109
+ * @since 0.1.0
110
+ */
111
+ export const admissionClass = (notification) => notification.delivery;
112
+ /**
113
+ * The key a pending notification coalesces on, or `null` when it must never
114
+ * be coalesced. Only system events coalesce, and only when they declare a key.
115
+ *
116
+ * @param notification the notification to key
117
+ * @category getters
118
+ * @since 0.1.0
119
+ */
120
+ export const coalesceKey = (notification) => notification._tag === "system-event" ? notification.coalescingKey ?? null : null;
121
+ //# sourceMappingURL=Notification.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Notification.js","sourceRoot":"","sources":["../../src/Notification.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE/B;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;AAEtE;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC;IACtC,WAAW,EAAE,MAAM,CAAC,MAAM;IAC1B,eAAe,EAAE,MAAM,CAAC,MAAM;IAC9B,UAAU,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3E,WAAW,EAAE,MAAM,CAAC,MAAM;IAC1B,WAAW,EAAE,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC;CAC1C,CAAC,CAAA;AAUF,MAAM,MAAM,GAAG;IACb,EAAE,EAAE,MAAM,CAAC,cAAc;IACzB,eAAe,EAAE,MAAM,CAAC,cAAc;IACtC,UAAU,EAAE,UAAU;IACtB,OAAO,EAAE,MAAM,CAAC,IAAI;CACrB,CAAA;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,YAAY,CAAC,aAAa,EAAE;IAC3D,GAAG,MAAM;IACT,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;CAClC,CAAC,CAAA;AAUF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC,YAAY,CAAC,gBAAgB,EAAE;IACjE,GAAG,MAAM;IACT,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;CAClC,CAAC,CAAA;AAUF;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,YAAY,CAAC,cAAc,EAAE;IAC7D,GAAG,MAAM;IACT,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;IACjC,aAAa,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;CAC9C,CAAC,CAAA;AAUF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,UAAU,EAAE,aAAa,EAAE,WAAW,CAAC,CAAC,CAAA;AAUlF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,YAA0B,EAAqB,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAA;AAEtG;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,YAA0B,EAAiB,EAAE,CACvE,YAAY,CAAC,IAAI,KAAK,cAAc,CAAC,CAAC,CAAC,YAAY,CAAC,aAAa,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA","sourcesContent":["/**\n * Durable notification payloads and their admission classification.\n *\n * @since 0.1.0\n */\n\nimport { Schema } from \"effect\"\n\n/**\n * Producer-supplied attribution carried as data alongside the authenticated\n * source actor. These labels never authorize a notification or a model action.\n * They pass through the journal's ordinary redaction policy.\n *\n * @category models\n * @since 1.0.0\n */\nexport const Attribution = Schema.Record(Schema.String, Schema.String)\n\n/**\n * Durable origin of a notification: who said it, from where, and at which\n * turn.\n *\n * Provenance travels with the notification because the run that receives one\n * is not the run that wrote it. An operator steer, a parent run's event, and a\n * webhook all arrive on the same queue, and only these fields say which is\n * which after the fact.\n *\n * @category models\n * @since 0.1.0\n */\nexport const Provenance = Schema.Struct({\n sourceRunId: Schema.String,\n sourceLineageId: Schema.String,\n sourceTurn: Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0))),\n sourceActor: Schema.String,\n attribution: Schema.optional(Attribution)\n})\n\n/**\n * The decoded form of {@link Provenance}.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Provenance = typeof Provenance.Type\n\nconst common = {\n id: Schema.NonEmptyString,\n targetLineageId: Schema.NonEmptyString,\n provenance: Provenance,\n payload: Schema.Json\n}\n\n/**\n * A human message that must reach the model before the current turn closes.\n *\n * `id` is the caller's idempotency key: admitting it twice with the same\n * content is one notification, and admitting it with different content is a\n * producer bug the queue refuses. New admissions fingerprint the validated\n * content before journal redaction, so redaction does not change retry identity.\n *\n * @category models\n * @since 0.1.0\n */\nexport const HumanSteer = Schema.TaggedStruct(\"human-steer\", {\n ...common,\n delivery: Schema.Literal(\"steer\")\n})\n\n/**\n * The decoded form of {@link HumanSteer}.\n *\n * @category models\n * @since 0.1.0\n */\nexport type HumanSteer = typeof HumanSteer.Type\n\n/**\n * A human message that waits for the run to become idle.\n *\n * The delivery class is the whole difference from {@link HumanSteer}: a\n * follow-up is not urgent enough to interrupt the turn in flight, so it is\n * promoted one at a time when the run would otherwise have nothing to do.\n *\n * @category models\n * @since 0.1.0\n */\nexport const HumanFollowup = Schema.TaggedStruct(\"human-followup\", {\n ...common,\n delivery: Schema.Literal(\"queue\")\n})\n\n/**\n * The decoded form of {@link HumanFollowup}.\n *\n * @category models\n * @since 0.1.0\n */\nexport type HumanFollowup = typeof HumanFollowup.Type\n\n/**\n * A machine-originated event. Events sharing a coalescing key and a target\n * lineage collapse to the most recent one while pending.\n *\n * Coalescing is what keeps a chatty producer from filling the queue: ten\n * updates about one condition are one pending notification carrying the latest\n * of them. The key is scoped to `targetLineageId`, so two branches reporting\n * one condition under the same ordinary key each keep their own event. An\n * event with no key never coalesces.\n *\n * @category models\n * @since 0.1.0\n */\nexport const SystemEvent = Schema.TaggedStruct(\"system-event\", {\n ...common,\n delivery: Schema.Literal(\"queue\"),\n coalescingKey: Schema.optional(Schema.String)\n})\n\n/**\n * The decoded form of {@link SystemEvent}.\n *\n * @category models\n * @since 0.1.0\n */\nexport type SystemEvent = typeof SystemEvent.Type\n\n/**\n * Any durable notification retained by the pending queue.\n *\n * `NotificationQueue.admit` decodes its argument against this union before it\n * journals anything, so a value that does not match it is refused rather than\n * acknowledged and then skipped by every replay. Journal-backed queues deeply\n * freeze decoded notifications, including payloads, before returning them from\n * `pending` or `drain`.\n *\n * @category models\n * @since 0.1.0\n */\nexport const Notification = Schema.Union([HumanSteer, HumanFollowup, SystemEvent])\n\n/**\n * Any durable notification retained by the pending queue.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Notification = typeof Notification.Type\n\n/**\n * How a notification is allowed to reach the model.\n *\n * `steer` notifications are promoted in a batch at turn close; `queue`\n * notifications are promoted one at a time when the run would otherwise idle.\n *\n * @param notification the notification to classify\n * @category getters\n * @since 0.1.0\n */\nexport const admissionClass = (notification: Notification): \"steer\" | \"queue\" => notification.delivery\n\n/**\n * The key a pending notification coalesces on, or `null` when it must never\n * be coalesced. Only system events coalesce, and only when they declare a key.\n *\n * @param notification the notification to key\n * @category getters\n * @since 0.1.0\n */\nexport const coalesceKey = (notification: Notification): string | null =>\n notification._tag === \"system-event\" ? notification.coalescingKey ?? null : null\n"]}