@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
@@ -10,11 +10,16 @@ export type SandboxRouteSession = SessionSandboxRef & {
10
10
  readonly activeTurnId: string | null;
11
11
  readonly status: string;
12
12
  };
13
+ /** A session together with a synchronous lookup over its sandbox owner chain. */
14
+ export type SandboxRouteRead = {
15
+ readonly session: SandboxRouteSession;
16
+ readonly lookup: (id: string) => SessionSandboxRef | undefined;
17
+ };
13
18
  export type SandboxRouteDeps = {
14
19
  readonly sandbox: SandboxManager;
15
- readonly session: (id: string) => SandboxRouteSession;
16
- readonly lookup: (id: string) => SessionSandboxRef | undefined;
17
- readonly getAction: (id: string) => Action | undefined;
20
+ /** The session (rejects when missing) and its owner chain, read in one transaction. */
21
+ readonly session: (id: string) => Promise<SandboxRouteRead>;
22
+ readonly getAction: (id: string) => Promise<Action | undefined>;
18
23
  };
19
24
  /**
20
25
  * `POST /v1/sessions/:id/sandbox/:tool` — application principal.
@@ -32,4 +37,8 @@ export declare function validateSandboxAttach(body: {
32
37
  sandbox?: {
33
38
  session: string;
34
39
  };
35
- }, newManifest: SessionSandboxRef["manifest"], lookup: (id: string) => SessionSandboxRef | undefined): string | undefined;
40
+ }, newManifest: SessionSandboxRef["manifest"], lookup: (id: string) => SessionSandboxRef | undefined,
41
+ /** Acting for a subject: another owner's session is the 404 of a missing one. */
42
+ options?: {
43
+ opaque?: boolean;
44
+ }): string | undefined;
@@ -34,9 +34,9 @@ function toolInput(body) {
34
34
  const { claimId: _c, generation: _g, requestId: _r, ...input } = record;
35
35
  return input;
36
36
  }
