stitchkit 0.102.0 → 0.103.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 (32) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/application/diagnostic-journal-contract.d.ts +46 -7
  3. package/dist/application/diagnostic-journal-contract.d.ts.map +1 -1
  4. package/dist/application/diagnostic-journal-frame.d.ts +9 -0
  5. package/dist/application/diagnostic-journal-frame.d.ts.map +1 -0
  6. package/dist/application/diagnostic-journal-manager.d.ts.map +1 -1
  7. package/dist/application/diagnostic-journal-read-contract.d.ts +110 -0
  8. package/dist/application/diagnostic-journal-read-contract.d.ts.map +1 -0
  9. package/dist/application/diagnostic-journal-reader.d.ts +13 -0
  10. package/dist/application/diagnostic-journal-reader.d.ts.map +1 -0
  11. package/dist/application/diagnostic-journal-recovery.d.ts +4 -0
  12. package/dist/application/diagnostic-journal-recovery.d.ts.map +1 -0
  13. package/dist/application/diagnostic-journal-storage.d.ts +2 -0
  14. package/dist/application/diagnostic-journal-storage.d.ts.map +1 -1
  15. package/dist/entrypoints/application/diagnostic-journal.d.ts +1 -0
  16. package/dist/entrypoints/application/diagnostic-journal.d.ts.map +1 -1
  17. package/dist/entrypoints/application/diagnostic-journal.js +213 -19
  18. package/dist/entrypoints/application.d.ts +1 -0
  19. package/dist/entrypoints/application.d.ts.map +1 -1
  20. package/dist/entrypoints/application.js +10 -2
  21. package/dist/index-z94wht4y.js +156 -0
  22. package/llms/application-diagnostic-journal.txt +2 -0
  23. package/llms/application.2.txt +157 -267
  24. package/llms/application.3.txt +278 -0
  25. package/llms/application.txt +43 -158
  26. package/llms/stitchkit.txt +4 -4
  27. package/llms/{upgrading-0.75.0-0.102.0.txt → upgrading-0.75.0-0.103.0.txt} +32 -3
  28. package/llms/upgrading.txt +1 -1
  29. package/llms-full.txt +80 -8
  30. package/llms.txt +4 -4
  31. package/package.json +1 -1
  32. package/dist/index-h124epcf.js +0 -103
@@ -1,16 +1,171 @@
1
- # stitchkit/application — stitchkit docs slice, part 2 of 2
1
+ # stitchkit/application — stitchkit docs slice, part 2 of 3
2
2
 
3
3
  > Generated from the `docs/` tree by `bun run gen:llms`. The index of all slices is
4
4
  > `llms.txt` in this package.
5
+ > Continues in `llms/application.3.txt`.
5
6
 
6
7
  Parts of this slice:
7
8
  - `llms/application.txt`: Guide: Managed application kernel (docs/guide/application-kernel.md)
8
- - `llms/application.2.txt`: Guide: Managed application kernel (docs/guide/application-kernel.md) · Guide: Application migration recipes (docs/guide/application-migration-recipes.md) · API reference: `stitchkit/application` (docs/api/reference.md)
9
+ - `llms/application.2.txt`: Guide: Managed application kernel (docs/guide/application-kernel.md) · Guide: Application migration recipes (docs/guide/application-migration-recipes.md)
10
+ - `llms/application.3.txt`: API reference: `stitchkit/application` (docs/api/reference.md)
9
11
 
10
12
  ==============================================================================
11
13
  # Guide: Managed application kernel (docs/guide/application-kernel.md) (continued)
12
14
  ==============================================================================
13
15
 
