@nylorun/runtime 0.10.0-beta → 0.12.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 (194) hide show
  1. package/CHANGELOG.md +86 -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 +13 -4
  13. package/dist/core/sandbox-routes.js +14 -12
  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 +51 -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 +645 -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/002_session_owner.d.ts +6 -0
  67. package/dist/store/postgres/migrations/002_session_owner.js +13 -0
  68. package/dist/store/postgres/migrations/index.d.ts +49 -0
  69. package/dist/store/postgres/migrations/index.js +76 -0
  70. package/dist/store/postgres/names.d.ts +11 -0
  71. package/dist/store/postgres/names.js +42 -0
  72. package/dist/store/postgres/store.d.ts +11 -0
  73. package/dist/store/postgres/store.js +797 -0
  74. package/dist/store/postgres/tenants.d.ts +90 -0
  75. package/dist/store/postgres/tenants.js +214 -0
  76. package/dist/store/types.d.ts +442 -0
  77. package/dist/store/types.js +10 -0
  78. package/dist/streams/basin.d.ts +23 -0
  79. package/dist/streams/basin.js +51 -0
  80. package/dist/streams/memory.d.ts +25 -0
  81. package/dist/streams/memory.js +124 -0
  82. package/dist/streams/relay.d.ts +37 -0
  83. package/dist/streams/relay.js +254 -0
  84. package/dist/streams/types.d.ts +115 -0
  85. package/dist/streams/types.js +86 -0
  86. package/dist/tenant/actions.d.ts +153 -0
  87. package/dist/tenant/actions.js +191 -0
  88. package/dist/tenant/advance.d.ts +15 -0
  89. package/dist/tenant/advance.js +439 -0
  90. package/dist/tenant/auth.d.ts +32 -0
  91. package/dist/tenant/auth.js +169 -0
  92. package/dist/tenant/commands.d.ts +3 -0
  93. package/dist/tenant/commands.js +393 -0
  94. package/dist/tenant/context.d.ts +147 -0
  95. package/dist/tenant/context.js +48 -0
  96. package/dist/tenant/effects.d.ts +20 -0
  97. package/dist/tenant/effects.js +564 -0
  98. package/dist/tenant/envelope.d.ts +0 -3
  99. package/dist/tenant/envelope.js +2 -34
  100. package/dist/tenant/ephemeral.d.ts +15 -4
  101. package/dist/tenant/ephemeral.js +106 -38
  102. package/dist/tenant/http.d.ts +38 -0
  103. package/dist/tenant/http.js +53 -0
  104. package/dist/tenant/live.d.ts +92 -0
  105. package/dist/tenant/live.js +290 -0
  106. package/dist/tenant/model-setting.d.ts +16 -0
  107. package/dist/tenant/model-setting.js +12 -0
  108. package/dist/tenant/module.d.ts +29 -8
  109. package/dist/tenant/module.js +165 -158
  110. package/dist/tenant/paths.d.ts +12 -8
  111. package/dist/tenant/paths.js +38 -12
  112. package/dist/tenant/principals.d.ts +12 -13
  113. package/dist/tenant/principals.js +50 -38
  114. package/dist/tenant/quarantine-error.d.ts +1 -1
  115. package/dist/tenant/quarantine-error.js +2 -8
  116. package/dist/tenant/quarantine.d.ts +0 -8
  117. package/dist/tenant/quarantine.js +8 -30
  118. package/dist/tenant/reset.d.ts +7 -6
  119. package/dist/tenant/reset.js +7 -51
  120. package/dist/tenant/routes-tenant.d.ts +12 -0
  121. package/dist/tenant/routes-tenant.js +137 -0
  122. package/dist/tenant/routes.d.ts +9 -0
  123. package/dist/tenant/routes.js +151 -0
  124. package/dist/tenant/runtime.d.ts +59 -58
  125. package/dist/tenant/runtime.js +243 -2201
  126. package/dist/tenant/scheduler.d.ts +25 -0
  127. package/dist/tenant/scheduler.js +84 -0
  128. package/dist/tenant/session.d.ts +60 -0
  129. package/dist/tenant/session.js +94 -0
  130. package/dist/tenant/sessions.d.ts +40 -0
  131. package/dist/tenant/sessions.js +163 -0
  132. package/dist/tenant/status.d.ts +13 -4
  133. package/dist/tenant/status.js +76 -41
  134. package/dist/tenant/store-memory.d.ts +9 -12
  135. package/dist/tenant/store-memory.js +79 -68
  136. package/dist/tenant/store-pg.d.ts +15 -0
  137. package/dist/tenant/store-pg.js +140 -0
  138. package/dist/tenant/streams.d.ts +126 -0
  139. package/dist/tenant/streams.js +419 -0
  140. package/dist/tenant/sweep.d.ts +15 -0
  141. package/dist/tenant/sweep.js +122 -0
  142. package/dist/tenant/transcript.d.ts +104 -0
  143. package/dist/tenant/transcript.js +58 -0
  144. package/dist/tenant/types.d.ts +101 -15
  145. package/dist/tenant/types.js +23 -1
  146. package/dist/tenant/worker.d.ts +84 -0
  147. package/dist/tenant/worker.js +189 -0
  148. package/dist/vault/service.d.ts +44 -31
  149. package/dist/vault/service.js +454 -360
  150. package/dist/version.d.ts +1 -1
  151. package/dist/version.js +1 -1
  152. package/package.json +15 -14
  153. package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
  154. package/dist/adapters/sandbox/microsandbox.js +0 -238
  155. package/dist/core/store.d.ts +0 -32
  156. package/dist/core/store.js +0 -143
  157. package/dist/launcher/commands.d.ts +0 -24
  158. package/dist/launcher/commands.js +0 -260
  159. package/dist/launcher/ensure-config.d.ts +0 -16
  160. package/dist/launcher/ensure-config.js +0 -79
  161. package/dist/launcher/errors.d.ts +0 -12
  162. package/dist/launcher/errors.js +0 -22
  163. package/dist/launcher/host-config.d.ts +0 -17
  164. package/dist/launcher/host-config.js +0 -64
  165. package/dist/launcher/host-state.d.ts +0 -17
  166. package/dist/launcher/host-state.js +0 -61
  167. package/dist/launcher/lifecycle.d.ts +0 -66
  168. package/dist/launcher/lifecycle.js +0 -407
  169. package/dist/launcher/locks.d.ts +0 -26
  170. package/dist/launcher/locks.js +0 -117
  171. package/dist/launcher/logs.d.ts +0 -18
  172. package/dist/launcher/logs.js +0 -117
  173. package/dist/launcher/main.d.ts +0 -2
  174. package/dist/launcher/main.js +0 -66
  175. package/dist/launcher/output.d.ts +0 -11
  176. package/dist/launcher/output.js +0 -68
  177. package/dist/launcher/paths.d.ts +0 -20
  178. package/dist/launcher/paths.js +0 -36
  179. package/dist/launcher/probe.d.ts +0 -23
  180. package/dist/launcher/probe.js +0 -72
  181. package/dist/launcher/protocol.d.ts +0 -62
  182. package/dist/launcher/protocol.js +0 -1
  183. package/dist/launcher/schema-guard.d.ts +0 -6
  184. package/dist/launcher/schema-guard.js +0 -36
  185. package/dist/launcher/spawn.d.ts +0 -22
  186. package/dist/launcher/spawn.js +0 -85
  187. package/dist/launcher/status.d.ts +0 -6
  188. package/dist/launcher/status.js +0 -65
  189. package/dist/tenant/migration.d.ts +0 -16
  190. package/dist/tenant/migration.js +0 -116
  191. package/dist/tenant/schema.d.ts +0 -23
  192. package/dist/tenant/schema.js +0 -151
  193. package/dist/tenant/store-fs.d.ts +0 -16
  194. package/dist/tenant/store-fs.js +0 -205
