@nylorun/runtime 0.12.0-beta → 0.14.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 (223) hide show
  1. package/CHANGELOG.md +361 -0
  2. package/README.md +69 -15
  3. package/dist/adapters/execution/restate.d.ts +7 -2
  4. package/dist/adapters/execution/restate.js +38 -8
  5. package/dist/adapters/replication/pgoutput.d.ts +16 -0
  6. package/dist/adapters/replication/pgoutput.js +169 -0
  7. package/dist/adapters/streams/s2.js +37 -20
  8. package/dist/admin-openapi.json +1071 -0
  9. package/dist/api/a2a/card.d.ts +15 -0
  10. package/dist/api/a2a/card.js +24 -0
  11. package/dist/api/a2a/endpoint.d.ts +8 -0
  12. package/dist/api/a2a/endpoint.js +81 -0
  13. package/dist/api/a2a/protocol.d.ts +144 -0
  14. package/dist/api/a2a/protocol.js +188 -0
  15. package/dist/api/a2a/routes.d.ts +19 -0
  16. package/dist/api/a2a/routes.js +289 -0
  17. package/dist/api/a2a/tasks.d.ts +50 -0
  18. package/dist/api/a2a/tasks.js +186 -0
  19. package/dist/api/ag-ui/endpoint.d.ts +8 -0
  20. package/dist/api/ag-ui/endpoint.js +126 -0
  21. package/dist/api/ag-ui/history.d.ts +7 -0
  22. package/dist/api/ag-ui/history.js +60 -0
  23. package/dist/api/ag-ui/routes.d.ts +32 -0
  24. package/dist/api/ag-ui/routes.js +305 -0
  25. package/dist/api/ag-ui/session-id.d.ts +1 -0
  26. package/dist/api/ag-ui/session-id.js +12 -0
  27. package/dist/api/ag-ui/sse.d.ts +9 -0
  28. package/dist/api/ag-ui/sse.js +7 -0
  29. package/dist/api/ag-ui/translate.d.ts +38 -0
  30. package/dist/api/ag-ui/translate.js +222 -0
  31. package/dist/api/components.d.ts +7825 -0
  32. package/dist/api/components.js +83 -0
  33. package/dist/api/http/app.d.ts +18 -0
  34. package/dist/api/http/app.js +58 -0
  35. package/dist/api/http/body.d.ts +4 -0
  36. package/dist/api/http/body.js +37 -0
  37. package/dist/api/http/define.d.ts +39 -0
  38. package/dist/api/http/define.js +130 -0
  39. package/dist/api/http/respond.d.ts +9 -0
  40. package/dist/api/http/respond.js +44 -0
  41. package/dist/api/http/routes/access.d.ts +24 -0
  42. package/dist/api/http/routes/access.js +259 -0
  43. package/dist/api/http/routes/actions.d.ts +8 -0
  44. package/dist/api/http/routes/actions.js +76 -0
  45. package/dist/api/http/routes/endpoints.d.ts +7 -0
  46. package/dist/api/http/routes/endpoints.js +100 -0
  47. package/dist/api/http/routes/sessions.d.ts +3 -0
  48. package/dist/api/http/routes/sessions.js +205 -0
  49. package/dist/api/http/routes/tenant.d.ts +10 -0
  50. package/dist/api/http/routes/tenant.js +172 -0
  51. package/dist/api/http/routes/vaults.d.ts +8 -0
  52. package/dist/api/http/routes/vaults.js +167 -0
  53. package/dist/api/openapi.d.ts +19 -0
  54. package/dist/api/openapi.js +138 -0
  55. package/dist/api/route.d.ts +12 -0
  56. package/dist/api/route.js +4 -0
  57. package/dist/core/bearer.d.ts +8 -0
  58. package/dist/core/bearer.js +11 -0
  59. package/dist/core/flow-host.d.ts +7 -15
  60. package/dist/core/flow-host.js +19 -41
  61. package/dist/core/sandbox-routes.d.ts +12 -7
  62. package/dist/core/sandbox-routes.js +24 -25
  63. package/dist/execution/memory.d.ts +8 -5
  64. package/dist/execution/memory.js +33 -13
  65. package/dist/execution/types.d.ts +38 -6
  66. package/dist/execution/types.js +12 -3
  67. package/dist/host/admin-api.d.ts +22 -0
  68. package/dist/host/admin-api.js +178 -0
  69. package/dist/host/app.d.ts +34 -0
  70. package/dist/host/app.js +185 -0
  71. package/dist/host/config-for.d.ts +2 -0
  72. package/dist/host/config-for.js +1 -0
  73. package/dist/host/config.d.ts +11 -0
  74. package/dist/host/cors.d.ts +27 -0
  75. package/dist/host/cors.js +64 -0
  76. package/dist/host/create-host.d.ts +31 -3
  77. package/dist/host/create-host.js +168 -327
  78. package/dist/host/http.d.ts +14 -3
  79. package/dist/host/http.js +64 -23
  80. package/dist/host/main.js +55 -4
  81. package/dist/host/stack-config.d.ts +20 -0
  82. package/dist/host/stack-config.js +55 -0
  83. package/dist/mcp/pool.d.ts +19 -1
  84. package/dist/mcp/pool.js +60 -10
  85. package/dist/model/catalog.d.ts +2 -10
  86. package/dist/model/classify.d.ts +13 -0
  87. package/dist/model/classify.js +61 -0
  88. package/dist/model/models.d.ts +10 -1
  89. package/dist/model/models.js +14 -6
  90. package/dist/model/pi-model.d.ts +17 -2
  91. package/dist/model/pi-model.js +366 -222
  92. package/dist/openapi.json +25696 -0
  93. package/dist/sandbox/manager.d.ts +13 -1
  94. package/dist/sandbox/manager.js +8 -11
  95. package/dist/sandbox/resolve.d.ts +33 -0
  96. package/dist/sandbox/resolve.js +81 -0
  97. package/dist/sandbox/session-sandbox.d.ts +11 -0
  98. package/dist/sandbox/session-sandbox.js +51 -0
  99. package/dist/sandbox/share.d.ts +14 -2
  100. package/dist/sandbox/share.js +33 -3
  101. package/dist/sandbox/tenant-config.d.ts +37 -0
  102. package/dist/sandbox/tenant-config.js +51 -0
  103. package/dist/sandbox/tools.d.ts +7 -9
  104. package/dist/sandbox/tools.js +11 -3
  105. package/dist/sandbox/types.d.ts +8 -2
  106. package/dist/store/event.d.ts +19 -0
  107. package/dist/store/event.js +49 -0
  108. package/dist/store/memory.d.ts +20 -8
  109. package/dist/store/memory.js +265 -100
  110. package/dist/store/postgres/connect.d.ts +13 -0
  111. package/dist/store/postgres/connect.js +5 -0
  112. package/dist/store/postgres/migrations/003_subject_tokens.d.ts +8 -0
  113. package/dist/store/postgres/migrations/003_subject_tokens.js +42 -0
  114. package/dist/store/postgres/migrations/004_publishable_keys.d.ts +6 -0
  115. package/dist/store/postgres/migrations/004_publishable_keys.js +18 -0
  116. package/dist/store/postgres/migrations/005_action_endpoints.d.ts +6 -0
  117. package/dist/store/postgres/migrations/005_action_endpoints.js +32 -0
  118. package/dist/store/postgres/migrations/006_drop_executors.d.ts +11 -0
  119. package/dist/store/postgres/migrations/006_drop_executors.js +32 -0
  120. package/dist/store/postgres/migrations/007_event_time.d.ts +7 -0
  121. package/dist/store/postgres/migrations/007_event_time.js +14 -0
  122. package/dist/store/postgres/migrations/008_durable_streams.d.ts +13 -0
  123. package/dist/store/postgres/migrations/008_durable_streams.js +35 -0
  124. package/dist/store/postgres/migrations/index.d.ts +5 -1
  125. package/dist/store/postgres/migrations/index.js +22 -2
  126. package/dist/store/postgres/migrations/shared/index.d.ts +31 -0
  127. package/dist/store/postgres/migrations/shared/index.js +92 -0
  128. package/dist/store/postgres/record.d.ts +11 -0
  129. package/dist/store/postgres/record.js +64 -0
  130. package/dist/store/postgres/store.js +337 -95
  131. package/dist/store/postgres/tenants.js +14 -0
  132. package/dist/store/types.d.ts +176 -81
  133. package/dist/streams/basin.d.ts +18 -1
  134. package/dist/streams/basin.js +34 -2
  135. package/dist/streams/control.d.ts +18 -0
  136. package/dist/streams/control.js +29 -0
  137. package/dist/streams/memory.d.ts +3 -1
  138. package/dist/streams/memory.js +14 -8
  139. package/dist/streams/relay/core.d.ts +35 -0
  140. package/dist/streams/relay/core.js +366 -0
  141. package/dist/streams/relay/memory.d.ts +69 -0
  142. package/dist/streams/relay/memory.js +176 -0
  143. package/dist/streams/relay/types.d.ts +77 -0
  144. package/dist/streams/relay/types.js +7 -0
  145. package/dist/streams/types.d.ts +66 -36
  146. package/dist/streams/types.js +18 -53
  147. package/dist/tenant/access-policy.d.ts +30 -0
  148. package/dist/tenant/access-policy.js +52 -0
  149. package/dist/tenant/actions.d.ts +10 -149
  150. package/dist/tenant/actions.js +5 -167
  151. package/dist/tenant/advance.js +32 -0
  152. package/dist/tenant/auth.d.ts +19 -17
  153. package/dist/tenant/auth.js +90 -93
  154. package/dist/tenant/browser.d.ts +15 -0
  155. package/dist/tenant/browser.js +39 -0
  156. package/dist/tenant/commands.d.ts +18 -2
  157. package/dist/tenant/commands.js +121 -129
  158. package/dist/tenant/context.d.ts +75 -18
  159. package/dist/tenant/context.js +14 -10
  160. package/dist/tenant/delivery-token.d.ts +41 -0
  161. package/dist/tenant/delivery-token.js +105 -0
  162. package/dist/tenant/delivery.d.ts +62 -0
  163. package/dist/tenant/delivery.js +476 -0
  164. package/dist/tenant/effects.d.ts +5 -0
  165. package/dist/tenant/effects.js +138 -48
  166. package/dist/tenant/endpoints.d.ts +67 -0
  167. package/dist/tenant/endpoints.js +90 -0
  168. package/dist/tenant/ephemeral.d.ts +6 -0
  169. package/dist/tenant/ephemeral.js +60 -36
  170. package/dist/tenant/http.d.ts +8 -9
  171. package/dist/tenant/http.js +13 -27
  172. package/dist/tenant/jwt.d.ts +16 -0
  173. package/dist/tenant/jwt.js +50 -0
  174. package/dist/tenant/module.d.ts +3 -0
  175. package/dist/tenant/module.js +6 -12
  176. package/dist/tenant/outbound.d.ts +44 -0
  177. package/dist/tenant/outbound.js +127 -0
  178. package/dist/tenant/pool.d.ts +0 -2
  179. package/dist/tenant/pool.js +0 -18
  180. package/dist/tenant/reset.d.ts +0 -4
  181. package/dist/tenant/reset.js +0 -5
  182. package/dist/tenant/runtime.d.ts +10 -20
  183. package/dist/tenant/runtime.js +59 -42
  184. package/dist/tenant/scheduler.d.ts +7 -2
  185. package/dist/tenant/scheduler.js +7 -3
  186. package/dist/tenant/session-streams.d.ts +125 -0
  187. package/dist/tenant/session-streams.js +399 -0
  188. package/dist/tenant/sessions.d.ts +28 -4
  189. package/dist/tenant/sessions.js +118 -24
  190. package/dist/tenant/signing-keys.d.ts +55 -0
  191. package/dist/tenant/signing-keys.js +172 -0
  192. package/dist/tenant/slim.d.ts +8 -0
  193. package/dist/tenant/slim.js +21 -0
  194. package/dist/tenant/status.d.ts +0 -3
  195. package/dist/tenant/status.js +19 -12
  196. package/dist/tenant/streams.d.ts +60 -67
  197. package/dist/tenant/streams.js +140 -208
  198. package/dist/tenant/subject-limits.d.ts +16 -0
  199. package/dist/tenant/subject-limits.js +45 -0
  200. package/dist/tenant/sweep.d.ts +1 -10
  201. package/dist/tenant/sweep.js +29 -69
  202. package/dist/tenant/tokens.d.ts +14 -0
  203. package/dist/tenant/tokens.js +178 -0
  204. package/dist/tenant/transcript.d.ts +48 -5
  205. package/dist/tenant/transcript.js +58 -1
  206. package/dist/tenant/types.d.ts +29 -7
  207. package/dist/tenant/worker.d.ts +3 -1
  208. package/dist/tenant/worker.js +9 -3
  209. package/dist/vault/service.d.ts +18 -2
  210. package/dist/vault/service.js +57 -13
  211. package/dist/version.d.ts +1 -1
  212. package/dist/version.js +1 -1
  213. package/package.json +20 -8
  214. package/dist/core/executors.d.ts +0 -44
  215. package/dist/core/executors.js +0 -95
  216. package/dist/streams/relay.d.ts +0 -37
  217. package/dist/streams/relay.js +0 -254
  218. package/dist/tenant/live.d.ts +0 -92
  219. package/dist/tenant/live.js +0 -290
  220. package/dist/tenant/routes-tenant.d.ts +0 -12
  221. package/dist/tenant/routes-tenant.js +0 -137
  222. package/dist/tenant/routes.d.ts +0 -9
  223. package/dist/tenant/routes.js +0 -151