16
+ ## Durable process facts and owner notifications
17
+
18
+ `createProcessLifecycleLedger` records a bounded versioned list of process runs.
19
+ Start, readiness, draining and shutdown target both `runId` and `pid`, so hot
20
+ reload and PID reuse cannot close the wrong generation. `lifecycleLedgerResource`
21
+ drives it from the kernel's phases: `start` records the start, `activate` the
22
+ readiness, `stopAdmission` the drain — the run's `unavailableAt`, the moment it
23
+ stops answering new work — and `close` / `force` the stop. A run that stops
24
+ without ever becoming ready (a failed startup rolls back with `close` only)
25
+ ends as `startup-failed`, not `clean`.
26
+
27
+ A start classifies what happened to the newest run before it, and the
28
+ classification is decided by four facts — whether that run recorded its own
29
+ exit, whether the pid is the same, whether the version changed and whether it
30
+ ever became ready:
31
+
32
+ | newest run | same pid | version | `previousExit` | predecessor's `termination` |
33
+ |---|---|---|---|---|
34
+ | none | — | — | `first-boot` | — |
35
+ | recorded `stoppedAt` | — | — | `clean` / `forced` / `startup-failed` / `abnormal` as recorded | unchanged |
36
+ | still open | yes | — | `hot-reload` | `hot-reload`, closed at the new start |
37
+ | still open | no | changed | `handoff` (default) | stays open; it records its own stop later |
38
+ | still open | no | changed, `versionChangeOverlap: 'abnormal'` | `abnormal` / `startup-failed` | closed at the new start, as for the same version |
39
+ | still open, was ready | no | same | `abnormal` (default) | `abnormal`, closed at the new start — an upper bound, the crash time is unknown |
40
+ | still open, never ready | no | same | `startup-failed` (default) | `startup-failed`, closed at the new start |
41
+
42
+ Every other open run the start finds was abandoned too and is closed the same
43
+ way — two crashes in a row leave two open runs, and both are closed, not only
44
+ the newest. Open means `active` or `draining`.
45
+
46
+ A version of `unknown` on either side is never a version change, so a dev build
47
+ after a crashed release reads `abnormal`, not `handoff`. `forced` means the
48
+ process itself acknowledged a kill; `abnormal` means a successor found it dead.
49
+
50
+ ### Downtime is the window nobody answered in
51
+
52
+ `ReadyFact.downtimeMs` is `readyAt` of the new run minus `unavailableAt` of the
53
+ last run that served — the predecessor's drain plus the successor's boot. A
54
+ 110 s forced drain followed by a 4 s boot is 114 s of downtime; the gap between
55
+ the two processes alone would read 4 s before and after that defect was fixed.
56
+ Runs that never became ready answered nobody, so the count passes them: after a
57
+ failed start it still opens where the last serving run stopped answering. It is
58
+ `0` when that run still answers (a handoff whose predecessor has not drained
59
+ yet) and `null` on first boot and when the end was never timed — a crash or a
60
+ hot reload records no `unavailableAt`. `unavailableSince` names the moment it is
61
+ counted from. The gap between two processes is still there, under its own
62
+ name: `StartFact.processGapMs` is the start minus the predecessor's recorded
63
+ stop.
64
+
65
+ The ledger records what the process says while it can say it. stitchkit never
66
+ calls `process.exit` (ADR 0074), so a stop is recorded on the kernel's `close`
67
+ or `force` path, both awaited. An application that exits the process on its
68
+ own before its shutdown settles loses that last write; the successor then
69
+ finds the run open and closes it as `abnormal`, keeping the `unavailableAt`
70
+ already recorded — which is why the drain moment is written first, at
71
+ `stopAdmission`, and the downtime survives a stop that was never recorded.
72
+
73
+ The `abnormal` default is a choice, and it is the single-process deployment:
74
+ one process per build, a new pid of the same build means the old one stopped
75
+ answering. Where processes of **one** build overlap on purpose — a cluster, a
76
+ zero-downtime reload of the same build — pass `sameVersionOverlap: 'handoff'`
77
+ to the ledger, and every open run stays open until it records its own
78
+ shutdown. The cost of that setting is symmetric: a real crash under it is
79
+ reported as a handoff and the dead run stays open in the ledger until
80
+ retention drops it.
81
+
82
+ A version change is the other axis, and its default is the opposite:
83
+ `handoff`, because a release that starts the new build before stopping the
84
+ old one is the common zero-downtime shape. Where builds never overlap — one
85
+ process per deployment (a pm2 fork, a systemd unit, one container), the old
86
+ one stopped before the new one starts — pass `versionChangeOverlap:
87
+ 'abnormal'`. An open predecessor of another build then died without
88
+ recording its exit, most often killed during the release itself: it is closed
89
+ as `abnormal` (or `startup-failed`) at the new start, `downtimeMs` is `null`
90
+ instead of `0`, and `previousVersion` / `versionChanged` still describe the
91
+ release. A predecessor that did record its stop is read as before, and its
92
+ downtime is measured. The two settings compose: `sameVersionOverlap:
93
+ 'handoff'` keeps open only the runs of a build allowed to overlap the new one.
94
+
95
+ The list is kept in the order the transitions wrote it —
96
+ every write goes through one atomic update, so that order is the causal one,
97
+ and a successor whose clock lags its predecessor still finds it at the head;
98
+ `startedAt` is data, not the sort key. Retention (`retain`, default 20) drops
99
+ finished runs first and an open one — a live handoff predecessor — only when
100
+ nothing finished is left. Facts are published through the ledger's own
101
+ subscription and resource value; `ApplicationEventSink` remains the strict
102
+ application-state stream.
103
+
104
+ `createNotificationOutbox` is the transport-neutral durable delivery side. It
105
+ persists before send, claims an item with an expiring lease, carries one stable
106
+ idempotency key into the transport, retries under an injected clock and records
107
+ terminal drops. The guarantee is at-least-once: a crash after remote acceptance
108
+ but before the receipt is persisted may redeliver, so transports should use the
109
+ key when they support deduplication. The retry budget is sized for the outage
110
+ an owner notification has to outlive, not for a flaky call: the default backoff
111
+ doubles from one second and caps at sixty (`backoffDelay`, the one formula the
112
+ client's resumable streams also use, with jitter 0), and the 99 waits between
113
+ the default `maxAttempts` of 100 sum to 1+2+4+8+16+32 s plus 93 × 60 s — about
114
+ 94 minutes — before `onDropped` sees `attempt-limit`.
115
+ `state()` is a read and never fails on the bounds a transition enforces — a file
116
+ that grew past `maxStateBytes` under an older limit is inspectable and is
117
+ trimmed by the next transition. Two bounds do fail loudly: `enqueue` past
118
+ `maxQueue` throws rather than dropping silently, and a `backoffMs` that returns
119
+ `NaN`, a negative or an infinite delay rejects the flush. `stop()` lets the
120
+ send in flight finish and claims nothing more; a `send` without its own
121
+ deadline holds `stop()` — and so the resource's `force()` — for that one call,
122
+ so give the transport a timeout. Superseding a key that is being sent right now
123
+ does not recall it: the send completes, and only its receipt is not written.
124
+
125
+ `createDirectoryInbox` is the receiving side for another program on the same
126
+ host: it drops entries — a release transition, say — into a directory, and
127
+ the application takes each one at least once. It is a managed resource of its
128
+ own (`createDirectoryInbox({ id, directory, schema, handle })`, imported from
129
+ `stitchkit/application/directory-inbox` because it touches files; its types
130
+ and schemas are in `stitchkit/application`) and publishes a handle with
131
+ `flush()` and `state()`. The producer writes each entry
132
+ atomically as `<name>.json` — a temporary name, then a rename; names that start
133
+ with a dot are never read. Entries are delivered in name order, and only after
134
+ `activate`: none reaches the application before it is ready. `stopAdmission`
135
+ stops taking new ones, `drain` waits for the one in flight, and `force` aborts
136
+ the `signal` its handler was given.
137
+
138
+ Taken and done are separate durable records. Before `handle` runs, the entry
139
+ is claimed with a lease (`leaseMs`, default five minutes); after it returns,
140
+ a receipt replaces the claim and only then is the file removed. A process that
141
+ died mid-delivery leaves a claim, and the entry is taken again once the lease
142
+ runs out — at least once, not exactly once, so a handler that sends somewhere
143
+ should carry the entry's `key` as its idempotency key. A process that died
144
+ after the receipt leaves only the file, which the next pass removes without
145
+ delivering it again. A handler that throws is retried after a backoff from one
146
+ second doubling to five minutes; an entry that fails `schema`, is over
147
+ `maxEntryBytes` or was taken `maxAttempts` times (default 20 — the count
148
+ survives restarts, so an entry whose handling kills the process is not taken
149
+ forever) is moved to `rejected/` with its reason and reported to `onRejected`,
150
+ and the entries after it keep flowing. The state lives in
151
+ `<directory>/.inbox-state.json` under `createFileStateStore`'s inter-process
152
+ lock unless a `store` is supplied.
153
+
154
+ These primitives depend on the structural `StateStore`. On a server,
155
+ `createFileStateStore` supplies the shared Zod-validated JSON adapter with an
156
+ inter-process lock, unique temporary file, fsync and atomic rename. The lock
157
+ is a file with a heartbeat: the holder refreshes its mtime every third of
158
+ `staleLockMs` (default 3 s), a contender waits up to `lockTimeoutMs` (default
159
+ 10 s), and the stale bound must sit inside the timeout — otherwise a crashed
160
+ holder blocks every update until the lock ages out, which the constructor
161
+ refuses. A lock whose heartbeat is stale is reclaimed once its recorded pid is
162
+ gone; a live or unverifiable pid (a reused number, another user's process)
163
+ keeps it for ten stale bounds — and never fewer than thirty missed heartbeats,
164
+ so a scheduler stall on a loaded host cannot pull a live lock from under its
165
+ holder — after which the heartbeat wins and the lock is abandoned. Temporary files a crashed writer left beside the state are swept on
166
+ the store's first update. Ledger corruption may be declared reconstructable;
167
+ an outbox must fail closed rather than silently discard pending delivery.
168
+
14
169
  ## Recovering change subscriptions
15
170
 
16
171
  `changeSubscriptionResource` is an optional resource over a consumer-owned driver.
@@ -330,268 +485,3 @@ Keep the product code that still owns durable state and policy:
330
485
  Run the old and new lifecycle paths separately during development if needed,
331
486
  but do not ship both. Before cutover, verify there is exactly one signal binding,
332
487
  one timer owner, one admission gate and one resource cleanup chain.
333
-
334
- ==============================================================================
335
- # API reference: `stitchkit/application` (docs/api/reference.md)
336
- ==============================================================================
337
-
338
- ## `stitchkit/application`
339
-
340
- Server-only process-local application composition. See the
341
- [application kernel guide](../guide/application-kernel.md) and
342
- [architecture](../architecture/application-kernel.md). Complete consumer
343
- cutovers are covered by the executable
344
- [migration recipes](../guide/application-migration-recipes.md).
345
-
346
- ### Kernel and resources
347
-
348
- | Export | Kind | Summary |
349
- |--------|------|---------|
350
- | `createApplication` | function | compose a validated resource DAG into one startup, readiness, admission and shutdown state machine |
351
- | `StateStore` / `StateStoreUpdate` | _type_ | async atomic read/update boundary shared by durable application state machines |
352
- | `createProcessLifecycleLedger` / `ProcessLifecycleLedger` / `ProcessLifecycleLedgerConfig` | function / _type_ | durable bounded run history and typed fact subscription |
353
- | `LifecycleRunSchema` / `LifecycleRun` / `LifecycleStateSchema` / `LifecycleState` / `LifecycleTerminationSchema` / `LifecycleTermination` | schema / _type_ | versioned process-run state — open (`active`, `draining` with `unavailableAt`) and ended (`clean`, `forced`, `startup-failed`, `hot-reload`, `abnormal`) |
354
- | `PreviousExitSchema` / `PreviousExit` / `StartFact` / `ReadyFact` / `DrainingFact` / `ShutdownFact` / `ProcessLifecycleFact` | schema / _type_ | first-boot, hot-reload, clean, forced, startup-failed, handoff and abnormal lifecycle facts; `processGapMs` on start, `downtimeMs` (unavailable → ready) on ready |
355
- | `transitionProcessStart` / `transitionProcessReady` / `transitionProcessDraining` / `transitionProcessShutdown` | function | pure idempotent runId+pid-owned transitions |
356
- | `LifecycleTransition` / `TransitionStartInput` / `TransitionRunInput` / `TransitionShutdownInput` / `SameVersionOverlap` / `VersionChangeOverlap` | _type_ | pure transition inputs (`TransitionRunInput` for ready and draining, extended with `forced` for shutdown), the same-build overlap policy (`abnormal` default / `handoff`), the version-change overlap policy (`handoff` default / `abnormal`) and state+fact result |
357
- | `lifecycleLedgerResource` / `LifecycleLedgerResource` / `LifecycleLedgerResourceConfig` | function / _type_ | managed-resource adapter that records start, readiness, admission stop (draining) and shutdown |
358
- | `createNotificationOutbox` / `NotificationOutbox` / `NotificationOutboxConfig` | function / _type_ | bounded at-least-once queue with durable claims, leases, retries and supersede |
359
- | `NotificationOutboxItem` / `NotificationOutboxReceipt` / `NotificationOutboxState` / `NotificationSend` / `NotificationFailureClassification` / `DroppedNotification` / `EnqueueNotification` | _type_ | versioned queue records, stable send identity and delivery decisions |
360
- | `notificationOutboxStateSchema` | function | strict persisted-state schema over an application payload schema |
361
- | `notificationOutboxResource` / `NotificationOutboxResource` / `NotificationOutboxResourceConfig` | function / _type_ | thin managed-resource scheduler over an outbox handle |
362
- | `createDirectoryInbox` (from `stitchkit/application/directory-inbox`) / `DirectoryInbox` / `DirectoryInboxConfig` / `DirectoryInboxDelivery` / `DirectoryInboxResource` | function / _type_ | managed resource delivering each `<name>.json` entry another program drops into a directory to the application at least once, after readiness; separate durable taken/done records, leases, bounded retry |
363
- | `DirectoryInboxStateSchema` / `DirectoryInboxState` / `DirectoryInboxRejection` / `DirectoryInboxRejectionReasonSchema` / `DirectoryInboxRejectionReason` | schema / _type_ | the inbox's claims, receipts and rejections (`invalid`, `too-large`, `attempt-limit`) |
364
- | `ApplicationHandle.restart` | method | replace one resource and everything that depends on it, leaving the rest of the graph running and the process epoch unchanged |
365
- | `ApplicationRestartInputSchema` / `ApplicationRestartInput` | schema / _type_ | the resource to replace, by id, and optionally `gracePeriodMs` / `forceTimeoutMs` for this restart — the application's own shutdown budget otherwise |
366
- | `ApplicationRestartResultSchema` / `ApplicationRestartResult` | schema / _type_ | the subtree that was actually taken down and brought back, in start order, with the outcome, the reason on anything but success, and how long it took |
367
- | `ApplicationRestartOutcomeSchema` / `ApplicationRestartOutcome` | schema / _type_ | `restarted`, `failed` or `refused` — a refusal (unknown id, shutting down, not yet ready) is not a failure and touches nothing |
368
- | `ApplicationResourceFailure` | _type_ | one resource failure with the cause its phase label cannot carry — delivered to `onResourceFailure` |
369
- | `ApplicationResourcePhase` | _type_ | the phase a managed resource failed in — the vocabulary of `ApplicationResourceShutdown.failures` |
370
- | `ApplicationShutdownOptionsSchema` / `ApplicationShutdownOptions` | schema / _type_ | the two shutdown budgets and an abort signal — without the HTTP-only `retryAfterSeconds` |
371
- | `ApplicationShutdownBudgetSchema` / `ApplicationShutdownBudget` | schema / _type_ | the same two budgets without a signal — `ApplicationConfig.shutdown`, the default for `shutdown()` and the only budget a failed startup's rollback can read |
372
- | `ActivityTokenBrand` | const | the brand symbol `ActivityToken` carries, exported so `ActivityProjection` is implementable |
373
- | `defineManagedResource` | function | retain the exact typed resource declaration; every invoked start is rollback-eligible |
374
- | `managedServerResource` | function | own a managed server's lifecycle without copying its HTTP/WebSocket shutdown machine — a thunk is called during `start`, and the handle is published to dependants |
375
- | `managedResourceDependencyId` | function | the id of a dependency declared either as a string or as the resource itself |
376
- | `createApplicationHealthHandler` | function | build a Fetch-clean liveness or readiness response from the sanitized application snapshot |
377
- | `createApplicationOperationalHandlers` | function | compose always-readable status plus the canonical readiness/liveness handlers |
378
- | `ApplicationAdmissionError` | class | stable `APPLICATION_NOT_ACCEPTING` rejection from `admission.run(...)` |
379
- | `ApplicationConfig` / `ApplicationHandle` | _type_ | application declaration and its start/snapshot/subscription/admission/shutdown handle |
380
- | `ApplicationAdmission` / `ApplicationOperationLease` | _type_ | atomic process-local admission and idempotent release primitive |
381
- | `ManagedResource` / `ManagedResourceContext` / `ManagedResourceStartResult` | _type_ | resource lifecycle callbacks, shared deadlines, health reporting, separate readiness/completion promises and the value a resource publishes to its dependants |
382
- | `ManagedResourceAdmission` | _type_ | `context.admission`: `acquire()` now or `null`, and `acquireWhenAccepting(signal)` for a resource that fetches work — a poller waits instead of fetching while the application is starting or degraded; rejects on abort and once the application can no longer admit |
383
- | `ManagedResourceDependency` | _type_ | a dependency named by id or given as the resource itself — the second form is what `context.use(...)` can type |
384
- | `ManagedResourcePublished` | _type_ | the value type `context.use(resource)` returns, recovered from that resource's own `start` |
385
- | `ManagedResourcePublishesNoValue` | _type_ | what `context.use(...)` returns for a resource that publishes nothing — a branded refusal rather than `never`, so reading it does not silently compile |
386
- | `ManagedServerResourceConfig` | _type_ | the server or a sync/async factory receiving `ManagedResourceContext`, plus stable ID, dependencies and server shutdown policy (`retryAfterSeconds`, `realtimeCloseTimeoutMs`) |
387
- | `ManagedServerResource` | _type_ | the resource `managedServerResource` returns, whose `start` publishes the `ManagedServerHandle` |
388
- | `ApplicationHealthHandlerOptions` / `ApplicationHealthHandlerOptionsSchema` | _type_ / schema | liveness/readiness selection and sanitized `Retry-After` policy |
389
- | `ApplicationOperationalHandlers` / `ApplicationOperationalHandlersOptions` / `ApplicationOperationalHandlersOptionsSchema` | _type_ / schema | conventional status/readiness/liveness route surface and shared retry policy |
390
-
391
- ### Bounded admission
392
-
393
- | Export | Kind | Summary |
394
- |--------|------|---------|
395
- | `createBoundedAdmission` | function | process-local no-queue global/per-key concurrency and rate leases, optionally composed with application admission |
396
- | `BoundedAdmissionPolicySchema` / `BoundedAdmissionPolicy` | schema / _type_ | finite global budget and optional finite per-key budget with `maxKeys`, declared either as one flat ceiling or as a resolver — never both |
397
- | `BoundedAdmissionPerKeyLimitsSchema` / `BoundedAdmissionPerKeyLimits` | schema / _type_ | one key's resolved ceiling: `maxConcurrent` and optional `rate` |
398
- | `BoundedAdmissionPerKeyLimitResolver` | _type_ | `(key) => limits`, resolved on a key's first admission and cached until the key is evicted |
399
- | `BoundedRateBudgetSchema` / `BoundedRateBudget` | schema / _type_ | `{ limit, intervalMs }` monotonic sliding-window budget |
400
- | `BoundedAdmissionStateSchema` / `BoundedAdmissionState` | schema / _type_ | `accepting \| draining \| closed` |
401
- | `BoundedAdmissionRefusalReasonSchema` / `BoundedAdmissionRefusalReason` | schema / _type_ | exact local/upstream refusal vocabulary |
402
- | `BoundedAdmissionSnapshotSchema` / `BoundedAdmissionSnapshot` | schema / _type_ | absolute active/lifetime/refusal/rate-accounting counters |
403
- | `BoundedAdmission` / `BoundedAdmissionConfig` / `BoundedAdmissionClock` | _type_ | handle, policy/upstream/clock configuration and monotonic clock seam |
404
- | `BoundedAdmissionResult` / `BoundedAdmissionLeaseResult` / `BoundedAdmissionRefusedResult` | _type_ | explicit leased-or-refused acquisition result; retry time exists only for rate bounds |
405
- | `BoundedOperationLease` | _type_ | idempotent release lease, optionally carrying its key |
406
- | `BoundedOperationRunContext` / `BoundedOperationRunOptions` | _type_ | underlying work signal and caller abort/timeout wait budget |
407
- | `BoundedAdmissionDrainOptions` / `BoundedAdmissionDrainResult` / `BoundedAdmissionForceResult` | _type_ | bounded drain inputs and honest remaining-work results |
408
- | `BoundedAdmissionRefusalError` | class | `run()` refusal with reason and optional `retryAfterMs` |
409
- | `BoundedOperationWaitError` | class | caller wait ended as `cancelled` or `timed-out`; underlying capacity remains leased until work settles |
410
-
411
- ### Revision wake-up
412
-
413
- | Export | Kind | Summary |
414
- |--------|------|---------|
415
- | `createRevisionSignal` | function | finite broadcast wait for the next monotonic process-local revision |
416
- | `RevisionSignal` / `RevisionSignalConfig` / `RevisionSignalClock` / `RevisionSignalTimer` | _type_ | signal handle, waiter bound and replaceable timer boundary |
417
- | `RevisionSignalWaitOptions` | _type_ | optional caller `signal` and positive `timeoutMs`; absent options wait until advance or close |
418
- | `RevisionSignalWaitOutcomeSchema` / `RevisionSignalWaitOutcome` | schema / _type_ | `changed \| timed-out \| aborted \| closed \| capacity` |
419
- | `RevisionSignalWaitResultSchema` / `RevisionSignalWaitResult` | schema / _type_ | terminal wait outcome with the revision observed at settlement |
420
- | `RevisionSignalAdvanceResultSchema` / `RevisionSignalAdvanceResult` | schema / _type_ | successful `advanced` or post-close `closed`, with the resulting revision |
421
- | `RevisionSignalStateSchema` / `RevisionSignalState` | schema / _type_ | `open \| closed` lifecycle |
422
- | `RevisionSignalSnapshotSchema` / `RevisionSignalSnapshot` | schema / _type_ | current revision, finite pending count and lifetime outcome counters |
423
-
424
- ### Bounded delivery
425
-
426
- | Export | Kind | Summary |
427
- |--------|------|---------|
428
- | `createBoundedChannel` | function | finite single-reader async channel with explicit ordered or latest-value policy |
429
- | `BoundedChannelPolicySchema` / `BoundedChannelPolicy` | schema / _type_ | `ordered \| latest` retention policy |
430
- | `BoundedChannelStateSchema` / `BoundedChannelState` | schema / _type_ | `open \| draining \| closed \| failed` |
431
- | `BoundedChannelSnapshotSchema` / `BoundedChannelSnapshot` | schema / _type_ | exact retained count/bytes, waiter and outcome counters |
432
- | `BoundedChannel` / `BoundedChannelConfig` / `BoundedChannelCloseOptions` | _type_ | iterator/offer handle, explicit count/byte/size policy and drain/discard close mode |
433
- | `BoundedChannelOfferResult` | _type_ | `delivered`, `queued`, `coalesced`, or reasoned `refused` outcome |
434
- | `BoundedChannelReaderError` | class | refusal of a second concurrent pending `next()` |
435
- | `createCreditWindow` | function | finite byte-credit lease window with exact once-only replenishment |
436
- | `CreditWindow` / `CreditWindowSnapshot` / `CreditWindowSnapshotSchema` | _type_ / schema | byte-credit handle and absolute accounting record |
437
- | `CreditAcquireResult` / `CreditLease` | _type_ | reasoned refusal or idempotently releasable byte-credit lease |
438
- | `CreditAcquireWaitOptions` | _type_ | `signal` / `timeoutMs` for the waiting `acquire` overload; absent budget waits until credit, close or abort |
439
- | `CreditWaitResult` / `CreditWaitRefusalReason` | _type_ | waiting-acquire outcome; `insufficient-credit` is absent by construction, `timed-out` and `aborted` replace it |
440
-
441
- ### Bounded diagnostic journal
442
-
443
- | Export | Kind | Summary |
444
- |--------|------|---------|
445
- | `createDiagnosticJournal` | function | create one schema-owned FIFO JSONL writer with bounded retained memory, exclusive local path ownership and finite rotation |
446
- | `DiagnosticJournalConfig` / `DiagnosticJournal` | _type_ | owner schema/path/limits/failure observer and the synchronous `submit`, bounded-wait `flush`/`close`, status handle |
447
- | `DiagnosticJournalLimitsSchema` / `DiagnosticJournalLimits` | schema / _type_ | positive event, pending-item, pending-byte, file-byte and retained-file limits |
448
- | `DiagnosticJournalLockPolicySchema` / `DiagnosticJournalLockPolicy` | schema / _type_ | `refuse` (default) or `reclaim-stale`, which reclaims only a lock whose recorded owner is provably gone |
449
- | `readDiagnosticJournalLockDiagnosis` / `DiagnosticJournalLockDiagnosis` | function / _type_ | why a `reclaim-stale` acquisition refused — attribution, PID liveness and optional process `identity` evidence (`matched`, `different-boot`, `reused-pid`, `pid-gone`, `legacy`, `unavailable`) — read off the thrown `EEXIST` |
450
- | `DiagnosticJournalSubmitResultSchema` / `DiagnosticJournalSubmitResult` | schema / _type_ | accepted epoch/sequence or explicit invalid, oversized, capacity, closed or failed refusal |
451
- | `DiagnosticJournalStatusSchema` / `DiagnosticJournalStatus` | schema / _type_ | state, limits, exact admission/write/failure counters, pending ownership, rotations, partial tails and last safe sequences |
452
- | `DiagnosticJournalFrameSchema` / `DiagnosticJournalFrame` | schema / _type_ | version-1 JSONL frame carrying process epoch, contiguous accepted sequence and schema-validated JSON event |
453
- | `DiagnosticJournalWaitResultSchema` / `DiagnosticJournalWaitResult` | schema / _type_ | flush settlement boundary with truthful settled, timed-out or cancelled result |
454
- | `DiagnosticJournalCloseResultSchema` / `DiagnosticJournalCloseResult` | schema / _type_ | physical close or caller timeout/cancellation without pretending an active write stopped |
455
- | `DiagnosticJournalStateSchema` / `DiagnosticJournalState` | schema / _type_ | `open \| draining \| closed \| failed` |
456
- | `DiagnosticJournalRefusalReasonSchema` / `DiagnosticJournalRefusalReason` | schema / _type_ | `closed \| failed \| invalid \| oversized \| item-capacity \| byte-capacity` |
457
- | `DiagnosticJournalFailurePhaseSchema` / `DiagnosticJournalFailurePhase` | schema / _type_ | internal `write \| rotation \| close` failure phase exposed only to status and the isolated observer |
458
- | `DiagnosticJournalWaitOptions` / `DiagnosticJournalFailure` | _type_ | caller wait signal/timeout and isolated internal failure callback record |
459
-
460
- `accepted` is bounded in-memory admission and `written` is completed append, not `fsync` or durable
461
- delivery. The journal has no reader/replay/upload API. See the
462
- [guide](../guide/application-kernel.md#bounded-local-diagnostic-journal),
463
- [architecture](../architecture/diagnostic-journal.md) and [ADR
464
- 0134](../decisions/0134-diagnostic-journal-is-bounded-local-evidence.md).
465
-
466
- ### Managed schedules
467
-
468
- | Export | Kind | Summary |
469
- |--------|------|---------|
470
- | `createManagedSchedule` | function | fixed-rate process-local timer that activates after top-level readiness and participates in drain |
471
- | `ManagedScheduleOverlapSchema` / `ManagedScheduleOverlap` | schema / _type_ | `skip`, `queue-one` or bounded `parallel` overlap policy |
472
- | `ManagedScheduleErrorPolicySchema` / `ManagedScheduleErrorPolicy` | schema / _type_ | `continue` or `stop-schedule` after an observed callback failure |
473
- | `ManagedScheduleDescriptorSchema` / `ManagedScheduleDescriptor` | schema / _type_ | immutable public schedule identity and cadence policy |
474
- | `ManagedScheduleStatusSchema` / `ManagedScheduleStatus` | schema / _type_ | absolute schedule state, revision, counters and timestamps |
475
-
476
- Schedule authoring additionally exports `ManagedSchedule`, `ManagedScheduleConfig`,
477
- `ManagedScheduleRunContext`, `ManagedScheduleClock` and `ManagedScheduleTimer`.
478
- The clock exposes monotonic cadence/deadline arithmetic and a wall-clock projection for portable
479
- status timestamps; it is a deterministic test boundary, not a durable scheduler.
480
-
481
- ### Application and activity projection
482
-
483
- | Export | Kind | Summary |
484
- |--------|------|---------|
485
- | `createActivityProjection` | function | aggregate anonymous process-local activity into declared bounded stages and absolute snapshots |
486
- | `createApplicationSnapshotSink` | function | latest-value delivery with one write in flight and one replaceable pending revision |
487
- | `applicationLifecycleEvent` | function | project a sanitized lifecycle fact from a canonical application snapshot |
488
- | `createApplicationEventSink` | function | bounded failure-isolated lifecycle-event delivery; events are not canonical state |
489
- | `ActivitySnapshotSchema` / `ActivitySnapshot` | schema / _type_ | epoch, monotonic revision, timestamps, declared stages and aggregate counts |
490
- | `ApplicationSnapshotSinkStatusSchema` / `ApplicationSnapshotSinkStatus` | schema / _type_ | immutable latest-value sink delivery and coalescing counters |
491
- | `ApplicationLifecycleEventSchema` / `ApplicationLifecycleEvent` | schema / _type_ | sanitized operator-facing application lifecycle fact |
492
-
493
- Activity authoring additionally exports `ActivityId`, `ActivityIdSchema`,
494
- `ActivityStageId`, `ActivityStageIdSchema`, `ActivityStageSnapshot`,
495
- `ActivityStageSnapshotSchema`, `ActivityLiveState`, `ActivityLiveStateSchema`,
496
- `ActivityProjection`, `ActivityProjectionConfig`,
497
- `ActivityProjectionSubscriberError`, `ActivityToken` and `ActivityTransition`.
498
- Latest-value delivery exports `ApplicationSnapshotSink`,
499
- `ApplicationSnapshotSinkConfig`, `ApplicationSnapshotSinkError` and
500
- `RevisionedApplicationSnapshot`; event delivery exports `ApplicationEventSink`
501
- and `ApplicationEventSinkConfig`.
502
-
503
- ### Canonical application records
504
-
505
- These are also published on their own as
506
- [`stitchkit/application/schemas`](#stitchkitapplicationschemas), which carries
507
- no server runtime and can therefore be imported from a browser bundle.
508
-
509
- The entrypoint exports each Zod schema beside its inferred type:
510
- `ApplicationIdSchema` / `ApplicationId`, `ApplicationLifecycleSchema` /
511
- `ApplicationLifecycle`, `ApplicationHealthSchema` / `ApplicationHealth`,
512
- `ManagedResourceStateSchema` / `ManagedResourceState`,
513
- `ApplicationAdmissionSnapshotSchema` / `ApplicationAdmissionSnapshot`,
514
- `ManagedResourceSnapshotSchema` / `ManagedResourceSnapshot`,
515
- `ApplicationSnapshotSchema` / `ApplicationSnapshot`,
516
- `ApplicationStatusProjectionSchema` / `ApplicationStatusProjection` with
517
- `projectApplicationStatus` — what a status or probe endpoint may publish, and
518
- the function that derives it from a snapshot,
519
- `ApplicationResourceShutdownSchema` / `ApplicationResourceShutdown`, and
520
- `ApplicationShutdownResultSchema` / `ApplicationShutdownResult`.
521
-
522
- ### Decisions
523
-
524
- A pipeline of policies that each vote `allow`, `deny` or `defer`, sharing its
525
- vocabulary with an event topic declared `mode: 'decision'`.
526
-
527
- | Export | Kind | Summary |
528
- |--------|------|---------|
529
- | `createDecisionPipeline` | function | run policies in declaration order; the first terminal verdict wins and the rest do not run |
530
- | `DecisionPipelineConfig` | _type_ | `policyTimeoutMs` — how long one policy has to answer. Required, no default: a policy that never settles hangs every caller of the operation it guards |
531
- | `PolicyDecisionSchema` / `PolicyDecision` | schema / _type_ | `allow`, `deny` with a reason, or `defer` — a deny cannot be silent |
532
- | `DecisionPolicy` / `DecisionPipeline` | _types_ | one named voter over the caller's subject, and the pipeline it composes into |
533
- | `DecisionResult` / `DecisionTraceEntry` | _types_ | the verdict, and the trace of what actually ran — not what was declared |
534
- | `DecisionUndecidedError` | class | every policy deferred and no undecided outcome was declared: an unanswered question, raised rather than guessed |
535
- | `DecisionPolicyError` | class | a policy returned a non-decision, threw, or ran past `policyTimeoutMs` — one error for all three, carrying the policy id and the trace so far |
536
-
537
- ### Keyspace and watched reads
538
-
539
- | Export | Kind | Summary |
540
- |--------|------|---------|
541
- | `defineKeyspace` / `keyspaceResource` | function | a named record set read synchronously from memory and written through one serialised chain; backend first, then memory, then the change event |
542
- | `openKeyspace` / `OpenedKeyspace` | function / type | the same keyspace opened directly, for an application that owns its own lifecycle rather than declaring resources to a kernel |
543
- | `KeyspaceBackend` / `KeyspaceDeclaration` / `KeyspaceChange` / `KeyspaceResourceConfig` / `OpenKeyspace` | _types_ | the four-method durability port, the declaration, the announced change, the resource's options and the published handle. `backend` takes a value or a **factory** — a factory is called once per generation, so a restarted keyspace gets a backend that was never closed |
544
- | `memoryKeyspaceBackend` / `sqliteKeyspaceBackend` / `SqliteKeyspaceBackendConfig` | function / type | a disposable in-process backend, and one table with a key and a JSON payload over a caller-owned database |
545
- | `SqliteDatabase` / `SqliteStatement` / `SqliteValue` | _types_ | the minimal synchronous SQLite boundary the framework types against |
546
- | `createWatchHub` / `watchKey` | function | one read per question however many are asking: single-flight per key, re-read on a declared topic, publish only what changed |
547
- | `WatchHub` / `WatchHubConfig` / `WatchOperation` / `WatchSubscriber` / `AttachedWatcher` | _types_ | the hub, its read/watchable/invalidation policy, the operation identity, and one connection's attachment |
548
-
549
- → [ADR 0152](../decisions/0152-a-keyspace-is-memory-that-nothing-reaches-before-it-is-durable.md),
550
- [ADR 0153](../decisions/0153-a-watched-read-is-one-read-per-question.md).
551
-
552
- ### Managed change subscriptions and scoped watches
553
-
554
- `changeSubscriptionResource` (`stitchkit/application`) accepts
555
- `ChangeSubscriptionConfig`: `id`, unique `keys` (1–128, at most 256 characters each),
556
- `connect(signal)`, `reconcile(key, context)`, required `reconcileIntervalMs` (10 ms–1 h),
557
- optional `timeoutMs` (30000; 1 ms–1 h), `backoff`, and `onError`.
558
- `ChangeConnection` supplies `subscribe({ signal, hint, disconnected })`, which completes subscription
559
- before the first read; `close()` releases it. `ChangeReconciliation` exposes `signal`
560
- and a synchronous `commit(callback)` guard against late publication. Drivers must
561
- honor cancellation. Uncooperative work retains its slot until settled instead of
562
- allowing overlapping connections/reads. Errors reach `onError`; no raw payload or
563
- connection string is logged by this resource. Recovery runs after reconnect and
564
- periodically even when no notification arrives.
565
-
566
- `WatchHubConfig` adds `maxSources` (1024), `maxSubscribers` (1024), and optional
567
- `reconcileIntervalMs` (10 ms–1 h). `hub.attach(subscriber, scope?)` accepts a
568
- `WatchAdmissionScope` containing a **server-verified** `key` (1–512 characters) and
569
- revocation `signal`. The key separates sharing and is the optional third argument
570
- of `read(operation, args, scope)`; a key's `instance` routes frames and never
571
- separates sources. Attached over `maxSubscribers`, a subscriber gets a watcher whose
572
- `open` refuses with `Watch subscriber capacity exceeded`; `attach` does not throw
573
- for capacity. It must represent account, session and permissions;
574
- never trust a client-supplied owner ID as that scope.
575
-
576
-
577
-
578
- ### Notification action and projection receipts
579
-
580
- `NotificationOutboxConfig.delivery` accepts `NotificationDeliveryConfig`:
581
- immutable executor `version`, pure `actions(payload)` plan selector (1–128 unique
582
- names, each at most 128 characters), and idempotent `project(notification, receipts)`.
583
- `send` receives optional `action` and stable `idempotencyKey`, and returns a JSON
584
- remote receipt (void becomes null). A result that is not JSON or does not fit the
585
- byte budget is kept as `receipt: null` with `unrecorded: 'not-json' | 'too-large'`;
586
- the action stays confirmed and is not sent again. `retiredVersions` maps earlier
587
- plan versions to their `NotificationProjection` so queued items drain after an
588
- executor upgrade; an unknown version is reported to `onError` as
589
- `NotificationPlanVersionError` and waits, spending no attempt and not consulting
590
- `classify`, for an executor that knows it. `NotificationDeliveryStateSchema` validates the
591
- optional `NotificationDeliveryState` on queued items and completed receipts:
592
- `version`, `actions`, `receipts` and optional `projectedAt`. Checkpoints share the
593
- existing atomic StateStore and lease fence. Receipt retention and serialized byte
594
- budgets still apply. Single-send configuration remains valid without `delivery`.
595
- A remote result lost before its checkpoint may be sent again; provider idempotency
596
- or reconciliation is required. A projection may repeat after a crash before its
597
- checkpoint. Business-transaction enqueue and durable quarantine are not implied.