@@ -0,0 +1,58 @@
1
+ /** The model's tool call id and the harness invocation, for tool effects and tool actions. */
2
+ export function toolIds(context) {
3
+ const c = context;
4
+ return typeof c?.callId === "string" && typeof c.invocationId === "string"
5
+ ? { callId: c.callId, invocationId: c.invocationId }
6
+ : {};
7
+ }
8
+ const agentOf = (request) => request.agent ? { agent: request.agent } : {};
9
+ /**
10
+ * `message.assistant` for a completed model effect: the step's text and tool calls. The
11
+ * provider returns a `ModelCandidate` or a bare string; anything else writes no event.
12
+ */
13
+ export function assistantMessage(request, value) {
14
+ const invocationId = request.context
15
+ ?.invocationId;
16
+ if (typeof invocationId !== "string")
17
+ return undefined;
18
+ const blocks = typeof value === "string"
19
+ ? [{ type: "text", text: value }]
20
+ : Array.isArray(value?.output)
21
+ ? value.output
22
+ : [];
23
+ let text = "";
24
+ const toolCalls = [];
25
+ for (const block of blocks) {
26
+ if (block?.type === "text" && typeof block.text === "string")
27
+ text += block.text;
28
+ else if (block?.type === "tool-call" &&
29
+ typeof block.id === "string" &&
30
+ typeof block.name === "string")
31
+ toolCalls.push({ callId: block.id, name: block.name, input: block.args });
32
+ }
33
+ if (!text && toolCalls.length === 0)
34
+ return undefined;
35
+ return { invocationId, text, toolCalls, ...agentOf(request) };
36
+ }
37
+ /** `tool.completed` for an MCP or sandbox tool: its output, or the tool error it reported. */
38
+ export function toolCompleted(request, value) {
39
+ const ids = toolIds(request.context);
40
+ if (!("callId" in ids))
41
+ return undefined;
42
+ const outcome = value;
43
+ const result = outcome?.kind === "failed"
44
+ ? {
45
+ error: {
46
+ code: String(outcome.code ?? "tool.failed"),
47
+ message: String(outcome.message ?? ""),
48
+ },
49
+ }
50
+ : { output: outcome?.kind === "completed" ? outcome.output : value };
51
+ return {
52
+ ...ids,
53
+ capabilityId: request.capabilityId,
54
+ toolName: request.toolName,
55
+ ...result,
56
+ ...agentOf(request),
57
+ };
58
+ }
@@ -1,17 +1,20 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import type { AdminTenant, AdminTenantStatus, HostAggregate, TenantEnvelope } from "@nylorun/core/contracts";
3
3
  import type { FlowLimits } from "../core/limits.js";
