@nylorun/runtime 0.10.0-beta → 0.11.0-beta

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 (190) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +53 -51
  3. package/dist/adapters/execution/restate.d.ts +130 -0
  4. package/dist/adapters/execution/restate.js +442 -0
  5. package/dist/adapters/sandbox/virtual.js +1 -1
  6. package/dist/adapters/streams/s2.d.ts +18 -0
  7. package/dist/adapters/streams/s2.js +361 -0
  8. package/dist/core/executors.d.ts +4 -1
  9. package/dist/core/executors.js +20 -1
  10. package/dist/core/flow-host.d.ts +93 -132
  11. package/dist/core/flow-host.js +282 -547
  12. package/dist/core/sandbox-routes.d.ts +8 -3
  13. package/dist/core/sandbox-routes.js +10 -10
  14. package/dist/execution/memory.d.ts +54 -0
  15. package/dist/execution/memory.js +217 -0
  16. package/dist/execution/types.d.ts +125 -0
  17. package/dist/execution/types.js +52 -0
  18. package/dist/host/config-for.d.ts +2 -1
  19. package/dist/host/config-for.js +3 -21
  20. package/dist/host/config.d.ts +0 -8
  21. package/dist/host/create-host.d.ts +36 -1
  22. package/dist/host/create-host.js +48 -24
  23. package/dist/host/execution.d.ts +61 -0
  24. package/dist/host/execution.js +69 -0
  25. package/dist/host/http.d.ts +4 -0
  26. package/dist/host/http.js +6 -0
  27. package/dist/host/index.d.ts +3 -1
  28. package/dist/host/index.js +2 -0
  29. package/dist/host/main.js +106 -47
  30. package/dist/host/stack-config.d.ts +72 -0
  31. package/dist/host/stack-config.js +193 -0
  32. package/dist/index.d.ts +1 -1
  33. package/dist/infra/database.d.ts +16 -0
  34. package/dist/infra/database.js +31 -0
  35. package/dist/infra/execution.d.ts +39 -0
  36. package/dist/infra/execution.js +68 -0
  37. package/dist/infra/index.d.ts +42 -0
  38. package/dist/infra/index.js +56 -0
  39. package/dist/infra/readiness.d.ts +33 -0
  40. package/dist/infra/readiness.js +53 -0
  41. package/dist/infra/streams.d.ts +17 -0
  42. package/dist/infra/streams.js +33 -0
  43. package/dist/infra/workers.d.ts +39 -0
  44. package/dist/infra/workers.js +31 -0
  45. package/dist/model/pi-model.d.ts +2 -2
  46. package/dist/model/pi-model.js +5 -5
  47. package/dist/sandbox/manager.d.ts +26 -8
  48. package/dist/sandbox/manager.js +95 -47
  49. package/dist/sandbox/select.d.ts +1 -1
  50. package/dist/sandbox/select.js +4 -5
  51. package/dist/sandbox/types.d.ts +2 -2
  52. package/dist/store/canonical.d.ts +5 -0
  53. package/dist/store/canonical.js +17 -0
  54. package/dist/store/cursor.d.ts +9 -0
  55. package/dist/store/cursor.js +18 -0
  56. package/dist/store/memory.d.ts +57 -0
  57. package/dist/store/memory.js +643 -0
  58. package/dist/store/ownership.d.ts +19 -0
  59. package/dist/store/ownership.js +33 -0
  60. package/dist/store/postgres/connect.d.ts +25 -0
  61. package/dist/store/postgres/connect.js +25 -0
  62. package/dist/store/postgres/locking.d.ts +16 -0
  63. package/dist/store/postgres/locking.js +22 -0
  64. package/dist/store/postgres/migrations/001_initial.d.ts +27 -0
  65. package/dist/store/postgres/migrations/001_initial.js +196 -0
  66. package/dist/store/postgres/migrations/index.d.ts +49 -0
  67. package/dist/store/postgres/migrations/index.js +75 -0
  68. package/dist/store/postgres/names.d.ts +11 -0
  69. package/dist/store/postgres/names.js +42 -0
  70. package/dist/store/postgres/store.d.ts +11 -0
  71. package/dist/store/postgres/store.js +790 -0
  72. package/dist/store/postgres/tenants.d.ts +90 -0
  73. package/dist/store/postgres/tenants.js +207 -0
  74. package/dist/store/types.d.ts +440 -0
  75. package/dist/store/types.js +10 -0
  76. package/dist/streams/basin.d.ts +23 -0
  77. package/dist/streams/basin.js +51 -0
  78. package/dist/streams/memory.d.ts +25 -0
  79. package/dist/streams/memory.js +124 -0
  80. package/dist/streams/relay.d.ts +37 -0
  81. package/dist/streams/relay.js +254 -0
  82. package/dist/streams/types.d.ts +115 -0
  83. package/dist/streams/types.js +86 -0
  84. package/dist/tenant/actions.d.ts +153 -0
  85. package/dist/tenant/actions.js +191 -0
  86. package/dist/tenant/advance.d.ts +15 -0
  87. package/dist/tenant/advance.js +439 -0
  88. package/dist/tenant/auth.d.ts +11 -0
  89. package/dist/tenant/auth.js +34 -0
  90. package/dist/tenant/commands.d.ts +3 -0
  91. package/dist/tenant/commands.js +384 -0
  92. package/dist/tenant/context.d.ts +140 -0
  93. package/dist/tenant/context.js +40 -0
  94. package/dist/tenant/effects.d.ts +20 -0
  95. package/dist/tenant/effects.js +539 -0
  96. package/dist/tenant/envelope.d.ts +0 -3
  97. package/dist/tenant/envelope.js +2 -34
  98. package/dist/tenant/ephemeral.d.ts +15 -4
  99. package/dist/tenant/ephemeral.js +106 -38
  100. package/dist/tenant/http.d.ts +32 -0
  101. package/dist/tenant/http.js +51 -0
  102. package/dist/tenant/live.d.ts +92 -0
  103. package/dist/tenant/live.js +290 -0
  104. package/dist/tenant/model-setting.d.ts +16 -0
  105. package/dist/tenant/model-setting.js +12 -0
  106. package/dist/tenant/module.d.ts +29 -8
  107. package/dist/tenant/module.js +162 -158
  108. package/dist/tenant/paths.d.ts +12 -8
  109. package/dist/tenant/paths.js +38 -12
  110. package/dist/tenant/principals.d.ts +9 -13
  111. package/dist/tenant/principals.js +31 -38
  112. package/dist/tenant/quarantine-error.d.ts +1 -1
  113. package/dist/tenant/quarantine-error.js +2 -8
  114. package/dist/tenant/quarantine.d.ts +0 -8
  115. package/dist/tenant/quarantine.js +8 -30
  116. package/dist/tenant/reset.d.ts +7 -6
  117. package/dist/tenant/reset.js +7 -51
  118. package/dist/tenant/routes-tenant.d.ts +12 -0
  119. package/dist/tenant/routes-tenant.js +126 -0
  120. package/dist/tenant/routes.d.ts +9 -0
  121. package/dist/tenant/routes.js +142 -0
  122. package/dist/tenant/runtime.d.ts +59 -58
  123. package/dist/tenant/runtime.js +243 -2201
  124. package/dist/tenant/scheduler.d.ts +25 -0
  125. package/dist/tenant/scheduler.js +84 -0
  126. package/dist/tenant/session.d.ts +60 -0
  127. package/dist/tenant/session.js +94 -0
  128. package/dist/tenant/sessions.d.ts +34 -0
  129. package/dist/tenant/sessions.js +149 -0
  130. package/dist/tenant/status.d.ts +13 -4
  131. package/dist/tenant/status.js +76 -41
  132. package/dist/tenant/store-memory.d.ts +9 -12
  133. package/dist/tenant/store-memory.js +79 -68
  134. package/dist/tenant/store-pg.d.ts +15 -0
  135. package/dist/tenant/store-pg.js +140 -0
  136. package/dist/tenant/streams.d.ts +126 -0
  137. package/dist/tenant/streams.js +419 -0
  138. package/dist/tenant/sweep.d.ts +15 -0
  139. package/dist/tenant/sweep.js +122 -0
  140. package/dist/tenant/types.d.ts +95 -15
  141. package/dist/tenant/types.js +23 -1
  142. package/dist/tenant/worker.d.ts +84 -0
  143. package/dist/tenant/worker.js +189 -0
  144. package/dist/vault/service.d.ts +37 -31
  145. package/dist/vault/service.js +439 -359
  146. package/dist/version.d.ts +1 -1
  147. package/dist/version.js +1 -1
  148. package/package.json +13 -13
  149. package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
  150. package/dist/adapters/sandbox/microsandbox.js +0 -238
  151. package/dist/core/store.d.ts +0 -32
  152. package/dist/core/store.js +0 -143
  153. package/dist/launcher/commands.d.ts +0 -24
  154. package/dist/launcher/commands.js +0 -260
  155. package/dist/launcher/ensure-config.d.ts +0 -16
  156. package/dist/launcher/ensure-config.js +0 -79
  157. package/dist/launcher/errors.d.ts +0 -12
  158. package/dist/launcher/errors.js +0 -22
  159. package/dist/launcher/host-config.d.ts +0 -17
  160. package/dist/launcher/host-config.js +0 -64
  161. package/dist/launcher/host-state.d.ts +0 -17
  162. package/dist/launcher/host-state.js +0 -61
  163. package/dist/launcher/lifecycle.d.ts +0 -66
  164. package/dist/launcher/lifecycle.js +0 -407
  165. package/dist/launcher/locks.d.ts +0 -26
  166. package/dist/launcher/locks.js +0 -117
  167. package/dist/launcher/logs.d.ts +0 -18
  168. package/dist/launcher/logs.js +0 -117
  169. package/dist/launcher/main.d.ts +0 -2
  170. package/dist/launcher/main.js +0 -66
  171. package/dist/launcher/output.d.ts +0 -11
  172. package/dist/launcher/output.js +0 -68
  173. package/dist/launcher/paths.d.ts +0 -20
  174. package/dist/launcher/paths.js +0 -36
  175. package/dist/launcher/probe.d.ts +0 -23
  176. package/dist/launcher/probe.js +0 -72
  177. package/dist/launcher/protocol.d.ts +0 -62
  178. package/dist/launcher/protocol.js +0 -1
  179. package/dist/launcher/schema-guard.d.ts +0 -6
  180. package/dist/launcher/schema-guard.js +0 -36
  181. package/dist/launcher/spawn.d.ts +0 -22
  182. package/dist/launcher/spawn.js +0 -85
  183. package/dist/launcher/status.d.ts +0 -6
  184. package/dist/launcher/status.js +0 -65
  185. package/dist/tenant/migration.d.ts +0 -16
  186. package/dist/tenant/migration.js +0 -116
  187. package/dist/tenant/schema.d.ts +0 -23
  188. package/dist/tenant/schema.js +0 -151
  189. package/dist/tenant/store-fs.d.ts +0 -16
  190. package/dist/tenant/store-fs.js +0 -205