@@ -23,8 +23,6 @@
23
23
  * - `wakeLinkedWorkflow({ t, agentSessionId, turnId, output?, failed?, cancelled?, error?, schedule }): Promise<void>`
24
24
  * - `pendingAgentEffects(t): Promise<FlowEffect[]>`
25
25
  * - `reconcilePendingAgentEffect({ t, effectId, schedule }): Promise<boolean>`
26
- * - `claimedFnVerifyActions(t): Promise<Action[]>`
27
- * - `reofferFnVerifyClaim(t, actionId): Promise<boolean>`
28
26
  * - `planCancelCascade({ t, workflowSessionId, turnId }): Promise<CascadeCancelPlan>`
29
27
  * - `cancelSiblingWork({ t, workflowSessionId, turnId, siblingPaths?, cancelEffectIds? }): Promise<CancelSiblingResult>`
30
28
  * - `fenceWorkflowActions({ t, workflowSessionId, turnId }): Promise<{ cancelled; uncertain }>`
@@ -61,6 +59,9 @@ export function deriveSessionId(workflowSessionId, path, ...parts) {
61
59
  * (path, turnId, iterations) so prior verdicts cannot leak across Loop iterations.
62
60
  */
63
61
  export function deriveAgentEffectSessionId(workflowSessionId, path, request) {
62
+ // A flow agent used as a tool starts fresh on every call, like any agent used as a tool.
63
+ if (request.context?.role === "delegate")
64
+ return deriveSessionId(workflowSessionId, path, "delegate", String(request.context.delegationId ?? request.turnId));
64
65
  if (request.context?.role === "verify-agent") {
65
66
  const iterations = request.iterations ?? "-";
66
67
  const iterParts = iterations !== "-" && iterations.length > 0 ? iterations.split(".") : [];
@@ -88,11 +89,12 @@ export function isFlowEffect(request) {
88
89
  request.kind === "verify" ||
89
90
  isFlowToolEffect(request));
90
91
  }
91
- const OPEN_ACTION = ["pending", "claimed"];
92
+ /** Actions still to be settled: waiting to be delivered, or being delivered. */
93
+ const OPEN_ACTION = ["pending", "delivering"];
92
94
  function scheduleAfterCommit(t, schedule, id, wake) {
93
95
  t.afterCommit(() => schedule(id, wake));
94
96
  }
95
- /** Active agent turns + pending/claimed actions for one workflow turn. */
97
+ /** Active agent turns + pending/delivering actions for one workflow turn. */
96
98
  export async function countActiveFlowWork(t, workflowSessionId, turnId) {
97
99
  const effects = await t.effectsForTurn(workflowSessionId, turnId);
98
100
  const openActions = new Set((await t.actionsForSession(workflowSessionId, {
@@ -251,34 +253,6 @@ export async function reconcilePendingAgentEffect(input) {
251
253
  });
252
254
  return true;
253
255
  }
254
- /** Claimed `fn`/`verify` Actions, for the re-offer after a Tenant opens. */
255
- export async function claimedFnVerifyActions(t) {
256
- return t.actionsWithStatus(["claimed"], { kinds: ["fn", "verify"] });
257
- }
258
- /**
259
- * Re-offer one claimed `fn`/`verify` Action (SD-P7 / WF-C9): they are pure or repeat-safe, so
260
- * a claim left by an executor of an earlier process is offered again at once rather than
261
- * after its lease. The next claim bumps the generation, which fences a late result. Locks
262
- * only the Action's session. Returns true when it re-offered.
263
- */
264
- export async function reofferFnVerifyClaim(t, actionId) {
265
- const found = await t.get("actions", actionId);
266
- if (!found)
267
- return false;
268
- await t.lockSession(found.sessionId);
269
- // Read again under the session lock.
270
- const action = await t.get("actions", actionId);
271
- if (!action ||
272
- action.status !== "claimed" ||
273
- (action.kind !== "fn" && action.kind !== "verify"))
274
- return false;
275
- action.status = "pending";
276
- action.claimId = null;
277
- action.leaseExpiresAt = null;
278
- await t.put("actions", action.actionId, action);
279
- t.signalWork();
280
- return true;
281
- }
282
256
  /** Path depth for deepest-first cancel ordering. */
283
257
  export function pathDepth(path) {
284
258
  if (!path)
@@ -287,14 +261,14 @@ export function pathDepth(path) {
287
261
  }
288
262
  /**
289
263
  * Plan a cancel cascade for a workflow session: linked agents deepest first,
290
- * then pending → cancelled and claimed → uncertain actions (SD-P11 / WF-R53).
264
+ * then pending → cancelled and delivering → uncertain actions (SD-P11 / WF-R53).
291
265
  */
292
266
  export async function planCancelCascade(input) {
293
267
  const { t } = input;
294
268
  const agentEntries = (await t.linkedSessions(input.workflowSessionId)).map((linked) => ({ sessionId: linked.agentSessionId, path: linked.link.path }));
295
269
  agentEntries.sort((a, b) => pathDepth(b.path) - pathDepth(a.path));
296
270
  const pendingActionIds = [];
297
- const claimedActionIds = [];
271
+ const deliveringActionIds = [];
298
272
  const actions = await t.actionsForSession(input.workflowSessionId, {
299
273
  ...(input.turnId !== null ? { turnId: input.turnId } : {}),
300
274
  statuses: OPEN_ACTION,
@@ -302,19 +276,20 @@ export async function planCancelCascade(input) {
302
276
  for (const action of actions) {
303
277
  if (action.status === "pending")
304
278
  pendingActionIds.push(action.actionId);
305
- else if (action.status === "claimed")
306
- claimedActionIds.push(action.actionId);
279
+ // Being delivered: the code may already be running.
280
+ else
281
+ deliveringActionIds.push(action.actionId);
307
282
  }
308
283
  return {
309
284
  agentSessionIds: agentEntries.map((e) => e.sessionId),
310
285
  pendingActionIds,
311
- claimedActionIds,
286
+ deliveringActionIds,
312
287
  };
313
288
  }
314
289
  /**
315
290
  * Cancel sibling work under a Parallel/Map parent when one branch fails (PAR-R6).
316
291
  * Agent turns listed in `agentSessionIds` are returned for the caller to cancel;
317
- * pending actions → cancelled, claimed tool actions → uncertain.
292
+ * pending actions → cancelled, delivering tool actions → uncertain.
318
293
  * When `cancelEffectIds` is provided, those effects are marked cancelled and their
319
294
  * paths are included in the sibling path set.
320
295
  */
@@ -360,7 +335,8 @@ export async function cancelSiblingWork(input) {
360
335
  for (const action of actions) {
361
336
  if (!matchesSibling(action.path))
362
337
  continue;
363
- const next = action.status === "claimed" ? "uncertain" : "cancelled";
338
+ // Delivered work may already have had an external effect.
339
+ const next = action.status === "pending" ? "cancelled" : "uncertain";
364
340
  action.status = next;
365
341
  await t.put("actions", action.actionId, action);
366
342
  const effect = await t.get("effects", action.actionId);
@@ -376,7 +352,8 @@ export async function cancelSiblingWork(input) {
376
352
  return { cancelledActions, uncertainActions, agentSessionIds };
377
353
  }
378
354
  /**
379
- * Apply cancel fencing to workflow-session actions (pending→cancelled, claimed→uncertain).
355
+ * Apply cancel fencing to workflow-session actions (pending→cancelled,
356
+ * delivering→uncertain).
380
357
  */
381
358
  export async function fenceWorkflowActions(input) {
382
359
  const { t } = input;
@@ -389,7 +366,8 @@ export async function fenceWorkflowActions(input) {
389
366
  statuses: OPEN_ACTION,
390
367
  });
391
368
  for (const action of actions) {
392
- const next = action.status === "claimed" ? "uncertain" : "cancelled";
369
+ // Delivered work may already have had an external effect.
370
+ const next = action.status === "pending" ? "cancelled" : "uncertain";
393
371
  action.status = next;
394
372
  await t.put("actions", action.actionId, action);
395
373
  const effect = await t.get("effects", action.actionId);
@@ -27,18 +27,23 @@ export type SandboxRouteDeps = {
27
27
  */
28
28
  export declare function handleSessionSandboxTool(deps: SandboxRouteDeps, sessionId: string, toolName: string, body: unknown, signal: AbortSignal): Promise<SandboxToolOutcome>;
29
29
  /**
30
- * `POST /v1/actions/:id/sandbox/:tool` — executor, authorized by the live claim.
31
- * Body must include claimId and generation matching the claimed action.
30
+ * `POST /v1/actions/:id/sandbox/:tool` — the Action endpoint, authorized by the delivery token
31
+ * of the Action's current delivery (`delivery.generation`). The body is the tool's input.
32
+ */
33
+ export declare function handleActionSandboxTool(deps: SandboxRouteDeps, actionId: string, toolName: string, body: unknown, signal: AbortSignal, delivery: {
34
+ generation: number;
35
+ }): Promise<SandboxToolOutcome>;
36
+ /**
37
+ * Validate PutSession.sandbox attach: same owner, and a sandbox to share. A session whose
38
+ * definition declares a sandbox must declare the owner's spec; one whose definition declares
39
+ * none inherits it (`inherit`).
32
40
  */
33
- export declare function handleActionSandboxTool(deps: SandboxRouteDeps, actionId: string, toolName: string, body: unknown, signal: AbortSignal): Promise<SandboxToolOutcome>;
34
- /** Validate PutSession.sandbox attach: same owner, identical specs. */
35
41
  export declare function validateSandboxAttach(body: {
36
42
  ownerUserId: string;
37
- sandbox?: {
38
- session: string;
39
- };
43
+ sandbox?: unknown;
40
44
  }, newManifest: SessionSandboxRef["manifest"], lookup: (id: string) => SessionSandboxRef | undefined,
41
45
  /** Acting for a subject: another owner's session is the 404 of a missing one. */
42
46
  options?: {
43
47
  opaque?: boolean;
48
+ inherit?: boolean;
44
49
  }): string | undefined;
@@ -3,7 +3,7 @@
3
3
  * Wired from TenantRuntime; keeps route bodies out of the hot runtime file.
4
4
  */
5
5
  import { isSandboxToolName, } from "@nylorun/core/define";
6
- import { capabilityForSandbox, owningSandboxSessionId, sandboxSpecOf, sandboxSpecsEqual, } from "../sandbox/share.js";
6
+ import { capabilityForSandbox, owningSandboxSessionId, sandboxSpecOf, sandboxSpecsEqual, sessionSandboxSpec, } from "../sandbox/share.js";
7
7
  export class SandboxRouteError extends Error {
8
8
  status;
9
9
  constructor(status, message) {
@@ -15,7 +15,7 @@ export class SandboxRouteError extends Error {
15
15
  function resolveSpec(session, lookup) {
16
16
  const ownerId = owningSandboxSessionId(session, lookup);
17
17
  const owner = ownerId === session.id ? session : lookup(ownerId) ?? session;
18
- const spec = sandboxSpecOf(owner.manifest) ?? sandboxSpecOf(session.manifest);
18
+ const spec = sessionSandboxSpec(owner) ?? sessionSandboxSpec(session);
19
19
  if (!spec)
20
20
  throw new SandboxRouteError(404, "Session has no sandbox");
21
21
  return spec;
@@ -31,7 +31,7 @@ function toolInput(body) {
31
31
  if (typeof body !== "object" || Array.isArray(body))
32
32
  throw new SandboxRouteError(400, "Sandbox tool body must be an object");
33
33
  const record = body;
34
- const { claimId: _c, generation: _g, requestId: _r, ...input } = record;
34
+ const { requestId: _r, ...input } = record;
35
35
  return input;
36
36
  }
37
37
  async function runTool(deps, { session, lookup }, tool, input, signal) {
@@ -56,39 +56,36 @@ export async function handleSessionSandboxTool(deps, sessionId, toolName, body,
56
56
  return runTool(deps, read, tool, toolInput(body), signal);
57
57
  }
58
58
  /**
59
- * `POST /v1/actions/:id/sandbox/:tool` — executor, authorized by the live claim.
60
- * Body must include claimId and generation matching the claimed action.
59
+ * `POST /v1/actions/:id/sandbox/:tool` — the Action endpoint, authorized by the delivery token
60
+ * of the Action's current delivery (`delivery.generation`). The body is the tool's input.
61
61
  */
62
- export async function handleActionSandboxTool(deps, actionId, toolName, body, signal) {
62
+ export async function handleActionSandboxTool(deps, actionId, toolName, body, signal, delivery) {
63
63
  if (!body || typeof body !== "object" || Array.isArray(body))
64
64
  throw new SandboxRouteError(400, "Sandbox tool body must be an object");
65
- const record = body;
66
- const claimId = record.claimId;
67
- const generation = record.generation;
68
- if (typeof claimId !== "string" || claimId.length === 0)
69
- throw new SandboxRouteError(400, "claimId is required");
70
- if (typeof generation !== "number" || !Number.isInteger(generation) || generation < 1)
71
- throw new SandboxRouteError(400, "generation must be a positive integer");
72
65
  const action = await deps.getAction(actionId);
73
66
  if (!action)
74
67
  throw new SandboxRouteError(404, "Action not found");
75
- if (action.status !== "claimed" ||
76
- action.claimId !== claimId ||
77
- action.generation !== generation ||
78
- !action.leaseExpiresAt ||
79
- Date.parse(action.leaseExpiresAt) <= Date.now())
80
- throw new SandboxRouteError(409, "Stale or expired claim");
68
+ if (action.status !== "delivering" ||
69
+ action.generation !== delivery.generation ||
70
+ Date.parse(action.deadlineAt ?? "") <= Date.now())
71
+ throw new SandboxRouteError(409, "The delivery was cancelled, lost or delivered again");
81
72
  const read = await deps.session(action.sessionId);
82
73
  if (read.session.activeTurnId !== action.turnId)
83
74
  throw new SandboxRouteError(409, "Action unavailable");
84
- const tool = parseTool(toolName);
85
- return runTool(deps, read, tool, toolInput(body), signal);
75
+ return runTool(deps, read, parseTool(toolName), toolInput(body), signal);
86
76
  }
87
- /** Validate PutSession.sandbox attach: same owner, identical specs. */
77
+ /**
78
+ * Validate PutSession.sandbox attach: same owner, and a sandbox to share. A session whose
79
+ * definition declares a sandbox must declare the owner's spec; one whose definition declares
80
+ * none inherits it (`inherit`).
81
+ */
88
82
  export function validateSandboxAttach(body, newManifest, lookup,
89
83
  /** Acting for a subject: another owner's session is the 404 of a missing one. */
90
84
  options = {}) {
91
- const share = body.sandbox?.session;
85
+ const request = body.sandbox;
86
+ const share = typeof request === "object" && request !== null && "session" in request
87
+ ? String(request.session)
88
+ : undefined;
92
89
  if (!share)
93
90
  return undefined;
94
91
  const owner = lookup(share);
@@ -96,10 +93,12 @@ options = {}) {
96
93
  throw new SandboxRouteError(404, "Sandbox session not found");
97
94
  if (owner.ownerUserId !== body.ownerUserId)
98
95
  throw new SandboxRouteError(403, "Sandbox session must belong to the same owner");
99
- const ownerSpec = sandboxSpecOf(owner.manifest);
100
- const newSpec = sandboxSpecOf(newManifest);
96
+ const ownerSpec = sessionSandboxSpec(owner);
101
97
  if (!ownerSpec)
102
98
  throw new SandboxRouteError(400, "Target session has no sandbox to share");
99
+ if (options.inherit)
100
+ return owningSandboxSessionId(owner, lookup);
101
+ const newSpec = sandboxSpecOf(newManifest);
103
102
  if (!newSpec)
104
103
  throw new SandboxRouteError(400, "Session must declare a sandbox identical to the shared session");
105
104
  if (!sandboxSpecsEqual(ownerSpec, newSpec))
@@ -2,9 +2,9 @@ import { type DurableExecution, type Wake, type WorkerHandlers } from "./types.j
2
2
  export interface MemoryExecutionOptions {
3
3
  /** Delay between sweep passes of one Tenant. Default 5000. */
4
4
  sweepIntervalMs?: number;
5
- /** First retry delay after `advance` throws; doubles per attempt. Default 50. */
5
+ /** First retry delay after `advance` or `deliver` throws; doubles per attempt. Default 50. */
6
6
  retryDelayMs?: number;
7
- /** Attempts before giving up on a throwing `advance` and reporting it. Default 5. */
7
+ /** Attempts before giving up on a throwing handler and reporting it. Default 5. */
8
8
  maxAttempts?: number;
9
9
  /** How long a dedupe key merges repeated wakes. Default 24 hours. */
10
10
  dedupeRetentionMs?: number;
@@ -32,21 +32,24 @@ export declare class MemoryExecution implements DurableExecution {
32
32
  private readonly onError;
33
33
  constructor(options?: MemoryExecutionOptions);
34
34
  wake(tenantId: string, sessionId: string, wake: Wake): Promise<void>;
35
+ deliver(tenantId: string, actionId: string): Promise<void>;
35
36
  timer(tenantId: string, key: string, at: Date): Promise<void>;
36
37
  armSweep(tenantId: string): Promise<void>;
37
38
  disarmSweep(tenantId: string): Promise<void>;
38
39
  start(handlers: WorkerHandlers): Promise<void>;
39
40
  stop(): Promise<void>;
40
41
  /**
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.
42
+ * Resolves once no advance or delivery is running or queued and no busy or
43
+ * retry re-run is pending. Timers and sweeps are not waited for. Test helper.
43
44
  */
44
45
  idle(): Promise<void>;
45
46
  private enqueue;
46
47
  private drain;
47
48
  /** Drops dedupe keys older than the retention window. */
48
49
  private forgetDedupeKeys;
49
- private advanceOnce;
50
+ private runOnce;
51
+ /** Runs the key's handler; resolves to a delay when the key must run again. */
52
+ private call;
50
53
  private armTimer;
51
54
  private scheduleSweep;
52
55
  private later;
@@ -41,7 +41,10 @@ export class MemoryExecution {
41
41
  return;
42
42
  this.seen.set(dedupe, now);
43
43
  }
44
- this.enqueue(tenantId, sessionId);
44
+ this.enqueue("advance", tenantId, sessionId);
45
+ }
46
+ async deliver(tenantId, actionId) {
47
+ this.enqueue("deliver", tenantId, actionId);
45
48
  }
46
49
  async timer(tenantId, key, at) {
47
50
  const id = `${tenantId}\u0000${key}`;
@@ -96,8 +99,8 @@ export class MemoryExecution {
96
99
  await Promise.allSettled(this.inflight);
97
100
  }
98
101
  /**
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.
102
+ * Resolves once no advance or delivery is running or queued and no busy or
103
+ * retry re-run is pending. Timers and sweeps are not waited for. Test helper.
101
104
  */
102
105
  async idle() {
103
106
  while (this.delayed.size > 0 ||
@@ -105,13 +108,14 @@ export class MemoryExecution {
105
108
  [...this.keys.values()].some((s) => s.running || (s.queued && this.handlers)))
106
109
  await new Promise((resolve) => setTimeout(resolve, 1));
107
110
  }
108
- enqueue(tenantId, sessionId) {
109
- const key = sessionKey(tenantId, sessionId);
111
+ enqueue(kind, tenantId, id) {
112
+ const key = stateKey(kind, tenantId, id);
110
113
  let state = this.keys.get(key);
111
114
  if (!state)
112
115
  this.keys.set(key, (state = {
116
+ kind,
113
117
  tenantId,
114
- sessionId,
118
+ id,
115
119
  running: false,
116
120
  queued: false,
117
121
  attempts: 0,
@@ -126,14 +130,14 @@ export class MemoryExecution {
126
130
  try {
127
131
  while (state.queued && this.handlers) {
128
132
  state.queued = false;
129
- await this.advanceOnce(state, this.handlers);
133
+ await this.runOnce(state, this.handlers);
130
134
  }
131
135
  }
132
136
  finally {
133
137
  state.running = false;
134
138
  // Idle keys hold nothing (a retry keeps its attempt count); forget them so
135
139
  // long-lived processes stay small.
136
- const key = sessionKey(state.tenantId, state.sessionId);
140
+ const key = stateKey(state.kind, state.tenantId, state.id);
137
141
  if (!state.queued && state.attempts === 0 && this.keys.get(key) === state)
138
142
  this.keys.delete(key);
139
143
  }
@@ -147,14 +151,15 @@ export class MemoryExecution {
147
151
  this.seen.delete(dedupe);
148
152
  }
149
153
  }
150
- async advanceOnce(state, handlers) {
154
+ async runOnce(state, handlers) {
151
155
  const controller = new AbortController();
152
156
  state.controller = controller;
157
+ const again = () => this.enqueue(state.kind, state.tenantId, state.id);
153
158
  try {
154
- const result = await handlers.advance(state.tenantId, state.sessionId, controller.signal);
159
+ const retryAfterMs = await this.call(state, handlers, controller.signal);
155
160
  state.attempts = 0;
156
- if (result.status === "busy")
157
- this.later(result.retryAfterMs, () => this.enqueue(state.tenantId, state.sessionId));
161
+ if (retryAfterMs !== undefined)
162
+ this.later(retryAfterMs, again);
158
163
  }
159
164
  catch (error) {
160
165
  state.attempts += 1;
@@ -163,12 +168,23 @@ export class MemoryExecution {
163
168
  this.onError(error);
164
169
  }
165
170
  else
166
- this.later(this.retryDelayMs * 2 ** (state.attempts - 1), () => this.enqueue(state.tenantId, state.sessionId));
171
+ this.later(this.retryDelayMs * 2 ** (state.attempts - 1), again);
167
172
  }
168
173
  finally {
169
174
  state.controller = undefined;
170
175
  }
171
176
  }
177
+ /** Runs the key's handler; resolves to a delay when the key must run again. */
178
+ async call(state, handlers, signal) {
179
+ if (state.kind === "advance") {
180
+ const result = await handlers.advance(state.tenantId, state.id, signal);
181
+ return result.status === "busy" ? result.retryAfterMs : undefined;
182
+ }
183
+ if (!handlers.deliver)
184
+ throw new Error("WorkerHandlers.deliver is required for deliveries");
185
+ const result = await handlers.deliver(state.tenantId, state.id, signal);
186
+ return result.status === "retry" ? result.retryAfterMs : undefined;
187
+ }
172
188
  armTimer(id, timer) {
173
189
  timer.handle = setTimeout(() => {
174
190
  if (this.timers.get(id) !== timer)
@@ -215,3 +231,7 @@ export class MemoryExecution {
215
231
  void promise.finally(() => this.inflight.delete(promise));
216
232
  }
217
233
  }
234
+ /** Advances and deliveries are queued apart: a session id and an Action id may be equal. */
235
+ function stateKey(kind, tenantId, id) {
236
+ return `${kind}\u0000${sessionKey(tenantId, id)}`;
237
+ }
@@ -27,21 +27,31 @@
27
27
  * becomes `uncertain` is settled in the Session Store and `advance` returns
28
28
  * `done`.
29
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.
30
+ * armed Tenant is a no-op. The sweep settles lapsed deliveries and re-wakes
31
+ * orphaned `runnable` sessions.
32
+ * - **One delivery per Action at a time.** `deliver` runs
33
+ * `WorkerHandlers.deliver` for the key `<tenantId>:<actionId>` at least once
34
+ * after it resolves. Two deliveries for one key never overlap; repeated
35
+ * `deliver` calls may each run it, so the handler reads the Action's state
36
+ * and does nothing when there is nothing to deliver.
37
+ * - **Retry after `retry`.** A delivery that returns `retry` is run again for
38
+ * the same key after `retryAfterMs`. This is how an Action endpoint that is
39
+ * down or busy is retried; the handler throws only on infrastructure errors.
32
40
  */
33
41
  /** Why a session is woken (§12.3, "Where wakes come from"). */
34
42
  export type WakeReason =
35
43
  /** Session commands. */
36
44
  "message" | "approve" | "respond"
37
- /** An executor posted an Action result. */
45
+ /** An Action's outcome was recorded (a delivery's answer or a background result). */
38
46
  | "action_result"
39
47
  /** A linked agent turn completed, failed or was cancelled. */
40
48
  | "linked"
41
49
  /** Queued flow effects, pending agent effects reconciled. */
42
50
  | "flow"
43
51
  /** The Tenant sweep and `busy` re-wakes. */
44
- | "recover";
52
+ | "recover"
53
+ /** A long turn ended a segment at a step boundary and continues in the next (Model Calls §10). */
54
+ | "rollover";
45
55
  export declare const WAKE_REASONS: readonly WakeReason[];
46
56
  export interface Wake {
47
57
  reason: WakeReason;
@@ -59,6 +69,14 @@ export type AdvanceResult = {
59
69
  status: "busy";
60
70
  retryAfterMs: number;
61
71
  };
72
+ export type DeliverResult = {
73
+ status: "done";
74
+ }
75
+ /** The Action is still to be delivered; run again for this key after `retryAfterMs`. */
76
+ | {
77
+ status: "retry";
78
+ retryAfterMs: number;
79
+ };
62
80
  /** What a Worker runs when Durable Session Execution calls it. */
63
81
  export interface WorkerHandlers {
64
82
  /**
@@ -71,6 +89,11 @@ export interface WorkerHandlers {
71
89
  sweep(tenantId: string): Promise<void>;
72
90
  /** A durable timer set with `timer` fired. Required when `timer` is used. */
73
91
  fire?(tenantId: string, key: string): Promise<void>;
92
+ /**
93
+ * Delivers one Action to its Action endpoint. `signal` aborts when the Worker
94
+ * stops. Throw only on infrastructure errors. Required when `deliver` is used.
95
+ */
96
+ deliver?(tenantId: string, actionId: string, signal: AbortSignal): Promise<DeliverResult>;
74
97
  }
75
98
  export interface DurableExecution {
76
99
  /** At-least-once. `dedupeKey` merges repeated wakes for the same cause. */
@@ -81,6 +104,12 @@ export interface DurableExecution {
81
104
  * implementation can; `fire` must be idempotent either way.
82
105
  */
83
106
  timer(tenantId: string, key: string, at: Date): Promise<void>;
107
+ /**
108
+ * At-least-once: `WorkerHandlers.deliver(tenantId, actionId)` runs after this
109
+ * resolves, never overlapping another delivery of the same Action. Send it
110
+ * after the transaction that made the Action pending commits.
111
+ */
112
+ deliver(tenantId: string, actionId: string): Promise<void>;
84
113
  /** Arms the Tenant's self-re-arming sweep. Idempotent. */
85
114
  armSweep(tenantId: string): Promise<void>;
86
115
  /** Stops re-arming the Tenant's sweep (Tenant deleted). A pass already running finishes. */
@@ -109,14 +138,17 @@ export interface StuckInvocation {
109
138
  /** Service name without any prefix, e.g. `NylorunSession`. */
110
139
  service: string;
111
140
  handler: string;
112
- /** Object key: `<tenantId>:<sessionId>`, `<tenantId>` or `<tenantId>:<timer key>`. */
141
+ /**
142
+ * Object key: `<tenantId>:<sessionId>`, `<tenantId>`, `<tenantId>:<timer key>` or
143
+ * `<tenantId>:<actionId>`.
144
+ */
113
145
  key: string;
114
146
  tenantId?: string;
115
147
  retryCount: number;
116
148
  lastFailure?: string;
117
149
  modifiedAt?: string;
118
150
  }
119
- /** The key one advance at a time is serialized on. */
151
+ /** The key one advance (or one delivery, with an Action id) at a time is serialized on. */
120
152
  export declare function sessionKey(tenantId: string, sessionId: string): string;
121
153
  /** Splits a key made by `sessionKey`. Tenant ids never contain `:`. */
122
154
  export declare function parseSessionKey(key: string): {
@@ -27,8 +27,16 @@
27
27
  * becomes `uncertain` is settled in the Session Store and `advance` returns
28
28
  * `done`.
29
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.
30
+ * armed Tenant is a no-op. The sweep settles lapsed deliveries and re-wakes
31
+ * orphaned `runnable` sessions.
32
+ * - **One delivery per Action at a time.** `deliver` runs
33
+ * `WorkerHandlers.deliver` for the key `<tenantId>:<actionId>` at least once
34
+ * after it resolves. Two deliveries for one key never overlap; repeated
35
+ * `deliver` calls may each run it, so the handler reads the Action's state
36
+ * and does nothing when there is nothing to deliver.
37
+ * - **Retry after `retry`.** A delivery that returns `retry` is run again for
38
+ * the same key after `retryAfterMs`. This is how an Action endpoint that is
39
+ * down or busy is retried; the handler throws only on infrastructure errors.
32
40
  */
33
41
  export const WAKE_REASONS = [
34
42
  "message",
@@ -38,8 +46,9 @@ export const WAKE_REASONS = [
38
46
  "linked",
39
47
  "flow",
40
48
  "recover",
49
+ "rollover",
41
50
  ];
42
- /** The key one advance at a time is serialized on. */
51
+ /** The key one advance (or one delivery, with an Action id) at a time is serialized on. */
43
52
  export function sessionKey(tenantId, sessionId) {
44
53
  return `${tenantId}:${sessionId}`;
45
54
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The Admin API (`/v1/admin/**`): Tenants and the Host's status, for a caller with the admin
3
+ * key. The Host checks the listener, the protocol and the admin key first (`app.ts`); what
4
+ * gets here is authorized. Routes are declared once, for serving and for the OpenAPI document.
5
+ */
6
+ import { OpenAPIHono } from "@hono/zod-openapi";
7
+ import { type AdminStatus } from "@nylorun/core/contracts";
8
+ import { type NodeBindings, type TenantModule } from "../tenant/types.js";
9
+ export interface AdminApiOptions {
10
+ module: TenantModule;
11
+ /** `GET /v1/admin/status`. */
12
+ status(): Promise<AdminStatus>;
13
+ /** Stops the Host once the shutdown answer is sent. */
14
+ shutdown(): void;
15
+ /** `GET /v1/admin/openapi.json`: this API's OpenAPI document (`api/openapi.ts`). */
16
+ document?(): unknown;
17
+ }
18
+ type AdminEnv = {
19
+ Bindings: NodeBindings;
20
+ };
21
+ export declare function createAdminApi(options: AdminApiOptions): OpenAPIHono<AdminEnv>;
22
+ export {};