4
+ import type { SessionStore } from "../store/types.js";
4
5
  import type { SandboxBackend } from "../sandbox/types.js";
6
+ import type { TenantWorker } from "./worker.js";
5
7
  export type TenantMode = "shared" | "ephemeral" | "test";
8
+ /**
9
+ * The Tenant directory on the Host root. The Tenant's data lives in its store (a Postgres
10
+ * schema); this holds what stays on the Host: the vault key, plugin data, logs, and the
11
+ * private home, tmp and sandbox directories.
12
+ */
6
13
  export interface TenantPaths {
7
14
  root: string;
8
- envelope: string;
9
- database: string;
10
- lock: string;
11
15
  kek: string;
12
16
  home: string;
13
17
  tmp: string;
14
- migration: string;
15
18
  sandboxes: string;
16
19
  pluginData: string;
17
20
  logs: string;
@@ -34,12 +37,20 @@ export interface TenantConfig {
34
37
  mode: TenantMode;
35
38
  paths: TenantPaths;
36
39
  sandbox: {
37
- backend: "auto" | "microsandbox" | "virtual";
40
+ backend: "auto" | "virtual";
38
41
  backends?: readonly SandboxBackend[];
39
42
  };
40
43
  model: TenantModelConfig;
41
44
  childEnv: Readonly<Record<string, string>>;
45
+ /** Action claim lease. Default 30 s. */
42
46
  leaseMs?: number;
47
+ /** Ownership lease of an advance (§10.6); renewed every third while it runs. Default 30 s. */
48
+ ownerLeaseMs?: number;
49
+ /**
50
+ * Delay between Tenant sweep passes when the Tenant runs its own in-process execution.
51
+ * Default `min(leaseMs, 5 s)`. A Host-level execution sets its own.
52
+ */
53
+ sweepIntervalMs?: number;
43
54
  /** Operator flow limits (`RuntimeOptions.flow` / `workflows.md` §13). */
44
55
  flow?: Partial<FlowLimits>;
45
56
  /**
@@ -61,13 +72,19 @@ export interface TenantSummary {
61
72
  connectedExecutors: number;
62
73
  pendingActions: number;
63
74
  uncertainEffects: number;
75
+ /** Events committed but not yet relayed to Durable Streams. */
76
+ outboxDepth?: number;
77
+ /** The oldest unrelayed event's age; 0 when none waits. */
78
+ relayLagMs?: number;
64
79
  }
65
80
  /** An open Tenant Runtime. Created only by the Tenant module. */
66
81
  export interface TenantHandle {
67
82
  readonly envelope: TenantEnvelope;
83
+ /** The handlers Durable Session Execution calls for this Tenant (`worker.ts`). */
84
+ readonly worker?: TenantWorker;
68
85
  /** Headers already validated by the Host. Authenticates, authorizes, dispatches. */
69
86
  handle(request: IncomingMessage, response: ServerResponse, url: URL): Promise<void>;
70
- summary(): TenantSummary;
87
+ summary(): Promise<TenantSummary>;
71
88
  /** Stop scheduling; wait for or cancel active turns. */
72
89
  drain(activeWork: "drain" | "cancel", timeoutMs?: number): Promise<void>;
73
90
  close(): Promise<void>;
@@ -82,22 +99,43 @@ export type TenantResolution = {
82
99
  quarantine: Quarantine;
83
100
  };
84
101
  export interface Quarantine {
85
- code: "locked" | "kek-missing" | "corrupt" | "schema-too-new" | "migration-failed" | "envelope-invalid" | "open-timeout" | "open-failed";
102
+ code: "kek-missing" | "corrupt" | "schema-too-new" | "migration-failed" | "envelope-invalid" | "open-timeout" | "open-failed";
86
103
  message: string;
87
104
  repair: string;
88
- lockPath?: string;
89
- lockPid?: number;
90
105
  }
91
106
  export interface BootstrapPrincipal {
92
107
  principalId: string;
93
108
  credentialHash: string;
94
109
  idempotencyKey: string;
110
+ /** When set, also registers application principal `studio` with this hash. */
111
+ studioCredentialHash?: string;
112
+ /** Application principals whose keys the admin key derives (feature `derived-principals`). */
113
+ derivedPrincipals?: readonly DerivedPrincipal[];
95
114
  }
96
- /** The deep module (§8). HTTP, CLI and tests use only this. */
115
+ export interface DerivedPrincipal {
116
+ id: string;
117
+ credentialHash: string;
118
+ }
119
+ /**
120
+ * The deep module (§8). HTTP, CLI and tests use only this.
121
+ *
122
+ * Tenants open on demand (architecture §8.2): the first `resolve`, `status`, `worker` or
123
+ * `delete` naming a Tenant opens it (bounded by the open timeout) and caches the handle or
124
+ * the quarantine; `start` only marks discovery done, so a Host with many Tenants opens none
125
+ * at startup.
126
+ */
97
127
  export interface TenantModule {
128
+ /** Marks discovery done (`/ready`). Opens nothing. */
98
129
  start(): Promise<void>;
99
130
  readonly started: boolean;
100
- resolve(id: string): TenantResolution;
131
+ /** Opens the Tenant on first use; a missing Tenant is `not-found`. */
132
+ resolve(id: string): Promise<TenantResolution>;
133
+ /**
134
+ * The Tenant's Worker handlers, opening it on demand, for `TenantWorkers.resolve`.
135
+ * Undefined when the Tenant does not exist, is quarantined or is being deleted, or the
136
+ * module is closed.
137
+ */
138
+ worker(id: string): Promise<TenantWorker | undefined>;
101
139
  create(input: {
102
140
  tenantId: string;
103
141
  name: string;
@@ -105,21 +143,69 @@ export interface TenantModule {
105
143
  envelope: TenantEnvelope;
106
144
  created: boolean;
107
145
  }>;
146
+ /**
147
+ * Every Tenant the store holds, without opening any. A Tenant not opened yet is listed
148
+ * `open` when its envelope reads; opening it may still quarantine it.
149
+ */
108
150
  list(): Promise<readonly AdminTenant[]>;
109
151
  status(id: string): Promise<AdminTenantStatus | undefined>;
110
152
  delete(id: string, activeWork: "refuse" | "drain" | "cancel"): Promise<void>;
111
- summarize(): HostAggregate;
153
+ summarize(): Promise<HostAggregate>;
112
154
  close(): Promise<void>;
113
155
  }
114
- /** Storage adapter behind the module (§8): OSS directory+SQLite, and in-memory for tests. */
156
+ /**
157
+ * Storage adapter behind the module (§8): a Postgres schema per Tenant (`store-pg.ts`), and
158
+ * in-memory for tests (`store-memory.ts`).
159
+ */
115
160
  export interface TenantStore {
161
+ /** Ids of every Tenant the store holds. */
116
162
  enumerate(): Promise<readonly string[]>;
163
+ /** Throws a quarantine error when unreadable, `TenantNotFoundError` when gone. */
117
164
  readEnvelope(id: string): Promise<TenantEnvelope>;
165
+ /**
166
+ * Creates the Tenant, or returns `exists` when it already exists (whatever its bootstrap
167
+ * material; the module compares it with `bootstrapMatches`). The stored envelope's
168
+ * `schemaVersion` is the store's own; read it back with `readEnvelope`.
169
+ */
118
170
  create(envelope: TenantEnvelope, bootstrap: BootstrapPrincipal): Promise<"created" | "exists">;
119
171
  bootstrapMatches(id: string, bootstrap: BootstrapPrincipal): Promise<boolean>;
172
+ /**
173
+ * Opens the Tenant Runtime; runs migration. Throws `TenantNotFoundError` when the Tenant
174
+ * does not exist, `TenantUnavailableError` when something outside the Tenant failed, and
175
+ * any other error (a quarantine error, ideally) when the Tenant itself cannot be opened.
176
+ */
120
177
  open(id: string): Promise<TenantHandle>;
178
+ /** Removes the Tenant: its data and its Tenant directory. */
121
179
  trash(id: string, now: Date): Promise<void>;
180
+ /** Removes what a failed `create` left, never an existing Tenant. */
122
181
  removePartial(id: string): Promise<void>;
123
182
  }
124
- /** Injected into the store so WS-B can test without WS-A. */
125
- export type OpenTenantRuntime = (config: TenantConfig) => Promise<TenantHandle>;
183
+ /**
184
+ * Thrown by a store when a Tenant cannot be reached for a reason outside it (the database
185
+ * or Durable Session Execution is unavailable). The module does not quarantine it: the next
186
+ * use tries again. The Host answers 503.
187
+ */
188
+ export declare class TenantUnavailableError extends Error {
189
+ readonly tenantId: string;
190
+ readonly status = 503;
191
+ constructor(tenantId: string, options?: {
192
+ cause?: unknown;
193
+ });
194
+ }
195
+ /** Thrown by `TenantStore.open` for a Tenant that does not exist. */
196
+ export declare class TenantNotFoundError extends Error {
197
+ readonly tenantId: string;
198
+ constructor(tenantId: string);
199
+ }
200
+ /** What a Tenant store hands the Tenant Runtime it opens. */
201
+ export interface OpenedTenant {
202
+ /**
203
+ * The Tenant's opened Session Store. The Tenant Runtime owns it from here on and closes it
204
+ * on close or on a failed open.
205
+ */
206
+ store: SessionStore;
207
+ /** The Tenant envelope as its store reports it. */
208
+ envelope: TenantEnvelope;
209
+ }
210
+ /** Opens the Tenant Runtime on a store the Tenant store opened (injected, so tests can fake it). */
211
+ export type OpenTenantRuntime = (config: TenantConfig, opened: OpenedTenant) => Promise<TenantHandle>;
@@ -1 +1,23 @@
1
- export {};
1
+ /**
2
+ * Thrown by a store when a Tenant cannot be reached for a reason outside it (the database
3
+ * or Durable Session Execution is unavailable). The module does not quarantine it: the next
4
+ * use tries again. The Host answers 503.
5
+ */
6
+ export class TenantUnavailableError extends Error {
7
+ tenantId;
8
+ status = 503;
9
+ constructor(tenantId, options) {
10
+ super("Tenant is temporarily unavailable", options);
11
+ this.tenantId = tenantId;
12
+ this.name = "TenantUnavailableError";
13
+ }
14
+ }
15
+ /** Thrown by `TenantStore.open` for a Tenant that does not exist. */
16
+ export class TenantNotFoundError extends Error {
17
+ tenantId;
18
+ constructor(tenantId) {
19
+ super(`Tenant ${tenantId} not found`);
20
+ this.tenantId = tenantId;
21
+ this.name = "TenantNotFoundError";
22
+ }
23
+ }
@@ -0,0 +1,84 @@
1
+ import type { AdvanceResult, DurableExecution, StuckInvocation, WorkerHandlers } from "../execution/types.js";
2
+ import type { Logger } from "./types.js";
3
+ /** This process's Worker id: the `owner` it writes on the sessions it advances (§10.6). */
4
+ export declare const WORKER_ID: string;
5
+ /**
6
+ * Default advance deadline: 50 minutes, below the one-hour inactivity and abort timeouts the
7
+ * Restate adapter sets on the session object, so an advance always ends before Restate gives
8
+ * up on it.
9
+ */
10
+ export declare const DEFAULT_ADVANCE_DEADLINE_MS: number;
11
+ /** Default time an aborted advance has to settle before it is abandoned. */
12
+ export declare const DEFAULT_ADVANCE_GRACE_MS = 30000;
13
+ /** Why an advance's signal aborted (see "Abort reasons" above). */
14
+ export type AdvanceAbortKind = "cancel" | "shutdown" | "deadline" | "ownership.lost";
15
+ /** The abort reason of an advance. */
16
+ export declare class AdvanceAbort extends Error {
17
+ readonly kind: AdvanceAbortKind;
18
+ readonly name: string;
19
+ constructor(kind: AdvanceAbortKind, message: string, options?: {
20
+ cause?: unknown;
21
+ });
22
+ }
23
+ /** The abort reason of an advance that ran past its deadline. */
24
+ export declare class AdvanceDeadlineError extends AdvanceAbort {
25
+ readonly deadlineMs: number;
26
+ readonly name = "AdvanceDeadlineError";
27
+ constructor(deadlineMs: number);
28
+ }
29
+ /**
30
+ * The kind of an aborted signal's reason, or `undefined` while it has not aborted. A reason
31
+ * that is not an `AdvanceAbort` came from outside the Tenant (the execution stopping) and
32
+ * counts as `shutdown`.
33
+ */
34
+ export declare function abortKind(signal: AbortSignal): AdvanceAbortKind | undefined;
35
+ /** One open Tenant's handlers. */
36
+ export interface TenantWorker {
37
+ /** Advances one session. Session outcomes never throw; only infrastructure errors do. */
38
+ advance(sessionId: string, signal: AbortSignal): Promise<AdvanceResult>;
39
+ /** One pass of the Tenant sweep. */
40
+ sweep(): Promise<void>;
41
+ }
42
+ export interface TenantWorkersOptions {
43
+ /** Finds or opens a Tenant that is not registered on this process. */
44
+ resolve?: (tenantId: string) => Promise<TenantWorker | undefined>;
45
+ /** How long one advance may run before its signal aborts. Default `DEFAULT_ADVANCE_DEADLINE_MS`. */
46
+ advanceDeadlineMs?: number;
47
+ /**
48
+ * How long an advance whose signal aborted has to return before it is abandoned.
49
+ * Default `DEFAULT_ADVANCE_GRACE_MS`.
50
+ */
51
+ advanceGraceMs?: number;
52
+ /** Receives a warning for each abandoned advance. */
53
+ logger?: Logger;
54
+ }
55
+ /** Registry of open Tenants' workers, dispatched to by `tenantId`. */
56
+ export declare class TenantWorkers {
57
+ private readonly options;
58
+ private readonly workers;
59
+ private readonly deadlineMs;
60
+ /**
61
+ * How long an aborted advance has to return before it is abandoned. Closing a Tenant waits
62
+ * as long for its advances before it abandons them (`scheduler.ts` `waitForIdle`).
63
+ */
64
+ readonly graceMs: number;
65
+ constructor(options?: TenantWorkersOptions);
66
+ /** Registers an open Tenant's worker; the returned function unregisters exactly it. */
67
+ register(tenantId: string, worker: TenantWorker): () => void;
68
+ get(tenantId: string): TenantWorker | undefined;
69
+ private find;
70
+ /** The handlers to start a `DurableExecution` with. */
71
+ readonly handlers: WorkerHandlers;
72
+ /** Runs one advance under the deadline and the grace period (see the module comment). */
73
+ private bounded;
74
+ }
75
+ /** A Durable Session Execution together with the registry its handlers dispatch through. */
76
+ export interface TenantExecution {
77
+ readonly execution: DurableExecution;
78
+ readonly workers: TenantWorkers;
79
+ /**
80
+ * The Tenant's invocations that need an operator (paused or backing off), for Tenant
81
+ * status. Absent when the execution cannot report them (in-process execution).
82
+ */
83
+ readonly stuckInvocations?: (tenantId: string) => Promise<StuckInvocation[]>;
84
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * The Worker side of Durable Session Execution (architecture §12.3): what a `DurableExecution`
3
+ * calls for one Tenant, and the registry that lets one execution serve every Tenant open on
4
+ * this process.
5
+ *
6
+ * - `TenantWorker` is one open Tenant's handlers: `advance(sessionId, signal)` (§10.5, in
7
+ * `advance.ts`) and `sweep()` (the Tenant sweep, in `sweep.ts`).
8
+ * - `TenantWorkers` is the registry. Its `handlers` are the `WorkerHandlers` an execution is
9
+ * started with; they dispatch by `tenantId` to the registered `TenantWorker`. A Tenant
10
+ * registers when it opens and unregisters when it closes. A call for a Tenant that is not
11
+ * open here asks the optional `resolve` hook (the Host can open it on demand); without one,
12
+ * the advance returns `done` and the sweep does nothing. Nothing is lost: the Tenant's sweep
13
+ * re-wakes its orphaned sessions once it is open again.
14
+ * - `TenantExecution` pairs an execution with its registry. The Host creates one per process
15
+ * (`host/execution.ts`) and passes it to every Tenant it opens; a Tenant opened without one
16
+ * (tests, ephemeral) gets its own in-process `MemoryExecution`.
17
+ *
18
+ * ## Advance deadline
19
+ *
20
+ * Restate's abort timeout does not interrupt a running handler (the advance's signal does not
21
+ * fire), so the registry bounds every advance itself:
22
+ *
23
+ * 1. At `advanceDeadlineMs` the advance's signal aborts with an `AdvanceDeadlineError`. The
24
+ * in-flight effect is aborted and the segment settles as a normal outcome (the effect and
25
+ * the turn become `uncertain`, or the turn fails with the deadline's message).
26
+ * 2. Once the signal has aborted, for whatever reason (deadline, Worker stop, Restate attempt
27
+ * end), the advance has `advanceGraceMs` to return. One that does not (an effect that
28
+ * ignores its signal) is abandoned: the handler returns `busy`, so the execution runs the
29
+ * session again. The abandoned advance has stopped renewing its lease (`advance.ts`), so
30
+ * the next advance takes over when the lease lapses (§11.4), and the epoch fences anything
31
+ * the abandoned one still tries to write.
32
+ *
33
+ * ## Abort reasons
34
+ *
35
+ * Every abort of an advance carries an `AdvanceAbort` whose `kind` says why, because the
36
+ * advance treats them differently (`advance.ts`, `effects.ts`):
37
+ *
38
+ * - `cancel`: a user cancel (or a reset) committed first. Results that arrive afterwards are
39
+ * discarded and the turn stays cancelled (§10.7).
40
+ * - `deadline`: the advance ran too long (`AdvanceDeadlineError`). The segment settles as a
41
+ * normal outcome.
42
+ * - `shutdown`: this Worker gives the advance up (Worker stop, Tenant close, the Restate
43
+ * attempt ended). Nothing about the turn is settled: outcomes already in hand are recorded,
44
+ * ownership is released and the advance returns `busy`, so the next advance, here or on
45
+ * another Worker, resumes the segment from its checkpoint with the cached outcomes.
46
+ * - `ownership.lost`: another advance took the session over; this one writes nothing more.
47
+ *
48
+ * An abort that arrives through the execution's signal is a `shutdown`, whatever its reason.
49
+ */
50
+ import { hostname } from "node:os";
51
+ import { randomBytes } from "node:crypto";
52
+ /** This process's Worker id: the `owner` it writes on the sessions it advances (§10.6). */
53
+ export const WORKER_ID = `worker-${hostname()}-${process.pid}-${randomBytes(4).toString("hex")}`;
54
+ /**
55
+ * Default advance deadline: 50 minutes, below the one-hour inactivity and abort timeouts the
56
+ * Restate adapter sets on the session object, so an advance always ends before Restate gives
57
+ * up on it.
58
+ */
59
+ export const DEFAULT_ADVANCE_DEADLINE_MS = 50 * 60_000;
60
+ /** Default time an aborted advance has to settle before it is abandoned. */
61
+ export const DEFAULT_ADVANCE_GRACE_MS = 30_000;
62
+ /** The abort reason of an advance. */
63
+ export class AdvanceAbort extends Error {
64
+ kind;
65
+ name = "AdvanceAbort";
66
+ constructor(kind, message, options) {
67
+ super(message, options);
68
+ this.kind = kind;
69
+ }
70
+ }
71
+ /** The abort reason of an advance that ran past its deadline. */
72
+ export class AdvanceDeadlineError extends AdvanceAbort {
73
+ deadlineMs;
74
+ name = "AdvanceDeadlineError";
75
+ constructor(deadlineMs) {
76
+ super("deadline", `The advance ran past its ${deadlineMs} ms deadline`);
77
+ this.deadlineMs = deadlineMs;
78
+ }
79
+ }
80
+ /**
81
+ * The kind of an aborted signal's reason, or `undefined` while it has not aborted. A reason
82
+ * that is not an `AdvanceAbort` came from outside the Tenant (the execution stopping) and
83
+ * counts as `shutdown`.
84
+ */
85
+ export function abortKind(signal) {
86
+ if (!signal.aborted)
87
+ return undefined;
88
+ return signal.reason instanceof AdvanceAbort ? signal.reason.kind : "shutdown";
89
+ }
90
+ /** The execution's abort reason as a `shutdown`, keeping its message. */
91
+ function shutdownOf(reason) {
92
+ if (reason instanceof AdvanceAbort)
93
+ return reason;
94
+ return new AdvanceAbort("shutdown", reason instanceof Error ? reason.message : "The Worker stopped the advance", { cause: reason });
95
+ }
96
+ const DONE = { status: "done" };
97
+ /** Registry of open Tenants' workers, dispatched to by `tenantId`. */
98
+ export class TenantWorkers {
99
+ options;
100
+ workers = new Map();
101
+ deadlineMs;
102
+ /**
103
+ * How long an aborted advance has to return before it is abandoned. Closing a Tenant waits
104
+ * as long for its advances before it abandons them (`scheduler.ts` `waitForIdle`).
105
+ */
106
+ graceMs;
107
+ constructor(options = {}) {
108
+ this.options = options;
109
+ this.deadlineMs = positive("advanceDeadlineMs", options.advanceDeadlineMs ?? DEFAULT_ADVANCE_DEADLINE_MS);
110
+ this.graceMs = positive("advanceGraceMs", options.advanceGraceMs ?? DEFAULT_ADVANCE_GRACE_MS);
111
+ }
112
+ /** Registers an open Tenant's worker; the returned function unregisters exactly it. */
113
+ register(tenantId, worker) {
114
+ this.workers.set(tenantId, worker);
115
+ return () => {
116
+ if (this.workers.get(tenantId) === worker)
117
+ this.workers.delete(tenantId);
118
+ };
119
+ }
120
+ get(tenantId) {
121
+ return this.workers.get(tenantId);
122
+ }
123
+ async find(tenantId) {
124
+ return this.workers.get(tenantId) ?? (await this.options.resolve?.(tenantId));
125
+ }
126
+ /** The handlers to start a `DurableExecution` with. */
127
+ handlers = {
128
+ advance: async (tenantId, sessionId, signal) => {
129
+ const worker = await this.find(tenantId);
130
+ return worker ? this.bounded(tenantId, sessionId, worker, signal) : DONE;
131
+ },
132
+ sweep: async (tenantId) => {
133
+ await (await this.find(tenantId))?.sweep();
134
+ },
135
+ };
136
+ /** Runs one advance under the deadline and the grace period (see the module comment). */
137
+ async bounded(tenantId, sessionId, worker, signal) {
138
+ const controller = new AbortController();
139
+ const abort = (reason) => {
140
+ if (!controller.signal.aborted)
141
+ controller.abort(reason);
142
+ };
143
+ const forward = () => abort(shutdownOf(signal.reason));
144
+ if (signal.aborted)
145
+ forward();
146
+ else
147
+ signal.addEventListener("abort", forward, { once: true });
148
+ const deadline = setTimeout(() => abort(new AdvanceDeadlineError(this.deadlineMs)), this.deadlineMs);
149
+ let grace;
150
+ let armGrace;
151
+ const abandoned = new Promise((resolve) => {
152
+ armGrace = () => {
153
+ grace = setTimeout(() => {
154
+ this.options.logger?.warn("advance abandoned after abort", {
155
+ tenantId,
156
+ sessionId,
157
+ graceMs: this.graceMs,
158
+ reason: controller.signal.reason instanceof Error
159
+ ? controller.signal.reason.message
160
+ : String(controller.signal.reason),
161
+ });
162
+ resolve({ status: "busy", retryAfterMs: 0 });
163
+ }, this.graceMs);
164
+ };
165
+ if (controller.signal.aborted)
166
+ armGrace();
167
+ else
168
+ controller.signal.addEventListener("abort", armGrace, { once: true });
169
+ });
170
+ try {
171
+ // `race` keeps handling the advance's outcome after it is abandoned.
172
+ return await Promise.race([worker.advance(sessionId, controller.signal), abandoned]);
173
+ }
174
+ finally {
175
+ clearTimeout(deadline);
176
+ clearTimeout(grace);
177
+ signal.removeEventListener("abort", forward);
178
+ if (armGrace)
179
+ controller.signal.removeEventListener("abort", armGrace);
180
+ }
181
+ }
182
+ }
183
+ /** The longest delay `setTimeout` keeps (about 24.8 days). */
184
+ const MAX_TIMEOUT_MS = 2 ** 31 - 1;
185
+ function positive(name, value) {
186
+ if (!Number.isFinite(value) || value <= 0 || value > MAX_TIMEOUT_MS)
187
+ throw new Error(`${name} must be positive and at most ${MAX_TIMEOUT_MS}`);
188
+ return value;
189
+ }
@@ -1,5 +1,5 @@
1
- import type { DatabaseSync } from "node:sqlite";
2
1
  import type { CreateCredentialRequest, CreateVaultRequest, CredentialInfo, CredentialSelection, HostModelProviderInfo, HostModelView, PutHostModelRequest, RotateCredentialRequest, SelectHostModelRequest, VaultInfo } from "@nylorun/core/contracts";
2
+ import type { SessionStore, Tx } from "../store/types.js";
3
3
  export type HostModelSecret = {
4
4
  provider: string;
5
5
  model: string;
@@ -31,27 +31,42 @@ export type AuthorizeResult = {
31
31
  credentialIds: string[];
32
32
  reason: string;
33
33
  };
34
+ export interface VaultServiceOptions {
35
+ store: SessionStore;
36
+ /** The Tenant key-encryption key. May read or create the key file. */
37
+ kek: () => Buffer;
38
+ /** Used only for OAuth refresh, always outside a transaction. */
39
+ fetch: typeof fetch;
40
+ }
34
41
  export declare class VaultService {
35
- private readonly db;
36
- private readonly tx;
42
+ private readonly store;
37
43
  private readonly kek;
38
44
  private readonly fetchImpl;
39
- constructor(db: DatabaseSync, tx: <T>(fn: () => T) => T, kek: () => Buffer, fetchImpl: typeof fetch);
40
- createVault(body: CreateVaultRequest): VaultInfo;
41
- listVaults(ownerUserId: string): VaultInfo[];
42
- getVault(id: string): VaultInfo;
43
- deleteVault(id: string): {
45
+ constructor(options: VaultServiceOptions);
46
+ createVault(body: CreateVaultRequest): Promise<VaultInfo>;
47
+ listVaults(ownerUserId: string): Promise<VaultInfo[]>;
48
+ getVault(id: string): Promise<VaultInfo>;
49
+ /** Another owner's vault, or the host vault, is the same 404 as a missing one. */
50
+ assertOwner(id: string, ownerUserId: string): Promise<void>;
51
+ deleteVault(id: string): Promise<{
44
52
  id: string;
45
- };
46
- createCredential(vaultId: string, body: CreateCredentialRequest): CredentialInfo;
47
- listCredentials(vaultId: string): CredentialInfo[];
48
- getCredential(vaultId: string, id: string): CredentialInfo;
49
- rotateCredential(vaultId: string, id: string, body: RotateCredentialRequest): CredentialInfo;
50
- deleteCredential(vaultId: string, id: string): {
53
+ }>;
54
+ createCredential(vaultId: string, body: CreateCredentialRequest): Promise<CredentialInfo>;
55
+ listCredentials(vaultId: string): Promise<CredentialInfo[]>;
56
+ getCredential(vaultId: string, id: string): Promise<CredentialInfo>;
57
+ rotateCredential(vaultId: string, id: string, body: RotateCredentialRequest): Promise<CredentialInfo>;
58
+ deleteCredential(vaultId: string, id: string): Promise<{
51
59
  id: string;
52
- };
53
- assertAttachment(ownerUserId: string, vaultIds: readonly string[], selections: readonly CredentialSelection[]): void;
54
- recordAttachment(sessionId: string, vaultIds: readonly string[]): void;
60
+ }>;
61
+ /**
62
+ * Checks a session's vaults and selections. Run in the session PUT transaction. `opaque`
63
+ * (a request acting for a subject) reports another owner's vault as the 404 of a missing one.
64
+ */
65
+ assertAttachment(t: Tx, ownerUserId: string, vaultIds: readonly string[], selections: readonly CredentialSelection[], options?: {
66
+ opaque?: boolean;
67
+ }): Promise<void>;
68
+ /** Writes the `attach` audit row. Run in the session PUT transaction. */
69
+ recordAttachment(t: Tx, sessionId: string, vaultIds: readonly string[]): Promise<void>;
55
70
  authorize(input: {
56
71
  sessionId: string;
57
72
  vaultIds: readonly string[];
@@ -59,13 +74,14 @@ export declare class VaultService {
59
74
  url: string;
60
75
  serverName?: string;
61
76
  }): Promise<AuthorizeResult>;
62
- getHostModel(): HostModelView;
63
- listHostProviders(): {
77
+ getHostModel(): Promise<HostModelView>;
78
+ listHostProviders(): Promise<{
64
79
  providers: HostModelProviderInfo[];
65
- };
66
- putHostModel(body: PutHostModelRequest): HostModelView;
67
- selectHostModel(body: SelectHostModelRequest): HostModelView;
68
- readHostModel(): HostModelSecret | undefined;
80
+ }>;
81
+ putHostModel(body: PutHostModelRequest): Promise<HostModelView>;
82
+ selectHostModel(body: SelectHostModelRequest): Promise<HostModelView>;
83
+ /** Reads the active host model secret. Decrypts after the read commits. */
84
+ readHostModel(): Promise<HostModelSecret | undefined>;
69
85
  updateHostCredential(credential: {
70
86
  type: "api_key" | "oauth";
71
87
  key?: string;
@@ -73,27 +89,24 @@ export declare class VaultService {
73
89
  refresh?: string;
74
90
  access?: string;
75
91
  expires?: number;
76
- }): void;
77
- reject(route: string): void;
92
+ }): Promise<void>;
93
+ reject(route: string): Promise<void>;
94
+ /** Calls the token endpoint outside any transaction, then stores the result. */
78
95
  private refresh;
79
96
  private replay;
80
- private insertCredential;
97
+ /** Re-seals a user credential's payload and returns the updated row. */
81
98
  private writePayload;
82
- private readPayload;
83
99
  private vaultInfo;
84
- private vaultRow;
85
100
  private ensureHostVault;
86
101
  private validateHostModel;
87
102
  private insertHostCredential;
88
- private readHostPayload;
103
+ private hostModelView;
89
104
  private hostModelRows;
90
105
  private hostCredentialRowFor;
91
106
  private activeHostCredentialRow;
92
107
  private activeProviderId;
93
108
  private setActiveProvider;
94
109
  private deleteHostProviderRows;
95
- private credentialInfo;
96
110
  private credentialRow;
97
- private rowsForVault;
98
111
  private audit;
99
112
  }