37
- async function runTool(deps, session, tool, input, signal) {
38
- const spec = resolveSpec(session, deps.lookup);
39
- const ownerId = owningSandboxSessionId(session, deps.lookup);
37
+ async function runTool(deps, { session, lookup }, tool, input, signal) {
38
+ const spec = resolveSpec(session, lookup);
39
+ const ownerId = owningSandboxSessionId(session, lookup);
40
40
  const ref = {
41
41
  id: ownerId,
42
42
  activeTurnId: session.activeTurnId,
@@ -49,11 +49,11 @@ async function runTool(deps, session, tool, input, signal) {
49
49
  * Refused while the session has an active agent turn.
50
50
  */
51
51
  export async function handleSessionSandboxTool(deps, sessionId, toolName, body, signal) {
52
- const session = deps.session(sessionId);
53
- if (session.activeTurnId)
52
+ const read = await deps.session(sessionId);
53
+ if (read.session.activeTurnId)
54
54
  throw new SandboxRouteError(409, "Sandbox is unavailable while the session has an active agent turn");
55
55
  const tool = parseTool(toolName);
56
- return runTool(deps, session, tool, toolInput(body), signal);
56
+ return runTool(deps, read, tool, toolInput(body), signal);
57
57
  }
58
58
  /**
59
59
  * `POST /v1/actions/:id/sandbox/:tool` — executor, authorized by the live claim.
@@ -69,7 +69,7 @@ export async function handleActionSandboxTool(deps, actionId, toolName, body, si
69
69
  throw new SandboxRouteError(400, "claimId is required");
70
70
  if (typeof generation !== "number" || !Number.isInteger(generation) || generation < 1)
71
71
  throw new SandboxRouteError(400, "generation must be a positive integer");
72
- const action = deps.getAction(actionId);
72
+ const action = await deps.getAction(actionId);
73
73
  if (!action)
74
74
  throw new SandboxRouteError(404, "Action not found");
75
75
  if (action.status !== "claimed" ||
@@ -78,19 +78,21 @@ export async function handleActionSandboxTool(deps, actionId, toolName, body, si
78
78
  !action.leaseExpiresAt ||
79
79
  Date.parse(action.leaseExpiresAt) <= Date.now())
80
80
  throw new SandboxRouteError(409, "Stale or expired claim");
81
- const session = deps.session(action.sessionId);
82
- if (session.activeTurnId !== action.turnId)
81
+ const read = await deps.session(action.sessionId);
82
+ if (read.session.activeTurnId !== action.turnId)
83
83
  throw new SandboxRouteError(409, "Action unavailable");
84
84
  const tool = parseTool(toolName);
85
- return runTool(deps, session, tool, toolInput(body), signal);
85
+ return runTool(deps, read, tool, toolInput(body), signal);
86
86
  }
87
87
  /** Validate PutSession.sandbox attach: same owner, identical specs. */
88
- export function validateSandboxAttach(body, newManifest, lookup) {
88
+ export function validateSandboxAttach(body, newManifest, lookup,
89
+ /** Acting for a subject: another owner's session is the 404 of a missing one. */
90
+ options = {}) {
89
91
  const share = body.sandbox?.session;
90
92
  if (!share)
91
93
  return undefined;
92
94
  const owner = lookup(share);
93
- if (!owner)
95
+ if (!owner || (options.opaque && owner.ownerUserId !== body.ownerUserId))
94
96
  throw new SandboxRouteError(404, "Sandbox session not found");
95
97
  if (owner.ownerUserId !== body.ownerUserId)
96
98
  throw new SandboxRouteError(403, "Sandbox session must belong to the same owner");
@@ -0,0 +1,54 @@
1
+ import { type DurableExecution, type Wake, type WorkerHandlers } from "./types.js";
2
+ export interface MemoryExecutionOptions {
3
+ /** Delay between sweep passes of one Tenant. Default 5000. */
4
+ sweepIntervalMs?: number;
5
+ /** First retry delay after `advance` throws; doubles per attempt. Default 50. */
6
+ retryDelayMs?: number;
7
+ /** Attempts before giving up on a throwing `advance` and reporting it. Default 5. */
8
+ maxAttempts?: number;
9
+ /** How long a dedupe key merges repeated wakes. Default 24 hours. */
10
+ dedupeRetentionMs?: number;
11
+ /** Receives handler failures that are not retried. */
12
+ onError?: (error: unknown) => void;
13
+ }
14
+ /**
15
+ * In-process `DurableExecution`. Not durable: wakes, timers and sweeps live in
16
+ * memory. It keeps the seam's ordering guarantees within one process, which is
17
+ * what unit tests and the Runtime before Restate (Wave 2) need.
18
+ */
19
+ export declare class MemoryExecution implements DurableExecution {
20
+ private handlers?;
21
+ private readonly keys;
22
+ /** Dedupe keys seen, with the time each was first seen; insertion order is age order. */
23
+ private readonly seen;
24
+ private readonly timers;
25
+ private readonly sweeps;
26
+ private readonly delayed;
27
+ private readonly inflight;
28
+ private readonly sweepIntervalMs;
29
+ private readonly retryDelayMs;
30
+ private readonly maxAttempts;
31
+ private readonly dedupeRetentionMs;
32
+ private readonly onError;
33
+ constructor(options?: MemoryExecutionOptions);
34
+ wake(tenantId: string, sessionId: string, wake: Wake): Promise<void>;
35
+ timer(tenantId: string, key: string, at: Date): Promise<void>;
36
+ armSweep(tenantId: string): Promise<void>;
37
+ disarmSweep(tenantId: string): Promise<void>;
38
+ start(handlers: WorkerHandlers): Promise<void>;
39
+ stop(): Promise<void>;
40
+ /**
41
+ * Resolves once no advance is running or queued and no busy or retry
42
+ * re-wake is pending. Timers and sweeps are not waited for. Test helper.
43
+ */
44
+ idle(): Promise<void>;
45
+ private enqueue;
46
+ private drain;
47
+ /** Drops dedupe keys older than the retention window. */
48
+ private forgetDedupeKeys;
49
+ private advanceOnce;
50
+ private armTimer;
51
+ private scheduleSweep;
52
+ private later;
53
+ private track;
54
+ }
@@ -0,0 +1,217 @@
1
+ import { WAKE_REASONS, sessionKey, } from "./types.js";
2
+ /**
3
+ * In-process `DurableExecution`. Not durable: wakes, timers and sweeps live in
4
+ * memory. It keeps the seam's ordering guarantees within one process, which is
5
+ * what unit tests and the Runtime before Restate (Wave 2) need.
6
+ */
7
+ export class MemoryExecution {
8
+ handlers;
9
+ keys = new Map();
10
+ /** Dedupe keys seen, with the time each was first seen; insertion order is age order. */
11
+ seen = new Map();
12
+ timers = new Map();
13
+ sweeps = new Map();
14
+ delayed = new Set();
15
+ inflight = new Set();
16
+ sweepIntervalMs;
17
+ retryDelayMs;
18
+ maxAttempts;
19
+ dedupeRetentionMs;
20
+ onError;
21
+ constructor(options = {}) {
22
+ this.sweepIntervalMs = options.sweepIntervalMs ?? 5000;
23
+ this.retryDelayMs = options.retryDelayMs ?? 50;
24
+ this.maxAttempts = options.maxAttempts ?? 5;
25
+ this.dedupeRetentionMs = options.dedupeRetentionMs ?? 24 * 60 * 60_000;
26
+ this.onError =
27
+ options.onError ??
28
+ ((error) => queueMicrotask(() => {
29
+ throw error;
30
+ }));
31
+ }
32
+ async wake(tenantId, sessionId, wake) {
33
+ if (!WAKE_REASONS.includes(wake.reason))
34
+ throw new Error(`Unknown wake reason: ${String(wake.reason)}`);
35
+ const key = sessionKey(tenantId, sessionId);
36
+ if (wake.dedupeKey !== undefined) {
37
+ const now = Date.now();
38
+ this.forgetDedupeKeys(now);
39
+ const dedupe = `${key}\u0000${wake.dedupeKey}`;
40
+ if (this.seen.has(dedupe))
41
+ return;
42
+ this.seen.set(dedupe, now);
43
+ }
44
+ this.enqueue(tenantId, sessionId);
45
+ }
46
+ async timer(tenantId, key, at) {
47
+ const id = `${tenantId}\u0000${key}`;
48
+ const existing = this.timers.get(id);
49
+ if (existing?.handle)
50
+ clearTimeout(existing.handle);
51
+ const timer = { tenantId, key, at: at.getTime() };
52
+ this.timers.set(id, timer);
53
+ if (this.handlers)
54
+ this.armTimer(id, timer);
55
+ }
56
+ async armSweep(tenantId) {
57
+ if (this.sweeps.has(tenantId))
58
+ return;
59
+ const sweep = { running: false };
60
+ this.sweeps.set(tenantId, sweep);
61
+ if (this.handlers)
62
+ this.scheduleSweep(tenantId, sweep, 0);
63
+ }
64
+ async disarmSweep(tenantId) {
65
+ const sweep = this.sweeps.get(tenantId);
66
+ if (sweep?.handle)
67
+ clearTimeout(sweep.handle);
68
+ this.sweeps.delete(tenantId);
69
+ }
70
+ async start(handlers) {
71
+ if (this.handlers)
72
+ throw new Error("DurableExecution already started");
73
+ this.handlers = handlers;
74
+ for (const state of this.keys.values())
75
+ if (state.queued && !state.running)
76
+ this.drain(state);
77
+ for (const [id, timer] of this.timers)
78
+ this.armTimer(id, timer);
79
+ for (const [tenantId, sweep] of this.sweeps)
80
+ this.scheduleSweep(tenantId, sweep, 0);
81
+ }
82
+ async stop() {
83
+ this.handlers = undefined;
84
+ for (const handle of this.delayed)
85
+ clearTimeout(handle);
86
+ this.delayed.clear();
87
+ for (const timer of this.timers.values())
88
+ if (timer.handle)
89
+ clearTimeout(timer.handle);
90
+ for (const sweep of this.sweeps.values())
91
+ if (sweep.handle)
92
+ clearTimeout(sweep.handle);
93
+ for (const state of this.keys.values())
94
+ state.controller?.abort();
95
+ while (this.inflight.size > 0)
96
+ await Promise.allSettled(this.inflight);
97
+ }
98
+ /**
99
+ * Resolves once no advance is running or queued and no busy or retry
100
+ * re-wake is pending. Timers and sweeps are not waited for. Test helper.
101
+ */
102
+ async idle() {
103
+ while (this.delayed.size > 0 ||
104
+ this.inflight.size > 0 ||
105
+ [...this.keys.values()].some((s) => s.running || (s.queued && this.handlers)))
106
+ await new Promise((resolve) => setTimeout(resolve, 1));
107
+ }
108
+ enqueue(tenantId, sessionId) {
109
+ const key = sessionKey(tenantId, sessionId);
110
+ let state = this.keys.get(key);
111
+ if (!state)
112
+ this.keys.set(key, (state = {
113
+ tenantId,
114
+ sessionId,
115
+ running: false,
116
+ queued: false,
117
+ attempts: 0,
118
+ }));
119
+ state.queued = true;
120
+ if (!state.running && this.handlers)
121
+ this.drain(state);
122
+ }
123
+ drain(state) {
124
+ state.running = true;
125
+ this.track((async () => {
126
+ try {
127
+ while (state.queued && this.handlers) {
128
+ state.queued = false;
129
+ await this.advanceOnce(state, this.handlers);
130
+ }
131
+ }
132
+ finally {
133
+ state.running = false;
134
+ // Idle keys hold nothing (a retry keeps its attempt count); forget them so
135
+ // long-lived processes stay small.
136
+ const key = sessionKey(state.tenantId, state.sessionId);
137
+ if (!state.queued && state.attempts === 0 && this.keys.get(key) === state)
138
+ this.keys.delete(key);
139
+ }
140
+ })());
141
+ }
142
+ /** Drops dedupe keys older than the retention window. */
143
+ forgetDedupeKeys(now) {
144
+ for (const [dedupe, seenAt] of this.seen) {
145
+ if (now - seenAt < this.dedupeRetentionMs)
146
+ break;
147
+ this.seen.delete(dedupe);
148
+ }
149
+ }
150
+ async advanceOnce(state, handlers) {
151
+ const controller = new AbortController();
152
+ state.controller = controller;
153
+ try {
154
+ const result = await handlers.advance(state.tenantId, state.sessionId, controller.signal);
155
+ state.attempts = 0;
156
+ if (result.status === "busy")
157
+ this.later(result.retryAfterMs, () => this.enqueue(state.tenantId, state.sessionId));
158
+ }
159
+ catch (error) {
160
+ state.attempts += 1;
161
+ if (state.attempts >= this.maxAttempts || controller.signal.aborted) {
162
+ state.attempts = 0;
163
+ this.onError(error);
164
+ }
165
+ else
166
+ this.later(this.retryDelayMs * 2 ** (state.attempts - 1), () => this.enqueue(state.tenantId, state.sessionId));
167
+ }
168
+ finally {
169
+ state.controller = undefined;
170
+ }
171
+ }
172
+ armTimer(id, timer) {
173
+ timer.handle = setTimeout(() => {
174
+ if (this.timers.get(id) !== timer)
175
+ return;
176
+ this.timers.delete(id);
177
+ const handlers = this.handlers;
178
+ if (!handlers)
179
+ return;
180
+ if (!handlers.fire) {
181
+ this.onError(new Error("WorkerHandlers.fire is required for timers"));
182
+ return;
183
+ }
184
+ this.track(handlers.fire(timer.tenantId, timer.key).catch(this.onError));
185
+ }, Math.max(0, timer.at - Date.now()));
186
+ }
187
+ scheduleSweep(tenantId, sweep, delay) {
188
+ if (sweep.running || sweep.handle)
189
+ return;
190
+ sweep.handle = setTimeout(() => {
191
+ sweep.handle = undefined;
192
+ const handlers = this.handlers;
193
+ if (!handlers || this.sweeps.get(tenantId) !== sweep)
194
+ return;
195
+ sweep.running = true;
196
+ this.track(handlers
197
+ .sweep(tenantId)
198
+ .catch(this.onError)
199
+ .finally(() => {
200
+ sweep.running = false;
201
+ if (this.handlers && this.sweeps.get(tenantId) === sweep)
202
+ this.scheduleSweep(tenantId, sweep, this.sweepIntervalMs);
203
+ }));
204
+ }, delay);
205
+ }
206
+ later(ms, fn) {
207
+ const handle = setTimeout(() => {
208
+ this.delayed.delete(handle);
209
+ fn();
210
+ }, Math.max(0, ms));
211
+ this.delayed.add(handle);
212
+ }
213
+ track(promise) {
214
+ this.inflight.add(promise);
215
+ void promise.finally(() => this.inflight.delete(promise));
216
+ }
217
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * The Durable Session Execution seam (architecture §12.3).
3
+ *
4
+ * Durable Session Execution decides when to look at a session again. It never
5
+ * holds session state: every outcome lives in the Session Store, and `advance`
6
+ * is safe to repeat (§10.1). Restate is the supported implementation
7
+ * (`adapters/execution/restate.ts`); `execution/memory.ts` is the in-process
8
+ * implementation used by unit tests, `startEphemeralRuntime` and a Host
9
+ * started without Restate endpoints.
10
+ *
11
+ * ## Guarantees every implementation keeps
12
+ *
13
+ * - **One advance per key at a time.** The key is `<tenantId>:<sessionId>`.
14
+ * Two `advance` calls for one key never overlap, on one process or many.
15
+ * Different keys may run concurrently.
16
+ * - **At least once after a wake.** When `wake` resolves, an `advance` for the
17
+ * key will start after that point (possibly merged with other wakes). Wakes
18
+ * are sent after the Session Store transaction that caused them commits
19
+ * (`Tx.afterCommit`), never inside it.
20
+ * - **Dedupe.** Wakes carrying the same `dedupeKey` for the same key cause at
21
+ * most one advance for that cause (within the implementation's retention
22
+ * window, at least 24 hours for Restate).
23
+ * - **Busy re-wake.** An advance that returns `busy` is re-run for the same key
24
+ * after `retryAfterMs`.
25
+ * - **Retries on infrastructure errors.** A handler that throws is retried with
26
+ * backoff. Session outcomes never throw: a turn that fails, pauses, waits or
27
+ * becomes `uncertain` is settled in the Session Store and `advance` returns
28
+ * `done`.
29
+ * - **Sweeps.** `armSweep` arms one self-re-arming sweep per Tenant; arming an
30
+ * armed Tenant is a no-op. The sweep expires claims, re-wakes orphaned
31
+ * `runnable` sessions and relays outbox rows left behind.
32
+ */
33
+ /** Why a session is woken (§12.3, "Where wakes come from"). */
34
+ export type WakeReason =
35
+ /** Session commands. */
36
+ "message" | "approve" | "respond"
37
+ /** An executor posted an Action result. */
38
+ | "action_result"
39
+ /** A linked agent turn completed, failed or was cancelled. */
40
+ | "linked"
41
+ /** Queued flow effects, pending agent effects reconciled. */
42
+ | "flow"
43
+ /** The Tenant sweep and `busy` re-wakes. */
44
+ | "recover";
45
+ export declare const WAKE_REASONS: readonly WakeReason[];
46
+ export interface Wake {
47
+ reason: WakeReason;
48
+ /**
49
+ * Merges repeated wakes for the same cause, e.g. the command's idempotency
50
+ * key. Scoped to the session key. Absent means never deduped.
51
+ */
52
+ dedupeKey?: string;
53
+ }
54
+ export type AdvanceResult = {
55
+ status: "done";
56
+ }
57
+ /** Another Worker holds a live lease; run again for this key after `retryAfterMs`. */
58
+ | {
59
+ status: "busy";
60
+ retryAfterMs: number;
61
+ };
62
+ /** What a Worker runs when Durable Session Execution calls it. */
63
+ export interface WorkerHandlers {
64
+ /**
65
+ * Advances one session (§10.5). `signal` aborts when the Worker stops or the
66
+ * session is cancelled through the control stream. Throw only on
67
+ * infrastructure errors.
68
+ */
69
+ advance(tenantId: string, sessionId: string, signal: AbortSignal): Promise<AdvanceResult>;
70
+ /** One pass of the Tenant sweep. Re-armed by the implementation afterwards. */
71
+ sweep(tenantId: string): Promise<void>;
72
+ /** A durable timer set with `timer` fired. Required when `timer` is used. */
73
+ fire?(tenantId: string, key: string): Promise<void>;
74
+ }
75
+ export interface DurableExecution {
76
+ /** At-least-once. `dedupeKey` merges repeated wakes for the same cause. */
77
+ wake(tenantId: string, sessionId: string, wake: Wake): Promise<void>;
78
+ /**
79
+ * Durable one-shot timer that calls `WorkerHandlers.fire(tenantId, key)` at
80
+ * or after `at`. Setting a key again replaces the earlier time where the
81
+ * implementation can; `fire` must be idempotent either way.
82
+ */
83
+ timer(tenantId: string, key: string, at: Date): Promise<void>;
84
+ /** Arms the Tenant's self-re-arming sweep. Idempotent. */
85
+ armSweep(tenantId: string): Promise<void>;
86
+ /** Stops re-arming the Tenant's sweep (Tenant deleted). A pass already running finishes. */
87
+ disarmSweep(tenantId: string): Promise<void>;
88
+ /** Starts delivering to `handlers` (serving the Worker endpoint, for Restate). */
89
+ start(handlers: WorkerHandlers): Promise<void>;
90
+ /** Stops delivering, aborts running advances' signals and waits for them. */
91
+ stop(): Promise<void>;
92
+ /**
93
+ * Resolves when the backing service answers, rejects otherwise (readiness,
94
+ * `infra/execution.ts`). Absent for in-process implementations.
95
+ */
96
+ probe?(signal: AbortSignal): Promise<void>;
97
+ /**
98
+ * Invocations for `tenantId` that need an operator: paused after exhausting
99
+ * retries, or backing off after failures (Tenant status). Absent for
100
+ * in-process implementations.
101
+ */
102
+ stuckInvocations?(tenantId: string): Promise<StuckInvocation[]>;
103
+ }
104
+ /** An invocation that needs an operator (`DurableExecution.stuckInvocations`). */
105
+ export interface StuckInvocation {
106
+ id: string;
107
+ /** `paused` or `backing-off`. */
108
+ status: string;
109
+ /** Service name without any prefix, e.g. `NylorunSession`. */
110
+ service: string;
111
+ handler: string;
112
+ /** Object key: `<tenantId>:<sessionId>`, `<tenantId>` or `<tenantId>:<timer key>`. */
113
+ key: string;
114
+ tenantId?: string;
115
+ retryCount: number;
116
+ lastFailure?: string;
117
+ modifiedAt?: string;
118
+ }
119
+ /** The key one advance at a time is serialized on. */
120
+ export declare function sessionKey(tenantId: string, sessionId: string): string;
121
+ /** Splits a key made by `sessionKey`. Tenant ids never contain `:`. */
122
+ export declare function parseSessionKey(key: string): {
123
+ tenantId: string;
124
+ sessionId: string;
125
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The Durable Session Execution seam (architecture §12.3).
3
+ *
4
+ * Durable Session Execution decides when to look at a session again. It never
5
+ * holds session state: every outcome lives in the Session Store, and `advance`
6
+ * is safe to repeat (§10.1). Restate is the supported implementation
7
+ * (`adapters/execution/restate.ts`); `execution/memory.ts` is the in-process
8
+ * implementation used by unit tests, `startEphemeralRuntime` and a Host
9
+ * started without Restate endpoints.
10
+ *
11
+ * ## Guarantees every implementation keeps
12
+ *
13
+ * - **One advance per key at a time.** The key is `<tenantId>:<sessionId>`.
14
+ * Two `advance` calls for one key never overlap, on one process or many.
15
+ * Different keys may run concurrently.
16
+ * - **At least once after a wake.** When `wake` resolves, an `advance` for the
17
+ * key will start after that point (possibly merged with other wakes). Wakes
18
+ * are sent after the Session Store transaction that caused them commits
19
+ * (`Tx.afterCommit`), never inside it.
20
+ * - **Dedupe.** Wakes carrying the same `dedupeKey` for the same key cause at
21
+ * most one advance for that cause (within the implementation's retention
22
+ * window, at least 24 hours for Restate).
23
+ * - **Busy re-wake.** An advance that returns `busy` is re-run for the same key
24
+ * after `retryAfterMs`.
25
+ * - **Retries on infrastructure errors.** A handler that throws is retried with
26
+ * backoff. Session outcomes never throw: a turn that fails, pauses, waits or
27
+ * becomes `uncertain` is settled in the Session Store and `advance` returns
28
+ * `done`.
29
+ * - **Sweeps.** `armSweep` arms one self-re-arming sweep per Tenant; arming an
30
+ * armed Tenant is a no-op. The sweep expires claims, re-wakes orphaned
31
+ * `runnable` sessions and relays outbox rows left behind.
32
+ */
33
+ export const WAKE_REASONS = [
34
+ "message",
35
+ "approve",
36
+ "respond",
37
+ "action_result",
38
+ "linked",
39
+ "flow",
40
+ "recover",
41
+ ];
42
+ /** The key one advance at a time is serialized on. */
43
+ export function sessionKey(tenantId, sessionId) {
44
+ return `${tenantId}:${sessionId}`;
45
+ }
46
+ /** Splits a key made by `sessionKey`. Tenant ids never contain `:`. */
47
+ export function parseSessionKey(key) {
48
+ const at = key.indexOf(":");
49
+ if (at <= 0 || at === key.length - 1)
50
+ throw new Error(`Invalid session key: ${key}`);
51
+ return { tenantId: key.slice(0, at), sessionId: key.slice(at + 1) };
52
+ }
@@ -2,7 +2,8 @@ import type { HostConfigFile } from "./config.js";
2
2
  import type { Logger, TenantConfig } from "../tenant/types.js";
3
3
  /**
4
4
  * Builds a TenantConfig for `openTenantRuntime`.
5
- * Reads seeded sandbox backend from the Tenant database when present (I1).
5
+ * The sandbox backend here is the Host default; a Tenant's seeded `sandbox.backend`
6
+ * setting overrides it when the Tenant opens (I1, A18).
6
7
  */
7
8
  export declare function configForFactory(options: {
8
9
  hostRoot: string;
@@ -1,33 +1,15 @@
1
- import { existsSync } from "node:fs";
2
- import { DatabaseSync } from "node:sqlite";
3
1
  import { tenantPaths } from "../tenant/paths.js";
4
- import { readTenantConfig } from "../tenant/schema.js";
5
2
  import { tenantChildEnvironment } from "./environment.js";
6
3
  /**
7
4
  * Builds a TenantConfig for `openTenantRuntime`.
8
- * Reads seeded sandbox backend from the Tenant database when present (I1).
5
+ * The sandbox backend here is the Host default; a Tenant's seeded `sandbox.backend`
6
+ * setting overrides it when the Tenant opens (I1, A18).
9
7
  */
10
8
  export function configForFactory(options) {
11
9
  const { baseline } = options;
12
10
  return (id) => {
13
11
  const tenant = tenantPaths(options.hostRoot, id);
14
- let sandboxBackend = "auto";
15
- if (existsSync(tenant.database)) {
16
- try {
17
- const db = new DatabaseSync(tenant.database, { readOnly: true });
18
- try {
19
- const seeded = readTenantConfig(db).sandboxBackend;
20
- if (seeded)
21
- sandboxBackend = seeded;
22
- }
23
- finally {
24
- db.close();
25
- }
26
- }
27
- catch {
28
- /* unreadable — keep default */
29
- }
30
- }
12
+ const sandboxBackend = "auto";
31
13
  return {
32
14
  tenantId: id,
33
15
  mode: options.mode ?? "shared",
@@ -10,14 +10,6 @@ export interface HostConfigFile {
10
10
  nodeExtraCaCerts?: string;
11
11
  };
12
12
  }
13
- /** host-state.json — process only */
14
- export interface HostStateFile {
15
- pid: number;
16
- startedAt: string;
17
- version: string;
18
- entry: string;
19
- url: string;
20
- }
21
13
  /** host-credentials.json, mode 0600 */
22
14
  export interface HostCredentialsFile {
23
15
  adminKey: string;
@@ -1,5 +1,6 @@
1
- import type { Logger, TenantModule } from "../tenant/types.js";
1
+ import { type Logger, type TenantModule } from "../tenant/types.js";
2
2
  import type { HostConfigFile, HostCredentialsFile } from "./config.js";
3
+ import type { ContainerListen } from "./stack-config.js";
3
4
  export interface CreateHostOptions {
4
5
  hostRoot: string;
5
6
  module: TenantModule;
@@ -10,10 +11,44 @@ export interface CreateHostOptions {
10
11
  coreVersion: string;
11
12
  /** Process id reported by `/health` and admin host status. Defaults to `process.pid`. */
12
13
  pid?: number;
14
+ /**
15
+ * Container mode: bind this address and port instead of host.json's, and
16
+ * accept only `allowedHosts` in the `Host` check (replacing the
17
+ * loopback-only rule). host.json then describes the client-facing address.
18
+ */
19
+ listen?: ContainerListen;
20
+ /**
21
+ * The client-facing URL `/v1/admin/status` reports as `host.url`
22
+ * (`NYLORUN_PUBLIC_URL`). Defaults to the bound address, which in container
23
+ * mode is `http://0.0.0.0:4000`.
24
+ */
25
+ publicUrl?: string;
26
+ /**
27
+ * Infrastructure readiness (`infra/readiness.ts`). `/ready` adds its checks
28
+ * and answers 503 while it reports not ok. Default: listener and discovery
29
+ * only.
30
+ */
31
+ readiness?: () => Promise<{
32
+ ok: boolean;
33
+ checks: Record<string, boolean>;
34
+ }>;
35
+ /**
36
+ * Shutdown steps around closing the Tenants. `close()` runs them whatever asked for it
37
+ * (SIGTERM in `host/main.ts`, `POST /v1/admin/host/shutdown`): the listener stops, then
38
+ * `beforeTenants` (stop the Worker so no advance starts on a closing Tenant), the Tenants
39
+ * close, then `afterTenants` (end the infrastructure clients). A failing step is logged
40
+ * and shutdown goes on.
41
+ */
42
+ shutdown?: {
43
+ beforeTenants?(): Promise<void>;
44
+ afterTenants?(): Promise<void>;
45
+ };
13
46
  }
14
47
  export interface HostServer {
15
48
  listen(): Promise<void>;
16
49
  close(): Promise<void>;
50
+ /** Settles once `close()` has finished, whatever called it. */
51
+ readonly closed: Promise<void>;
17
52
  readonly url: string;
18
53
  }
19
54
  /** Constant-time comparison of SHA-256 digests of the presented and stored admin keys. */