@@ -0,0 +1,440 @@
1
+ /**
2
+ * The Session Store seam (architecture §12.2).
3
+ *
4
+ * The Session Store records what happened. It is async, transactional and
5
+ * tenant-scoped: one `SessionStore` per Tenant. Postgres is the supported
6
+ * implementation; `store/memory.ts` is the in-memory fake for unit tests.
7
+ *
8
+ * ## Invariants every implementation keeps
9
+ *
10
+ * 1. **One transaction per `tx` call, READ COMMITTED or stronger.** Nothing a
11
+ * transaction wrote is visible to others before it commits, and nothing is
12
+ * kept when `fn` throws: document writes, event sequences, outbox rows,
13
+ * `afterCommit` callbacks and `signalWork` are all discarded, and `tx`
14
+ * rejects with the error `fn` threw.
15
+ * 2. **Session-scoped writes lock the session row first.** `lockSession` takes
16
+ * a row lock (`SELECT … FOR UPDATE`) held until the transaction ends. Effect
17
+ * intent and outcome, Action transitions, checkpoint settlement and event
18
+ * writes for one session are serialized through it. Idempotency comparisons
19
+ * (`canonical(...)`) run inside the same locked transaction. `event` and the
20
+ * ownership methods take the lock themselves.
21
+ * 3. **Per-session event sequence.** `event` allocates the session's next
22
+ * sequence under the session lock, starting at 0, without gaps across
23
+ * committed transactions. The sequence is also the S2 sequence number of the
24
+ * event in the session's stream (`sessions/<id>/<incarnation>`,
25
+ * `streams/types.ts`), and the cursor is `base64url("<sessionId>:<seq>")`
26
+ * (see `store/cursor.ts`).
27
+ * 4. **No external I/O inside `fn`.** No model, tool, MCP, sandbox, Restate or
28
+ * S2 call, and no `fetch`, runs inside a transaction. Wakes go through
29
+ * `afterCommit`, executor notifications through `signalWork`, and events are
30
+ * delivered to commit listeners after commit (seam rule 1 and 2).
31
+ * 5. **No nested transactions.** Calling `store.tx` from inside `fn` rejects.
32
+ * A `Tx` must not be used after its `tx` call settles.
33
+ * 6. **Post-commit order.** After a commit, the store first calls every commit
34
+ * listener once with the transaction's events (in allocation order) and its
35
+ * `signalWork` flag, then runs the `afterCommit` callbacks in registration
36
+ * order and awaits them. Listener and callback failures are reported to the
37
+ * store's error hook; they never reject `tx`, because the commit stands.
38
+ * Recovery from a lost post-commit step is the Tenant sweep's job.
39
+ * 7. **Documents are values.** `get` and queries return fresh copies; mutating
40
+ * them changes nothing until `put`.
41
+ * 8. **Ownership columns are store-managed.** `owner`, `epoch` and
42
+ * `ownerExpiresAt` on a session are read with the session but written only
43
+ * through `takeOwnership`, `renewOwnership` and `releaseOwnership`; `put`
44
+ * ignores them. A session starts with no owner and epoch 0.
45
+ *
46
+ * ## Typed queries, no scans
47
+ *
48
+ * There is no generic table scan. Every read the Runtime needs is a typed
49
+ * method on `Tx` (`sessionsWithStatus`, `expiredClaims`, `pendingActions`,
50
+ * `effectsForTurn`, `linkedSessions`, `counts`, …) that an implementation can
51
+ * back with an index, and principals, executors, vaults and Tenant settings
52
+ * have their own methods rather than raw SQL outside the store. Session history
53
+ * is not read from the store: the relay moves events from the outbox to
54
+ * Durable Streams, and history and SSE read them there (`tenant/streams.ts`).
55
+ */
56
+ import type { Action, LiveEvent } from "@nylorun/core/contracts";
57
+ import type { HostEffect } from "@nylorun/harness/run";
58
+ /** Tables holding JSON documents keyed by `id`. */
59
+ export type DocTable = "definitions" | "sessions" | "commands" | "checkpoints" | "effects" | "actions" | "sandboxes" | "links";
60
+ export declare const DOC_TABLES: readonly DocTable[];
61
+ /** Session lifecycle (architecture §10.2). */
62
+ export type SessionStatus = "idle" | "runnable" | "running" | "waiting" | "paused" | "uncertain" | "completed" | "failed" | "cancelled";
63
+ /** Effect states (§10.3), plus the flow host's `queued` and `cancelled`. */
64
+ export type EffectStatus = "pending" | "invoking" | "completed" | "uncertain" | "queued" | "cancelled";
65
+ export type ActionStatus = Action["status"];
66
+ export type ActionKind = Action["kind"];
67
+ export type EffectKind = HostEffect["kind"];
68
+ /**
69
+ * Ownership of a session by a Worker (§10.6). Read with the session; written
70
+ * only through the ownership methods on `Tx`.
71
+ */
72
+ export interface SessionOwnership {
73
+ /** Worker id holding the lease, or null. */
74
+ owner: string | null;
75
+ /** Incremented by every successful `takeOwnership`. Starts at 0. */
76
+ epoch: number;
77
+ /** ISO time the owner's lease ends, or null without an owner. */
78
+ ownerExpiresAt: string | null;
79
+ }
80
+ /**
81
+ * The fields of a session document that the store indexes. The
82
+ * Runtime's own session type extends this; everything else in the body is
83
+ * opaque to the store.
84
+ */
85
+ export interface SessionDoc {
86
+ id: string;
87
+ agentId: string;
88
+ status: SessionStatus | (string & {});
89
+ activeTurnId: string | null;
90
+ /**
91
+ * Names the session's event stream (`sessions/<id>/<incarnation>`, `streams/types.ts`).
92
+ * Set when the session is created; `event` reports it with each event (`Commit`).
93
+ */
94
+ streamIncarnation?: string;
95
+ }
96
+ /** The fields of an effect document the store indexes (`request.sessionId`, `request.turnId`, `request.kind`, `status`). */
97
+ export interface EffectDoc {
98
+ request: HostEffect;
99
+ status: EffectStatus | (string & {});
100
+ }
101
+ /** Actions are stored as the wire `Action`. Indexed: `sessionId`, `turnId`, `agentId`, `status`, `kind`, `leaseExpiresAt`. */
102
+ export type ActionDoc = Action;
103
+ /** A workflow → agent session link, keyed by the linked agent session id. Indexed: `workflowSessionId`. */
104
+ export interface LinkDoc {
105
+ workflowSessionId: string;
106
+ path: string;
107
+ effectId: string;
108
+ turnId: string;
109
+ }
110
+ /** A definition document. Indexed: `manifest.id`. */
111
+ export interface DefinitionDoc {
112
+ manifest: {
113
+ id: string;
114
+ };
115
+ }
116
+ /** A sandbox record, keyed by sandbox key. */
117
+ export interface SandboxDoc {
118
+ key: string;
119
+ }
120
+ /** A session as read from the store: its document plus the store-managed ownership fields. */
121
+ export type StoredSession<T extends SessionDoc = SessionDoc> = T & SessionOwnership;
122
+ /** A link row together with the agent session it names. */
123
+ export interface LinkedSession<S extends SessionDoc = SessionDoc> {
124
+ agentSessionId: string;
125
+ link: LinkDoc;
126
+ session: StoredSession<S>;
127
+ }
128
+ /** One committed-but-not-yet-relayed event (§12.4). Deleted once S2 has it. */
129
+ export interface OutboxRow {
130
+ sessionId: string;
131
+ seq: number;
132
+ event: LiveEvent;
133
+ }
134
+ export interface OutboxStats {
135
+ /** Unrelayed events. */
136
+ depth: number;
137
+ /** ISO `createdAt` of the oldest unrelayed event, or null when the outbox is empty. */
138
+ oldestCreatedAt: string | null;
139
+ }
140
+ /** What one commit produced, delivered to commit listeners after commit. */
141
+ export interface Commit {
142
+ /** Events written by the transaction, in allocation order. */
143
+ readonly events: readonly LiveEvent[];
144
+ /**
145
+ * The `streamIncarnation` of each event's session when the event was allocated (under the
146
+ * session lock), aligned with `events`; null for a session without one. The relay appends
147
+ * each event to that incarnation's stream.
148
+ */
149
+ readonly incarnations: readonly (string | null)[];
150
+ /** True when the transaction called `signalWork()`. */
151
+ readonly workAvailable: boolean;
152
+ }
153
+ export type CommitListener = (commit: Commit) => void;
154
+ /** Executors carry a bearer secret hash, so they use typed columns with a unique token hash. */
155
+ export interface ExecutorRow {
156
+ agentId: string;
157
+ tokenHash: string;
158
+ implementationVersion: string;
159
+ manifestHash?: string;
160
+ principalId?: string;
161
+ createdAt: string;
162
+ updatedAt: string;
163
+ }
164
+ export interface PrincipalRow {
165
+ id: string;
166
+ role: "application" | (string & {});
167
+ tokenHash: string;
168
+ idempotencyKey: string | null;
169
+ createdAt: string;
170
+ }
171
+ export interface VaultRow {
172
+ id: string;
173
+ name: string;
174
+ ownerUserId: string;
175
+ /** JSON text, or null. */
176
+ metadataJson: string | null;
177
+ createdAt: string;
178
+ scope: "user" | "host";
179
+ }
180
+ /** Envelope-encrypted secret material. Always in columns, never inside a JSON body. */
181
+ export interface SealedSecret {
182
+ kekId: string;
183
+ nonce: Uint8Array;
184
+ ciphertext: Uint8Array;
185
+ wrappedDek: Uint8Array;
186
+ }
187
+ export interface VaultCredentialRow extends SealedSecret {
188
+ id: string;
189
+ vaultId: string;
190
+ name: string;
191
+ type: "bearer" | "oauth" | "model";
192
+ /** JSON text of the non-secret binding (url, or provider and model). */
193
+ bindingJson: string;
194
+ expiresAt: string | null;
195
+ createdAt: string;
196
+ rotatedAt: string | null;
197
+ }
198
+ /** Fields `updateCredential` may change. */
199
+ export type VaultCredentialPatch = Partial<Omit<VaultCredentialRow, "id" | "vaultId" | "createdAt">>;
200
+ export interface VaultAuditRow {
201
+ id: string;
202
+ at: string;
203
+ actor: string;
204
+ action: string;
205
+ vaultId: string | null;
206
+ credentialId: string | null;
207
+ sessionId: string | null;
208
+ target: string | null;
209
+ outcome: string;
210
+ }
211
+ export interface VaultIdempotencyRow {
212
+ id: string;
213
+ bodyHash: string;
214
+ /** JSON text of the original response. */
215
+ response: string;
216
+ }
217
+ export interface SessionEffectFilter {
218
+ /** Only this turn. Absent means every turn. */
219
+ turnId?: string;
220
+ statuses?: readonly (EffectStatus | (string & {}))[];
221
+ }
222
+ export interface SessionActionFilter {
223
+ /** Only this turn. Absent means every turn. */
224
+ turnId?: string;
225
+ statuses?: readonly ActionStatus[];
226
+ }
227
+ /** Counts used by Tenant status, `summary` and `drain`. */
228
+ export interface StoreCounts {
229
+ sessions: number;
230
+ /** Sessions `running` or `runnable`. */
231
+ runningSessions: number;
232
+ /** Actions `pending` or `claimed`. */
233
+ pendingActions: number;
234
+ /** Effects `uncertain`. */
235
+ uncertainEffects: number;
236
+ sandboxes: number;
237
+ definitions: number;
238
+ }
239
+ export type ResetScope = "sessions" | "sandboxes" | "all";
240
+ /** Result of `takeOwnership`. */
241
+ export type TakeOwnership = {
242
+ status: "owned";
243
+ /** The new epoch every later transaction of this advance must present. */
244
+ epoch: number;
245
+ /** True when a previous owner's lease had expired without release: run takeover (§10.5 step 2). */
246
+ takeover: boolean;
247
+ previous: SessionOwnership;
248
+ } | {
249
+ status: "busy";
250
+ owner: string;
251
+ ownerExpiresAt: string;
252
+ } | {
253
+ status: "missing";
254
+ };
255
+ /** One tenant's Session Store. */
256
+ export interface SessionStore {
257
+ /** The Tenant this store serves; stamped on every event. */
258
+ readonly tenantId: string;
259
+ /**
260
+ * Runs `fn` in one READ COMMITTED transaction and commits when it resolves.
261
+ * Session-scoped writes call `lockSession` first. See the module invariants.
262
+ */
263
+ tx<T>(fn: (t: Tx) => Promise<T>): Promise<T>;
264
+ /**
265
+ * Registers a listener called once per committed transaction that wrote
266
+ * events or signalled work. Returns an unsubscribe function. The outbox
267
+ * relay (`streams/relay.ts`) subscribes here.
268
+ */
269
+ onCommit(listener: CommitListener): () => void;
270
+ /** Reachability and schema check for Tenant status and `/ready`. Never throws. */
271
+ health(): Promise<StoreHealth>;
272
+ close(): Promise<void>;
273
+ }
274
+ export interface StoreHealth {
275
+ ok: boolean;
276
+ schemaVersion: number;
277
+ expectedSchemaVersion: number;
278
+ }
279
+ /** Options every implementation accepts. */
280
+ export interface SessionStoreOptions {
281
+ tenantId: string;
282
+ /** Clock for event `createdAt`. Defaults to `() => new Date()`. */
283
+ now?: () => Date;
284
+ /** Receives listener and `afterCommit` failures, which never reject `tx`. */
285
+ onError?: (error: unknown) => void;
286
+ }
287
+ /** The transaction handle. Valid only inside its `tx` callback. */
288
+ export interface Tx {
289
+ get<T = any>(table: DocTable, id: string): Promise<T | undefined>;
290
+ /** Insert or replace. For `sessions`, ownership fields in `body` are ignored. */
291
+ put(table: DocTable, id: string, body: unknown): Promise<void>;
292
+ /**
293
+ * Deletes a document. Deleting a session also deletes its outbox rows, so a
294
+ * session created again with the same id starts at sequence 0 without
295
+ * colliding with rows the relay never took (its stream is a new incarnation).
296
+ */
297
+ delete(table: DocTable, id: string): Promise<void>;
298
+ /**
299
+ * Locks the session row for the rest of the transaction and returns the
300
+ * session (with ownership fields), or undefined when it does not exist.
301
+ * Locking twice in one transaction is a no-op.
302
+ *
303
+ * Lock order, so concurrent transactions cannot deadlock (Postgres takes row
304
+ * locks in statement order; the in-memory fake serializes whole transactions):
305
+ * - a linked agent (child) session is locked before its workflow (parent)
306
+ * session, never after it. `t.event` on a session takes its lock, so an
307
+ * event on a child after the parent is locked breaks the rule too. Work
308
+ * that starts from the parent and must touch children locks the children
309
+ * first, or splits into one transaction per session;
310
+ * - unrelated sessions touched by one transaction are locked in ascending
311
+ * id order (`lockSessions` in `store/postgres/locking.ts`), or split.
312
+ */
313
+ lockSession<T extends SessionDoc = SessionDoc>(id: string): Promise<StoredSession<T> | undefined>;
314
+ /**
315
+ * Allocates the session's next sequence (from 0) under the session lock,
316
+ * writes the event to the outbox and buffers it for commit listeners.
317
+ * Rejects when the session does not exist. The returned cursor is final.
318
+ */
319
+ event(sessionId: string, turnId: string | null, type: string, payload: unknown): Promise<LiveEvent>;
320
+ /** Runs `fn` after a successful commit (never on rollback). Used for wakes. */
321
+ afterCommit(fn: () => void | Promise<void>): void;
322
+ /** Signals `work_available` to executors after a successful commit. */
323
+ signalWork(): void;
324
+ /**
325
+ * Locks the session and takes ownership when it has no owner or the owner's
326
+ * lease ended at or before `now`; then increments `epoch` and sets the lease
327
+ * to `now + leaseMs`. A live lease (including this owner's own) is `busy`.
328
+ */
329
+ takeOwnership(sessionId: string, claim: {
330
+ owner: string;
331
+ now: Date;
332
+ leaseMs: number;
333
+ }): Promise<TakeOwnership>;
334
+ /** Extends the lease when `owner` and `epoch` still match. Returns false when ownership was lost. */
335
+ renewOwnership(sessionId: string, owner: string, epoch: number, until: Date): Promise<boolean>;
336
+ /** Clears the owner (keeping the epoch) when `owner` and `epoch` still match. */
337
+ releaseOwnership(sessionId: string, owner: string, epoch: number): Promise<boolean>;
338
+ /**
339
+ * Locks the session and throws `OwnershipLostError` (`ownership.lost`) when
340
+ * its epoch is not `epoch` or the session is gone. Every transaction of an
341
+ * advance calls this first; see `ownedTx` in `store/ownership.ts`.
342
+ */
343
+ assertEpoch<T extends SessionDoc = SessionDoc>(sessionId: string, epoch: number): Promise<StoredSession<T>>;
344
+ sessionsWithStatus<T extends SessionDoc = SessionDoc>(statuses: readonly (SessionStatus | (string & {}))[]): Promise<StoredSession<T>[]>;
345
+ /**
346
+ * Sessions `running` or `runnable` with no owner or an owner whose lease
347
+ * ended at or before `now`, oldest lease first. The sweep re-wakes these.
348
+ */
349
+ orphanedSessions<T extends SessionDoc = SessionDoc>(now: Date, limit: number): Promise<StoredSession<T>[]>;
350
+ listSessions<T extends SessionDoc = SessionDoc>(filter?: {
351
+ agentId?: string;
352
+ }): Promise<StoredSession<T>[]>;
353
+ listDefinitions<T extends DefinitionDoc = DefinitionDoc>(): Promise<T[]>;
354
+ listSandboxes<T extends SandboxDoc = SandboxDoc>(): Promise<T[]>;
355
+ /** Claimed actions whose lease ended at or before `now`, earliest lease first. */
356
+ expiredClaims(now: Date, limit: number): Promise<ActionDoc[]>;
357
+ /** `pending` actions offered to one agent's executor. */
358
+ pendingActions(agentId: string): Promise<ActionDoc[]>;
359
+ actionsForSession(sessionId: string, filter?: SessionActionFilter): Promise<ActionDoc[]>;
360
+ actionsWithStatus(statuses: readonly ActionStatus[], filter?: {
361
+ kinds?: readonly ActionKind[];
362
+ }): Promise<ActionDoc[]>;
363
+ /** `invoking` effects of one session; takeover turns them `uncertain`. */
364
+ invokingEffects<T extends EffectDoc = EffectDoc>(sessionId: string): Promise<T[]>;
365
+ effectsForSession<T extends EffectDoc = EffectDoc>(sessionId: string, filter?: SessionEffectFilter): Promise<T[]>;
366
+ effectsForTurn<T extends EffectDoc = EffectDoc>(sessionId: string, turnId: string, statuses?: readonly (EffectStatus | (string & {}))[]): Promise<T[]>;
367
+ effectsWithStatus<T extends EffectDoc = EffectDoc>(statuses: readonly (EffectStatus | (string & {}))[], filter?: {
368
+ kinds?: readonly EffectKind[];
369
+ }): Promise<T[]>;
370
+ /** Links of one workflow session joined to their existing agent sessions, ordered by agent session id. */
371
+ linkedSessions<S extends SessionDoc = SessionDoc>(workflowSessionId: string): Promise<LinkedSession<S>[]>;
372
+ counts(): Promise<StoreCounts>;
373
+ /** Unrelayed events, ordered by session id then sequence. */
374
+ outbox(limit: number, filter?: {
375
+ sessionId?: string;
376
+ }): Promise<OutboxRow[]>;
377
+ /**
378
+ * Deletes a session's outbox rows with `seq <= throughSeq`. Returns the number deleted.
379
+ * With `incarnation`, deletes only while the session exists with that `streamIncarnation`
380
+ * (null: none), in one statement: a relay that appended to an abandoned incarnation's
381
+ * stream never deletes the rows of a session created again with the same id.
382
+ */
383
+ deleteOutbox(sessionId: string, throughSeq: number, incarnation?: string | null): Promise<number>;
384
+ /** How many events are unrelayed, and the `createdAt` of the oldest (Tenant status). */
385
+ outboxStats(): Promise<OutboxStats>;
386
+ listExecutors(): Promise<ExecutorRow[]>;
387
+ getExecutor(agentId: string): Promise<ExecutorRow | undefined>;
388
+ /**
389
+ * Inserts or updates by `agentId`, keeping `createdAt` on update. Rejects
390
+ * when another agent already uses `tokenHash`.
391
+ */
392
+ putExecutor(row: Omit<ExecutorRow, "createdAt">): Promise<void>;
393
+ deleteExecutor(agentId: string): Promise<void>;
394
+ /** Rejects when the id or token hash already exists. */
395
+ insertPrincipal(row: PrincipalRow): Promise<void>;
396
+ principalByTokenHash(tokenHash: string): Promise<PrincipalRow | undefined>;
397
+ principalById(id: string): Promise<PrincipalRow | undefined>;
398
+ applicationTokenHashes(): Promise<string[]>;
399
+ /** Rejects on a duplicate id, or a second `host` vault. */
400
+ insertVault(row: VaultRow): Promise<void>;
401
+ getVault(id: string): Promise<VaultRow | undefined>;
402
+ /** User vaults of one owner, ordered by `createdAt`, then id. */
403
+ vaultsByOwner(ownerUserId: string): Promise<VaultRow[]>;
404
+ updateVaultMetadata(id: string, metadataJson: string | null): Promise<void>;
405
+ /** Deletes the vault and its credentials. */
406
+ deleteVault(id: string): Promise<void>;
407
+ /** Rejects on a duplicate id or an unknown vault. */
408
+ insertCredential(row: VaultCredentialRow): Promise<void>;
409
+ /** Looks a credential up by id alone (attachment checks); callers compare `vaultId`. */
410
+ getCredential(id: string): Promise<VaultCredentialRow | undefined>;
411
+ /** Credentials of one vault, ordered by `createdAt`, then id. */
412
+ credentialsForVault(vaultId: string, filter?: {
413
+ type?: VaultCredentialRow["type"];
414
+ }): Promise<VaultCredentialRow[]>;
415
+ /** Updates a credential of `vaultId`. Returns false when it does not exist. */
416
+ updateCredential(vaultId: string, id: string, patch: VaultCredentialPatch): Promise<boolean>;
417
+ /** Returns false when it does not exist. */
418
+ deleteCredential(vaultId: string, id: string): Promise<boolean>;
419
+ countCredentials(): Promise<number>;
420
+ insertVaultAudit(row: VaultAuditRow): Promise<void>;
421
+ /** Audit rows, oldest first. */
422
+ vaultAudit(filter?: {
423
+ vaultId?: string;
424
+ limit?: number;
425
+ }): Promise<VaultAuditRow[]>;
426
+ getVaultIdempotency(id: string): Promise<VaultIdempotencyRow | undefined>;
427
+ /** Rejects when the id exists. */
428
+ insertVaultIdempotency(row: VaultIdempotencyRow): Promise<void>;
429
+ getSetting(key: string): Promise<string | undefined>;
430
+ putSetting(key: string, value: string): Promise<void>;
431
+ /**
432
+ * Deletes Tenant state by scope, in this transaction:
433
+ * - `sessions`: sessions, commands, checkpoints, effects, actions, links and the outbox;
434
+ * - `sandboxes`: sandbox records;
435
+ * - `all`: both, plus definitions, executors and user vaults with their
436
+ * credentials. The host vault, principals, settings, audit and vault
437
+ * idempotency rows stay.
438
+ */
439
+ reset(scope: ResetScope): Promise<void>;
440
+ }
@@ -0,0 +1,10 @@
1
+ export const DOC_TABLES = [
2
+ "definitions",
3
+ "sessions",
4
+ "commands",
5
+ "checkpoints",
6
+ "effects",
7
+ "actions",
8
+ "sandboxes",
9
+ "links",
10
+ ];
@@ -0,0 +1,23 @@
1
+ /**
2
+ * S2 basin names for Tenants (architecture §12.4).
3
+ *
4
+ * S2 basin names are 8–48 characters of lowercase letters, digits and hyphens,
5
+ * and cannot begin or end with a hyphen. Tenant ids are `tn_` plus a 26
6
+ * character lowercase ULID, and `_` is not allowed, so:
7
+ *
8
+ * - a canonical Tenant id `tn_<ulid>` maps to `<prefix>tn-<ulid>` (29
9
+ * characters plus the prefix). The ULID contains no hyphen, so the mapping is
10
+ * one-to-one and readable in S2 tooling;
11
+ * - any other id (tests, legacy ids) maps to `<prefix>x-<slug>-<hash>`, where
12
+ * `slug` is the id lowercased with every run of other characters replaced by
13
+ * `-` (at most 12 characters) and `hash` is the first 16 hex digits of the
14
+ * id's SHA-256. The `x-` start keeps these apart from canonical names, and the
15
+ * hash keeps ids that slug alike apart.
16
+ *
17
+ * `prefix` separates deployments or test runs that share one S2 account (basin
18
+ * names are global in S2). It must itself be lowercase letters, digits and
19
+ * hyphens, start with a letter or digit, and be at most 16 characters.
20
+ */
21
+ export declare const MAX_BASIN_PREFIX_LENGTH = 16;
22
+ export declare function validateBasinPrefix(prefix: string): string;
23
+ export declare function tenantBasinName(tenantId: string, prefix?: string): string;
@@ -0,0 +1,51 @@
1
+ import { createHash } from "node:crypto";
2
+ import { TENANT_ID_PATTERN } from "@nylorun/core/compatibility";
3
+ /**
4
+ * S2 basin names for Tenants (architecture §12.4).
5
+ *
6
+ * S2 basin names are 8–48 characters of lowercase letters, digits and hyphens,
7
+ * and cannot begin or end with a hyphen. Tenant ids are `tn_` plus a 26
8
+ * character lowercase ULID, and `_` is not allowed, so:
9
+ *
10
+ * - a canonical Tenant id `tn_<ulid>` maps to `<prefix>tn-<ulid>` (29
11
+ * characters plus the prefix). The ULID contains no hyphen, so the mapping is
12
+ * one-to-one and readable in S2 tooling;
13
+ * - any other id (tests, legacy ids) maps to `<prefix>x-<slug>-<hash>`, where
14
+ * `slug` is the id lowercased with every run of other characters replaced by
15
+ * `-` (at most 12 characters) and `hash` is the first 16 hex digits of the
16
+ * id's SHA-256. The `x-` start keeps these apart from canonical names, and the
17
+ * hash keeps ids that slug alike apart.
18
+ *
19
+ * `prefix` separates deployments or test runs that share one S2 account (basin
20
+ * names are global in S2). It must itself be lowercase letters, digits and
21
+ * hyphens, start with a letter or digit, and be at most 16 characters.
22
+ */
23
+ export const MAX_BASIN_PREFIX_LENGTH = 16;
24
+ const BASIN_NAME = /^[a-z0-9][a-z0-9-]{6,46}[a-z0-9]$/;
25
+ const BASIN_PREFIX = /^(?:[a-z0-9][a-z0-9-]*)?$/;
26
+ export function validateBasinPrefix(prefix) {
27
+ if (prefix.length > MAX_BASIN_PREFIX_LENGTH || !BASIN_PREFIX.test(prefix))
28
+ throw new Error(`S2 basin prefix must be at most ${MAX_BASIN_PREFIX_LENGTH} lowercase letters, digits or hyphens, starting with a letter or digit`);
29
+ return prefix;
30
+ }
31
+ export function tenantBasinName(tenantId, prefix = "") {
32
+ validateBasinPrefix(prefix);
33
+ if (!tenantId)
34
+ throw new Error("tenantId is required");
35
+ let name;
36
+ if (TENANT_ID_PATTERN.test(tenantId)) {
37
+ name = `${prefix}tn-${tenantId.slice(3)}`;
38
+ }
39
+ else {
40
+ const slug = tenantId
41
+ .toLowerCase()
42
+ .replace(/[^a-z0-9]+/g, "-")
43
+ .slice(0, 12)
44
+ .replace(/^-+|-+$/g, "");
45
+ const hash = createHash("sha256").update(tenantId).digest("hex").slice(0, 16);
46
+ name = `${prefix}x-${slug ? `${slug}-` : ""}${hash}`;
47
+ }
48
+ if (!BASIN_NAME.test(name))
49
+ throw new Error(`Invalid S2 basin name ${name}`);
50
+ return name;
51
+ }
@@ -0,0 +1,25 @@
1
+ import type { AppendOptions, AppendResult, DurableStreams, ReadOptions, StreamRecord } from "./types.js";
2
+ /**
3
+ * In-memory `DurableStreams` for unit tests and a local development Host.
4
+ * Not durable: history does not survive a restart. Keeps the seam's
5
+ * sequence, conditional-append and resume semantics.
6
+ */
7
+ export declare class MemoryStreams implements DurableStreams {
8
+ /** tenantId → stream → records. */
9
+ private readonly tenants;
10
+ /** `tenantId\0stream` → wake-ups for readers waiting at the tail. */
11
+ private readonly waiters;
12
+ private closed;
13
+ append(tenantId: string, stream: string, records: readonly unknown[], options?: AppendOptions): Promise<AppendResult>;
14
+ read<T = unknown>(tenantId: string, stream: string, fromSeq: number, options?: ReadOptions): AsyncIterable<StreamRecord<T>>;
15
+ tail(tenantId: string, stream: string): Promise<number>;
16
+ ensureTenant(tenantId: string): Promise<void>;
17
+ deleteTenant(tenantId: string): Promise<void>;
18
+ deleteStream(tenantId: string, stream: string): Promise<void>;
19
+ listStreams(tenantId: string, prefix: string): Promise<string[]>;
20
+ close(): Promise<void>;
21
+ private log;
22
+ private waitForAppend;
23
+ private notify;
24
+ private notifyKey;
25
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * In-memory `DurableStreams` for unit tests and a local development Host.
3
+ * Not durable: history does not survive a restart. Keeps the seam's
4
+ * sequence, conditional-append and resume semantics.
5
+ */
6
+ export class MemoryStreams {
7
+ /** tenantId → stream → records. */
8
+ tenants = new Map();
9
+ /** `tenantId\0stream` → wake-ups for readers waiting at the tail. */
10
+ waiters = new Map();
11
+ closed = false;
12
+ async append(tenantId, stream, records, options = {}) {
13
+ if (this.closed)
14
+ throw new Error("DurableStreams is closed");
15
+ const streams = this.tenants.get(tenantId);
16
+ if (!streams)
17
+ throw new Error(`Tenant ${tenantId} has no stream basin`);
18
+ if (records.length === 0)
19
+ throw new Error("append needs at least one record");
20
+ const bodies = records.map((record) => {
21
+ const body = JSON.stringify(record);
22
+ if (body === undefined)
23
+ throw new Error("Stream records must be JSON");
24
+ return body;
25
+ });
26
+ let log = streams.get(stream);
27
+ const tail = log?.length ?? 0;
28
+ if (options.matchSeq !== undefined && options.matchSeq !== tail)
29
+ return { status: "seq_mismatch", tail };
30
+ if (!log)
31
+ streams.set(stream, (log = []));
32
+ const timestamp = Date.now();
33
+ for (const body of bodies)
34
+ log.push({ seq: log.length, timestamp, body });
35
+ this.notify(tenantId, stream);
36
+ return { status: "ok", start: tail, end: log.length };
37
+ }
38
+ read(tenantId, stream, fromSeq, options = {}) {
39
+ if (!Number.isInteger(fromSeq) || fromSeq < 0)
40
+ throw new Error("fromSeq must be a non-negative integer");
41
+ const follow = options.follow ?? true;
42
+ const signal = options.signal;
43
+ const self = this;
44
+ return {
45
+ async *[Symbol.asyncIterator]() {
46
+ let next = fromSeq;
47
+ const end = follow ? Infinity : self.log(tenantId, stream)?.length ?? 0;
48
+ while (!signal?.aborted && !self.closed) {
49
+ const log = self.log(tenantId, stream);
50
+ while (log && next < log.length && next < end) {
51
+ const record = log[next++];
52
+ yield {
53
+ seq: record.seq,
54
+ timestamp: record.timestamp,
55
+ body: JSON.parse(record.body),
56
+ };
57
+ if (signal?.aborted)
58
+ return;
59
+ }
60
+ if (next >= end || !self.tenants.has(tenantId))
61
+ return;
62
+ await self.waitForAppend(tenantId, stream, signal);
63
+ }
64
+ },
65
+ };
66
+ }
67
+ async tail(tenantId, stream) {
68
+ return this.log(tenantId, stream)?.length ?? 0;
69
+ }
70
+ async ensureTenant(tenantId) {
71
+ if (!this.tenants.has(tenantId))
72
+ this.tenants.set(tenantId, new Map());
73
+ }
74
+ async deleteTenant(tenantId) {
75
+ const streams = this.tenants.get(tenantId);
76
+ this.tenants.delete(tenantId);
77
+ for (const stream of streams?.keys() ?? [])
78
+ this.notify(tenantId, stream);
79
+ for (const key of this.waiters.keys())
80
+ if (key.startsWith(`${tenantId}\u0000`))
81
+ this.notifyKey(key);
82
+ }
83
+ async deleteStream(tenantId, stream) {
84
+ this.tenants.get(tenantId)?.delete(stream);
85
+ this.notify(tenantId, stream);
86
+ }
87
+ async listStreams(tenantId, prefix) {
88
+ return [...(this.tenants.get(tenantId)?.keys() ?? [])]
89
+ .filter((stream) => stream.startsWith(prefix))
90
+ .sort();
91
+ }
92
+ async close() {
93
+ this.closed = true;
94
+ for (const key of [...this.waiters.keys()])
95
+ this.notifyKey(key);
96
+ }
97
+ log(tenantId, stream) {
98
+ return this.tenants.get(tenantId)?.get(stream);
99
+ }
100
+ waitForAppend(tenantId, stream, signal) {
101
+ const key = `${tenantId}\u0000${stream}`;
102
+ return new Promise((resolve) => {
103
+ let set = this.waiters.get(key);
104
+ if (!set)
105
+ this.waiters.set(key, (set = new Set()));
106
+ const done = () => {
107
+ set.delete(done);
108
+ if (set.size === 0 && this.waiters.get(key) === set)
109
+ this.waiters.delete(key);
110
+ signal?.removeEventListener("abort", done);
111
+ resolve();
112
+ };
113
+ set.add(done);
114
+ signal?.addEventListener("abort", done, { once: true });
115
+ });
116
+ }
117
+ notify(tenantId, stream) {
118
+ this.notifyKey(`${tenantId}\u0000${stream}`);
119
+ }
120
+ notifyKey(key) {
121
+ for (const wake of [...(this.waiters.get(key) ?? [])])
122
+ wake();
123
+ }
124
+ }