@nylorun/runtime 0.21.0-beta → 0.21.1-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.
- package/CHANGELOG.md +22 -0
- package/README.md +4 -2
- package/dist/adapters/execution/restate.js +26 -1
- package/dist/core/flow-host.d.ts +2 -3
- package/dist/core/flow-host.js +10 -7
- package/dist/execution/types.d.ts +13 -5
- package/dist/execution/types.js +13 -5
- package/dist/host/create-host.d.ts +1 -1
- package/dist/host/create-host.js +2 -1
- package/dist/host/http.d.ts +0 -1
- package/dist/host/http.js +0 -6
- package/dist/host/stack-config.d.ts +2 -0
- package/dist/host/stack-config.js +42 -73
- package/dist/management-openapi.json +1 -1
- package/dist/openapi.json +1 -1
- package/dist/store/postgres/drizzle/0017_wake_outbox.sql +9 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +7 -0
- package/dist/store/postgres/schema.d.ts +103 -0
- package/dist/store/postgres/schema.js +16 -0
- package/dist/store/postgres/store.js +54 -4
- package/dist/store/types.d.ts +46 -5
- package/dist/tenant/advance.js +1 -4
- package/dist/tenant/commands.js +5 -5
- package/dist/tenant/context.d.ts +10 -7
- package/dist/tenant/runtime.js +2 -1
- package/dist/tenant/sweep.d.ts +14 -0
- package/dist/tenant/sweep.js +69 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.1-beta
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 070674d: **One source of truth for the manifest and Harness API schemas.**
|
|
8
|
+
|
|
9
|
+
- `@nylorun/core`: `Agent.from` (`agentFrom`) checks a manifest with `AgentManifestSchema`, the Runtime's own check, instead of a validator of its own, and refuses with `agent.build-failed` and the schema's messages (each prefixed with its path, such as `capabilities.0.tools.0.name: …`). It now refuses what the Runtime refuses at registration and it let through, such as unknown fields (it dropped them) or a tool or MCP server name that not every model provider accepts. The schema names a top-level `model` or `schemaVersion` and a capability's `model` as before, and the skill whose `files` are wrong (`Skill 'triage' files must include SKILL.md`). The rebuilt manifest is a frozen copy; the object passed in is no longer frozen. `AgentManifestSchema` and `WorkflowManifestSchema` are no longer force-cast to their types: the build checks that each accepts exactly `AgentManifest` and `WorkflowManifest`. `@nylorun/core/contracts` exports `McpServerManifestSchema`, `HEADER_NAME_PATTERN` and `LOOPBACK_HOSTS`. The Harness API's types (`@nylorun/core/harness-api`) are inferred from the schemas that validate its frames, so they match them: `TurnStart.manifest`, `TurnStart.checkpoint` and `RunRouting.rootManifest` are `object`, `WorkspaceCall.tool` is a sandbox tool name, `ABORT_REASONS` is a tuple, and properties are no longer `readonly` (arrays that were stay so).
|
|
10
|
+
- `@nylorun/harness`: `runDurable` checks the manifest once, when it rebuilds the agent, instead of twice. The unpublished, stale `schemas/manifest.schema.json` (manifest `schemaVersion` 2) is gone; `z.toJSONSchema(AgentManifestSchema)` is the manifest's JSON Schema.
|
|
11
|
+
- `@nylorun/agents`: a plugin's MCP servers are checked with core's `McpServerManifestSchema`, header-name pattern and loopback hosts. A server whose key is not a name every model provider accepts is skipped with a `plugin.mcp-server-skipped` warning, since the Runtime would refuse the manifest declaring it.
|
|
12
|
+
- `@nylorun/runtime`: the stack configuration reads its four listeners (`NYLORUN_LISTEN_*`, `NYLORUN_GATES_LISTEN_*`, `NYLORUN_HARNESS_LISTEN_*`, `NYLORUN_EGRESS_LISTEN_*`) with one parser; the variables and their errors are unchanged.
|
|
13
|
+
|
|
14
|
+
- 34d1a81: A commit can no longer lose its wake. A transaction that asks for an advance writes the wake to the Session Store's wake outbox (`Tx.wake`, migration `0017_wake_outbox` adds `nylorun.wakes`) and sends it to Durable Session Execution after commit, with an idempotency key (the wake's dedupe key, or one made from its row), deleting the row once Restate accepted it. A send that fails, or that a crash between the commit and the send cut off, is sent again by the Tenant sweep under the same key: on a process's first pass for any row, then for rows older than 2 s. Restate deduplicates the repeat, so a wake whose answer was lost runs one advance. The sweep's orphan scan no longer delivers lost wakes: it runs on a process's first pass and then once a minute, as the backstop for what the execution itself lost (Restate state wiped, an invocation killed, an in-process execution's queue). Restate owns when a session runs (wakes, one advance per session, retries, timers); the Session Store owns what happened (checkpoint, effect journal, events, the lease's epoch and the outbox), as `src/CONTEXT.md` and `adapters/execution/restate.ts` now describe. A Runtime of the previous beta refuses the migrated database (`schema-too-new`); while processes of both run, the old ones send wakes as before.
|
|
15
|
+
- Pin core to the tested release.
|
|
16
|
+
- Pin harness to the tested release.
|
|
17
|
+
- Updated dependencies [417f336]
|
|
18
|
+
- Updated dependencies [6fecd4c]
|
|
19
|
+
- Updated dependencies [7780b2f]
|
|
20
|
+
- Updated dependencies [070674d]
|
|
21
|
+
- Updated dependencies
|
|
22
|
+
- @nylorun/core@0.18.0-beta
|
|
23
|
+
- @nylorun/harness@0.25.0-beta
|
|
24
|
+
|
|
3
25
|
## 0.21.0-beta
|
|
4
26
|
|
|
5
27
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -42,8 +42,10 @@ whose key the admin key derives (the only derived key). Management keys come fro
|
|
|
42
42
|
rotates and deletes the Tenant's application keys (`PUT /v1/tenant/keys/{keyId}`); a rotated
|
|
43
43
|
or deleted key stops authenticating at once. A database written by a Runtime that kept several Tenants in one
|
|
44
44
|
database (`tenant_<id>` schemas), or by a pre-release build of one Tenant per database
|
|
45
|
-
(`schema_version` tables), is refused: this release starts fresh on a new database. Restate runs one advance of a session at a time
|
|
46
|
-
|
|
45
|
+
(`schema_version` tables), is refused: this release starts fresh on a new database. Restate runs one advance of a session at a time, delivers its wakes and holds
|
|
46
|
+
its timers and the Tenant's sweep (Durable Session Execution), and journals no effect:
|
|
47
|
+
checkpoints, effects, events and the wake outbox are the Session Store's, and a wake is
|
|
48
|
+
written to the outbox in the transaction that causes it, so a commit never loses it; every session's events are relayed from the record to
|
|
47
49
|
its own S2 stream, which history and SSE read (Durable Streams). One image runs every **service**,
|
|
48
50
|
and `--service` picks what a process runs: `core` serves the Runtime API, the
|
|
49
51
|
Management API and SSE and runs the stream relay, `loop` runs advances (the Worker), and
|
|
@@ -25,7 +25,32 @@
|
|
|
25
25
|
* What a run answers arms its timers and its retry.
|
|
26
26
|
*
|
|
27
27
|
* Wakes, sandbox reconciles, timers and sweep arming go through the ingress as one-way
|
|
28
|
-
* sends, so an API node can call them without ever calling `start`.
|
|
28
|
+
* sends, so an API node can call them without ever calling `start`. A wake a commit asked for
|
|
29
|
+
* comes from the Session Store's wake outbox (`Tx.wake`) with an idempotency key, so a send
|
|
30
|
+
* repeated after its answer was lost causes no second advance.
|
|
31
|
+
*
|
|
32
|
+
* ## What Restate owns, and what the Session Store owns
|
|
33
|
+
*
|
|
34
|
+
* Restate owns when a session runs: it delivers each wake at least once, deduped by its
|
|
35
|
+
* key for `dedupeRetentionMs`; runs at most one invocation per key at a time; retries an
|
|
36
|
+
* invocation whose attempt failed, so an advance whose Worker died runs again and takes the
|
|
37
|
+
* session over (§11.4); and keeps every timer: the busy re-wake, `NylorunTimer`,
|
|
38
|
+
* `NylorunSandbox` and the `NylorunTenant` sweep chain. The Session Store (Postgres) owns
|
|
39
|
+
* what happened: the checkpoint, the effect journal (`harness-api/record.ts`), the events
|
|
40
|
+
* (the Record), and the wake outbox that hands each committed wake to Restate. Restate
|
|
41
|
+
* journals no effect: an effect's intent and outcome commit with the session's events in
|
|
42
|
+
* one transaction, under the advance's lease, and remote harnesses and pod engines write
|
|
43
|
+
* them too, over the Harness API.
|
|
44
|
+
*
|
|
45
|
+
* Two Session Store mechanisms overlap Restate on purpose:
|
|
46
|
+
*
|
|
47
|
+
* - **The lease and its epoch** (§10.6). Restate's one invocation per key holds for an
|
|
48
|
+
* attempt, not for the code it started: an attempt that ends (abort timeout, a dropped
|
|
49
|
+
* connection) is retried while the previous attempt's handler may still run on its Worker,
|
|
50
|
+
* and a remote harness or pod engine may still hold the run. The epoch fences their writes,
|
|
51
|
+
* and `busy` answers the overlap.
|
|
52
|
+
* - **The advance deadline** (`tenant/worker.ts`). Restate's abort timeout does not stop a
|
|
53
|
+
* running handler, so the Worker bounds each advance itself, below `timeouts`.
|
|
29
54
|
*/
|
|
30
55
|
import { createServer } from "node:http2";
|
|
31
56
|
import * as restate from "@restatedev/restate-sdk";
|
package/dist/core/flow-host.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { EffectOutcome } from "@nylorun/core/contracts";
|
|
2
2
|
import { type JsonValue, type WorkflowManifest } from "@nylorun/core/define";
|
|
3
3
|
import type { HostEffect } from "@nylorun/harness/run";
|
|
4
|
-
import type {
|
|
5
|
-
import type { EffectDoc, Tx } from "../store/types.js";
|
|
4
|
+
import type { EffectDoc, Tx, WakeDelivery } from "../store/types.js";
|
|
6
5
|
import { type FlowLimits } from "./limits.js";
|
|
7
6
|
/** Deterministic agent session id: derive(workflowSessionId, path, …parts). */
|
|
8
7
|
export declare function deriveSessionId(workflowSessionId: string, path: string, ...parts: string[]): string;
|
|
@@ -99,7 +98,7 @@ export type CascadeCancelPlan = {
|
|
|
99
98
|
readonly agentSessionIds: string[];
|
|
100
99
|
};
|
|
101
100
|
/** Wakes a session after commit (`DurableExecution.wake` through the Tenant context). */
|
|
102
|
-
type Schedule =
|
|
101
|
+
type Schedule = WakeDelivery;
|
|
103
102
|
/** Effect documents as the flow host writes them. */
|
|
104
103
|
export type FlowEffect = EffectDoc & {
|
|
105
104
|
agentSessionId?: string;
|
package/dist/core/flow-host.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Every function that touches state takes the caller's transaction `t: Tx` and
|
|
5
5
|
* is async. Nothing here publishes, notifies or schedules directly (seam rule
|
|
6
6
|
* 1): events go through `t.event(...)`, which the store publishes after
|
|
7
|
-
* commit, and wakes go through `t.
|
|
7
|
+
* commit, and wakes go through `t.wake(id, wake, schedule)`, the wake outbox, which
|
|
8
|
+
* calls `schedule` after commit. Functions
|
|
8
9
|
* that rewrite a session-scoped document lock that session first with
|
|
9
10
|
* `t.lockSession` (a no-op when the caller already holds it).
|
|
10
11
|
*
|
|
@@ -35,8 +36,9 @@
|
|
|
35
36
|
* - `foreignInteractionConflict({ t, workflowSessionId, interactionId }): Promise<{ status: 409; message; ownerSessionId } | undefined>`
|
|
36
37
|
* - `wakeForQueuedEffects({ t, workflowSessionId, turnId, limits, schedule }): Promise<boolean>`
|
|
37
38
|
*
|
|
38
|
-
* `schedule
|
|
39
|
-
* `wake` carries the reason (`linked`, `flow`) and, where the cause has one, a dedupe key.
|
|
39
|
+
* `schedule` (a `WakeDelivery`, the Tenant's `ctx.wake`) runs after commit, never inside `t`;
|
|
40
|
+
* `wake` carries the reason (`linked`, `flow`) and, where the cause has one, a dedupe key. A
|
|
41
|
+
* wake `schedule` does not deliver stays in the outbox for the Tenant sweep (`Tx.wake`).
|
|
40
42
|
*
|
|
41
43
|
* An `agent` effect settles only from the linked turn it started. A linked agent session is
|
|
42
44
|
* reused by every iteration of a Loop (its id derives from the workflow and the path), so its
|
|
@@ -138,8 +140,9 @@ export async function recordVerdict(t, request, value) {
|
|
|
138
140
|
...(verdict.data !== undefined ? { data: verdict.data } : {}),
|
|
139
141
|
});
|
|
140
142
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
/** Writes the wake to the outbox; `schedule` delivers it after commit. */
|
|
144
|
+
function requestWake(t, schedule, id, wake) {
|
|
145
|
+
return t.wake(id, wake, schedule);
|
|
143
146
|
}
|
|
144
147
|
/** Active agent turns + tool nodes in flight for one workflow turn. */
|
|
145
148
|
export async function countActiveFlowWork(t, workflowSessionId, turnId) {
|
|
@@ -242,7 +245,7 @@ export async function wakeLinkedWorkflow(input) {
|
|
|
242
245
|
await settleAgentEffect({ t, effect, outcome });
|
|
243
246
|
workflow.status = "runnable";
|
|
244
247
|
await t.put("sessions", workflow.id, workflow);
|
|
245
|
-
|
|
248
|
+
await requestWake(t, input.schedule, workflow.id, {
|
|
246
249
|
reason: "linked",
|
|
247
250
|
dedupeKey: `linked:${link.turnId}:${link.effectId}`,
|
|
248
251
|
});
|
|
@@ -471,7 +474,7 @@ export async function wakeForQueuedEffects(input) {
|
|
|
471
474
|
workflow.status = "runnable";
|
|
472
475
|
await t.put("sessions", workflow.id, workflow);
|
|
473
476
|
}
|
|
474
|
-
|
|
477
|
+
await requestWake(t, input.schedule, input.workflowSessionId, {
|
|
475
478
|
reason: "flow",
|
|
476
479
|
});
|
|
477
480
|
return true;
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Durable Session Execution decides when to look at a session again. It never
|
|
5
5
|
* holds session state: every outcome lives in the Session Store, and `advance`
|
|
6
|
-
* is safe to repeat (§10.1).
|
|
6
|
+
* is safe to repeat (§10.1). It owns the schedule (wake delivery, one advance per
|
|
7
|
+
* key, retries, timers, the sweep chain); the Session Store owns the record (the
|
|
8
|
+
* checkpoint, the effect journal, the events) and the wake outbox that bridges the
|
|
9
|
+
* two. Restate is the supported implementation
|
|
7
10
|
* (`adapters/execution/restate.ts`); `execution/memory.ts` is the in-process
|
|
8
11
|
* implementation used by unit tests, `startEphemeralRuntime` and a Host
|
|
9
12
|
* started without Restate endpoints.
|
|
@@ -15,11 +18,14 @@
|
|
|
15
18
|
* Different keys may run concurrently.
|
|
16
19
|
* - **At least once after a wake.** When `wake` resolves, an `advance` for the
|
|
17
20
|
* 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.
|
|
21
|
+
* are sent after the Session Store transaction that caused them commits, never
|
|
22
|
+
* inside it: the transaction writes the wake to its outbox (`Tx.wake`), and the
|
|
23
|
+
* wake is sent after commit, or by the Tenant sweep when that send failed or a
|
|
24
|
+
* crash cut it off. A wake may therefore be sent more than once, always with the
|
|
25
|
+
* same `dedupeKey`.
|
|
20
26
|
* - **Dedupe.** Wakes carrying the same `dedupeKey` for the same key cause at
|
|
21
27
|
* most one advance for that cause (within the implementation's retention
|
|
22
|
-
* window, at least 24 hours for Restate).
|
|
28
|
+
* window, at least 24 hours for Restate). Every wake from the outbox has one.
|
|
23
29
|
* - **Busy re-wake.** An advance that returns `busy` is re-run for the same key
|
|
24
30
|
* after `retryAfterMs`.
|
|
25
31
|
* - **Retries on infrastructure errors.** A handler that throws is retried with
|
|
@@ -27,7 +33,9 @@
|
|
|
27
33
|
* becomes `uncertain` is settled in the Session Store and `advance` returns
|
|
28
34
|
* `done`.
|
|
29
35
|
* - **Sweeps.** `armSweep` arms one self-re-arming sweep per Tenant; arming an
|
|
30
|
-
* armed Tenant is a no-op. The sweep
|
|
36
|
+
* armed Tenant is a no-op. The sweep sends the outbox's undelivered wakes and,
|
|
37
|
+
* as a backstop for what an implementation lost, re-wakes orphaned sessions
|
|
38
|
+
* (`tenant/sweep.ts`).
|
|
31
39
|
* - **One reconcile per sandbox at a time** (F7.2). `sandbox` runs
|
|
32
40
|
* `WorkerHandlers.sandbox` for the key `<tenantId>:<sandboxId>` at least once
|
|
33
41
|
* after a `reconcile` resolves, never overlapping another run for the key. A
|
package/dist/execution/types.js
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Durable Session Execution decides when to look at a session again. It never
|
|
5
5
|
* holds session state: every outcome lives in the Session Store, and `advance`
|
|
6
|
-
* is safe to repeat (§10.1).
|
|
6
|
+
* is safe to repeat (§10.1). It owns the schedule (wake delivery, one advance per
|
|
7
|
+
* key, retries, timers, the sweep chain); the Session Store owns the record (the
|
|
8
|
+
* checkpoint, the effect journal, the events) and the wake outbox that bridges the
|
|
9
|
+
* two. Restate is the supported implementation
|
|
7
10
|
* (`adapters/execution/restate.ts`); `execution/memory.ts` is the in-process
|
|
8
11
|
* implementation used by unit tests, `startEphemeralRuntime` and a Host
|
|
9
12
|
* started without Restate endpoints.
|
|
@@ -15,11 +18,14 @@
|
|
|
15
18
|
* Different keys may run concurrently.
|
|
16
19
|
* - **At least once after a wake.** When `wake` resolves, an `advance` for the
|
|
17
20
|
* 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.
|
|
21
|
+
* are sent after the Session Store transaction that caused them commits, never
|
|
22
|
+
* inside it: the transaction writes the wake to its outbox (`Tx.wake`), and the
|
|
23
|
+
* wake is sent after commit, or by the Tenant sweep when that send failed or a
|
|
24
|
+
* crash cut it off. A wake may therefore be sent more than once, always with the
|
|
25
|
+
* same `dedupeKey`.
|
|
20
26
|
* - **Dedupe.** Wakes carrying the same `dedupeKey` for the same key cause at
|
|
21
27
|
* most one advance for that cause (within the implementation's retention
|
|
22
|
-
* window, at least 24 hours for Restate).
|
|
28
|
+
* window, at least 24 hours for Restate). Every wake from the outbox has one.
|
|
23
29
|
* - **Busy re-wake.** An advance that returns `busy` is re-run for the same key
|
|
24
30
|
* after `retryAfterMs`.
|
|
25
31
|
* - **Retries on infrastructure errors.** A handler that throws is retried with
|
|
@@ -27,7 +33,9 @@
|
|
|
27
33
|
* becomes `uncertain` is settled in the Session Store and `advance` returns
|
|
28
34
|
* `done`.
|
|
29
35
|
* - **Sweeps.** `armSweep` arms one self-re-arming sweep per Tenant; arming an
|
|
30
|
-
* armed Tenant is a no-op. The sweep
|
|
36
|
+
* armed Tenant is a no-op. The sweep sends the outbox's undelivered wakes and,
|
|
37
|
+
* as a backstop for what an implementation lost, re-wakes orphaned sessions
|
|
38
|
+
* (`tenant/sweep.ts`).
|
|
31
39
|
* - **One reconcile per sandbox at a time** (F7.2). `sandbox` runs
|
|
32
40
|
* `WorkerHandlers.sandbox` for the key `<tenantId>:<sandboxId>` at least once
|
|
33
41
|
* after a `reconcile` resolves, never overlapping another run for the key. A
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ResourceServerConfig } from "../tenant/resource-server.js";
|
|
2
2
|
import type { Logger, TenantModule } from "../tenant/types.js";
|
|
3
3
|
import type { HostConfigFile } from "./config.js";
|
|
4
|
-
import type
|
|
4
|
+
import { type ContainerListen } from "./stack-config.js";
|
|
5
5
|
export interface CreateHostOptions {
|
|
6
6
|
hostRoot: string;
|
|
7
7
|
module: TenantModule;
|
package/dist/host/create-host.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createServer, } from "node:http";
|
|
2
2
|
import { getRequestListener, RequestError } from "@hono/node-server";
|
|
3
3
|
import { createHostApp } from "./app.js";
|
|
4
|
-
import {
|
|
4
|
+
import { isLoopbackHost } from "./stack-config.js";
|
|
5
|
+
import { bindListener as bind, EXIT_NON_LOOPBACK, headerValue, HostListenError, isAllowedRequestHost, pathnameIsLogged, redactRoutePath, rejectedResponse, sendRejected, } from "./http.js";
|
|
5
6
|
export function createHost(options) {
|
|
6
7
|
const { module, config, logger, coreVersion } = options;
|
|
7
8
|
const pid = options.pid ?? process.pid;
|
package/dist/host/http.d.ts
CHANGED
|
@@ -54,4 +54,3 @@ export declare function adminKeyMatches(presented: string | undefined, adminKey:
|
|
|
54
54
|
export declare function headerValue(incoming: IncomingMessage, name: string): string | undefined;
|
|
55
55
|
/** A JSON request body, capped at `limit` bytes (413); `undefined` when there is none. */
|
|
56
56
|
export declare function readJsonBody(request: Request, limit?: number): Promise<unknown>;
|
|
57
|
-
export declare function isLoopbackHost(host: string): boolean;
|
package/dist/host/http.js
CHANGED
|
@@ -191,9 +191,3 @@ export async function readJsonBody(request, limit = 1024 * 1024) {
|
|
|
191
191
|
throw Object.assign(new Error("Invalid JSON"), { status: 400 });
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
|
-
export function isLoopbackHost(host) {
|
|
195
|
-
const normalized = host.trim().toLowerCase();
|
|
196
|
-
return (normalized === "127.0.0.1" ||
|
|
197
|
-
normalized === "::1" ||
|
|
198
|
-
normalized === "localhost");
|
|
199
|
-
}
|
|
@@ -237,6 +237,8 @@ export declare function parseServices(argv: readonly string[]): ServiceSelection
|
|
|
237
237
|
* IPv6 literals must be bracketed (`[::1]:4000`).
|
|
238
238
|
*/
|
|
239
239
|
export declare function normalizeAllowedHost(name: string, entry: string): string;
|
|
240
|
+
/** A loopback bind address: `127.0.0.1`, `::1` or `localhost`. */
|
|
241
|
+
export declare function isLoopbackHost(host: string): boolean;
|
|
240
242
|
/** Parse the stack configuration; throws `StackConfigError` naming the variable. */
|
|
241
243
|
export declare function parseStackConfig(env: EnvSnapshot, argv: readonly string[]): StackConfig;
|
|
242
244
|
/** Endpoint summary for logs: which endpoints are set, never their values. */
|
|
@@ -179,9 +179,10 @@ export function normalizeAllowedHost(name, entry) {
|
|
|
179
179
|
function loopbackForms(port) {
|
|
180
180
|
return [`localhost:${port}`, `127.0.0.1:${port}`, `[::1]:${port}`];
|
|
181
181
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
/** A loopback bind address: `127.0.0.1`, `::1` or `localhost`. */
|
|
183
|
+
export function isLoopbackHost(host) {
|
|
184
|
+
const normalized = host.trim().toLowerCase();
|
|
185
|
+
return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
|
|
185
186
|
}
|
|
186
187
|
function parseUrl(env, name, protocols) {
|
|
187
188
|
const raw = read(env, name);
|
|
@@ -198,32 +199,37 @@ function parseUrl(env, name, protocols) {
|
|
|
198
199
|
throw new StackConfigError(`${name} must use ${protocols.map((p) => p.replace(/:$/, "")).join(" or ")}; got ${url.protocol.replace(/:$/, "")}`);
|
|
199
200
|
return raw;
|
|
200
201
|
}
|
|
201
|
-
function
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
const rawAllowed = read(env, "NYLORUN_ALLOWED_HOSTS");
|
|
205
|
-
if (rawHost === undefined && rawPort === undefined && rawAllowed === undefined)
|
|
206
|
-
return undefined;
|
|
207
|
-
const host = rawHost ?? DEFAULT_CONTAINER_LISTEN_HOST;
|
|
202
|
+
function parseListener(env, prefix, defaultPort, hosts) {
|
|
203
|
+
const hostName = `${prefix}LISTEN_HOST`;
|
|
204
|
+
const host = read(env, hostName) ?? DEFAULT_CONTAINER_LISTEN_HOST;
|
|
208
205
|
if (/\s|\//.test(host))
|
|
209
|
-
throw new StackConfigError(
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
206
|
+
throw new StackConfigError(`${hostName} is not an address: ${host}`);
|
|
207
|
+
const portName = `${prefix}LISTEN_PORT`;
|
|
208
|
+
const rawPort = read(env, portName);
|
|
209
|
+
const port = rawPort === undefined ? defaultPort : parsePort(portName, rawPort);
|
|
210
|
+
if (!hosts)
|
|
211
|
+
return { host, port };
|
|
212
|
+
const allowedName = `${prefix}ALLOWED_HOSTS`;
|
|
213
|
+
const explicit = (read(env, allowedName) ?? "")
|
|
214
|
+
.split(",")
|
|
215
|
+
.map((entry) => entry.trim())
|
|
216
|
+
.filter((entry) => entry !== "")
|
|
217
|
+
.map((entry) => normalizeAllowedHost(allowedName, entry));
|
|
218
|
+
if (explicit.length === 0 && !isLoopbackHost(host))
|
|
219
|
+
throw new StackConfigError(`${allowedName} is required when ${hostName} is ${host}: list the Host headers ${hosts.senders}, e.g. ${hosts.example(port)}`);
|
|
222
220
|
// Loopback forms of the listen port serve probes from inside the container
|
|
223
221
|
// (health checks). A browser on the Docker host never sends them: it sends
|
|
224
222
|
// the published port.
|
|
225
|
-
|
|
226
|
-
|
|
223
|
+
return { host, port, allowedHosts: [...new Set([...explicit, ...loopbackForms(port)])] };
|
|
224
|
+
}
|
|
225
|
+
/** The API's listener (`NYLORUN_LISTEN_*`, `NYLORUN_ALLOWED_HOSTS`): none when none is set. */
|
|
226
|
+
function parseListen(env) {
|
|
227
|
+
if (["NYLORUN_LISTEN_HOST", "NYLORUN_LISTEN_PORT", "NYLORUN_ALLOWED_HOSTS"].every((name) => read(env, name) === undefined))
|
|
228
|
+
return undefined;
|
|
229
|
+
return parseListener(env, "NYLORUN_", DEFAULT_CONTAINER_LISTEN_PORT, {
|
|
230
|
+
senders: "clients send",
|
|
231
|
+
example: (port) => `runtime:${port},localhost:<published port>`,
|
|
232
|
+
});
|
|
227
233
|
}
|
|
228
234
|
const IDENTITY_KEY = /^publickeyv1_[1-9A-HJ-NP-Za-km-z]{32,64}$/;
|
|
229
235
|
function parseIdentityKeys(env) {
|
|
@@ -398,30 +404,14 @@ function parseSandboxes(env) {
|
|
|
398
404
|
* (required with `NYLORUN_HARNESS=remote`; optional when only sandbox pods connect).
|
|
399
405
|
*/
|
|
400
406
|
function parseHarnessListener(env, tokenRequired = true) {
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const port = rawPort === undefined
|
|
406
|
-
? DEFAULT_HARNESS_LISTEN_PORT
|
|
407
|
-
: parsePort("NYLORUN_HARNESS_LISTEN_PORT", rawPort);
|
|
408
|
-
const rawAllowed = read(env, "NYLORUN_HARNESS_ALLOWED_HOSTS");
|
|
409
|
-
const explicit = rawAllowed === undefined
|
|
410
|
-
? []
|
|
411
|
-
: rawAllowed
|
|
412
|
-
.split(",")
|
|
413
|
-
.map((entry) => entry.trim())
|
|
414
|
-
.filter((entry) => entry !== "")
|
|
415
|
-
.map((entry) => normalizeAllowedHost("NYLORUN_HARNESS_ALLOWED_HOSTS", entry));
|
|
416
|
-
if (explicit.length === 0 && !isLoopbackAddress(host))
|
|
417
|
-
throw new StackConfigError(`NYLORUN_HARNESS_ALLOWED_HOSTS is required when NYLORUN_HARNESS_LISTEN_HOST is ${host}: list the Host headers harnesses send, e.g. runtime:${port}`);
|
|
407
|
+
const listen = parseListener(env, "NYLORUN_HARNESS_", DEFAULT_HARNESS_LISTEN_PORT, {
|
|
408
|
+
senders: "harnesses send",
|
|
409
|
+
example: (port) => `runtime:${port}`,
|
|
410
|
+
});
|
|
418
411
|
const token = tokenRequired || read(env, "NYLORUN_HARNESS_TOKEN") !== undefined
|
|
419
412
|
? parseHarnessToken(env, "with NYLORUN_HARNESS=remote: the credential harnesses present")
|
|
420
413
|
: undefined;
|
|
421
|
-
return {
|
|
422
|
-
listen: { host, port, allowedHosts: [...new Set([...explicit, ...loopbackForms(port)])] },
|
|
423
|
-
...(token ? { token } : {}),
|
|
424
|
-
};
|
|
414
|
+
return { listen, ...(token ? { token } : {}) };
|
|
425
415
|
}
|
|
426
416
|
/** What a harness process must not hold: it runs next to agent code. */
|
|
427
417
|
const HARNESS_REFUSED = ["NYLORUN_DATABASE_URL", "NYLORUN_GATES_TOKEN", "NYLORUN_KEYS_URL"];
|
|
@@ -491,43 +481,22 @@ function parsePodHost(env, url) {
|
|
|
491
481
|
}
|
|
492
482
|
/** `StackConfig.gates` from `NYLORUN_GATES_*`. */
|
|
493
483
|
function parseGates(env) {
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
const port = rawPort === undefined
|
|
499
|
-
? DEFAULT_GATES_LISTEN_PORT
|
|
500
|
-
: parsePort("NYLORUN_GATES_LISTEN_PORT", rawPort);
|
|
501
|
-
const rawAllowed = read(env, "NYLORUN_GATES_ALLOWED_HOSTS");
|
|
502
|
-
const explicit = rawAllowed === undefined
|
|
503
|
-
? []
|
|
504
|
-
: rawAllowed
|
|
505
|
-
.split(",")
|
|
506
|
-
.map((entry) => entry.trim())
|
|
507
|
-
.filter((entry) => entry !== "")
|
|
508
|
-
.map((entry) => normalizeAllowedHost("NYLORUN_GATES_ALLOWED_HOSTS", entry));
|
|
509
|
-
if (explicit.length === 0 && !isLoopbackAddress(host))
|
|
510
|
-
throw new StackConfigError(`NYLORUN_GATES_ALLOWED_HOSTS is required when NYLORUN_GATES_LISTEN_HOST is ${host}: list the Host headers the loop sends, e.g. gateway:${port}`);
|
|
484
|
+
const listen = parseListener(env, "NYLORUN_GATES_", DEFAULT_GATES_LISTEN_PORT, {
|
|
485
|
+
senders: "the loop sends",
|
|
486
|
+
example: (port) => `gateway:${port}`,
|
|
487
|
+
});
|
|
511
488
|
const token = read(env, "NYLORUN_GATES_TOKEN");
|
|
512
489
|
if (token === undefined)
|
|
513
490
|
throw new StackConfigError("NYLORUN_GATES_TOKEN is required for --service gates: the token the loop presents (`nylorun start` sets it)");
|
|
514
491
|
if (!GATES_TOKEN.test(token))
|
|
515
492
|
throw new StackConfigError("NYLORUN_GATES_TOKEN must be at least 32 bytes as hex");
|
|
516
|
-
return {
|
|
517
|
-
listen: { host, port, allowedHosts: [...new Set([...explicit, ...loopbackForms(port)])] },
|
|
518
|
-
token,
|
|
519
|
-
};
|
|
493
|
+
return { listen, token };
|
|
520
494
|
}
|
|
521
495
|
/** The prefix of the credential resolver's variables (removed in protocol 10). */
|
|
522
496
|
const REMOVED_RESOLVER_PREFIX = "NYLORUN_RESOLVER_";
|
|
523
497
|
/** `StackConfig.egress` from `NYLORUN_EGRESS_LISTEN_*`. CONNECT has no Host header to check. */
|
|
524
498
|
function parseEgress(env) {
|
|
525
|
-
|
|
526
|
-
if (/\s|\//.test(host))
|
|
527
|
-
throw new StackConfigError(`NYLORUN_EGRESS_LISTEN_HOST is not an address: ${host}`);
|
|
528
|
-
const rawPort = read(env, "NYLORUN_EGRESS_LISTEN_PORT");
|
|
529
|
-
const port = rawPort === undefined ? DEFAULT_EGRESS_LISTEN_PORT : parsePort("NYLORUN_EGRESS_LISTEN_PORT", rawPort);
|
|
530
|
-
return { listen: { host, port } };
|
|
499
|
+
return { listen: parseListener(env, "NYLORUN_EGRESS_", DEFAULT_EGRESS_LISTEN_PORT) };
|
|
531
500
|
}
|
|
532
501
|
/** `StackConfig.modelGate` from `NYLORUN_GATES_URL` and `NYLORUN_GATES_TOKEN`. */
|
|
533
502
|
function parseModelGate(env) {
|
package/dist/openapi.json
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
CREATE TABLE "nylorun"."wakes" (
|
|
2
|
+
"id" text COLLATE "C" PRIMARY KEY NOT NULL,
|
|
3
|
+
"session_id" text COLLATE "C" NOT NULL,
|
|
4
|
+
"reason" text NOT NULL,
|
|
5
|
+
"dedupe_key" text,
|
|
6
|
+
"created_at" timestamp with time zone DEFAULT clock_timestamp() NOT NULL
|
|
7
|
+
);
|
|
8
|
+
--> statement-breakpoint
|
|
9
|
+
CREATE INDEX "wakes_created_at" ON "nylorun"."wakes" USING btree ("created_at");
|
|
@@ -3340,6 +3340,109 @@ export declare const controlSignals: import("drizzle-orm/pg-core").PgTableWithCo
|
|
|
3340
3340
|
};
|
|
3341
3341
|
dialect: 'pg';
|
|
3342
3342
|
}>;
|
|
3343
|
+
/**
|
|
3344
|
+
* The wake outbox (architecture §12.3): one row per wake a committed transaction asked for
|
|
3345
|
+
* (`Tx.wake`), written in that transaction, so a commit never loses its wake. The row is
|
|
3346
|
+
* deleted once Durable Session Execution has accepted the wake; until then the Tenant sweep
|
|
3347
|
+
* delivers it again, with the same idempotency key (`dedupe_key`, or `wake:<id>` without
|
|
3348
|
+
* one). `created_at` is the insert's own clock (`clock_timestamp()`).
|
|
3349
|
+
*/
|
|
3350
|
+
export declare const wakes: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
3351
|
+
name: "wakes";
|
|
3352
|
+
schema: "nylorun";
|
|
3353
|
+
columns: {
|
|
3354
|
+
id: import("drizzle-orm/pg-core").PgColumn<{
|
|
3355
|
+
name: "id";
|
|
3356
|
+
tableName: "wakes";
|
|
3357
|
+
dataType: "custom";
|
|
3358
|
+
columnType: "PgCustomColumn";
|
|
3359
|
+
data: string;
|
|
3360
|
+
driverParam: string;
|
|
3361
|
+
notNull: true;
|
|
3362
|
+
hasDefault: false;
|
|
3363
|
+
isPrimaryKey: true;
|
|
3364
|
+
isAutoincrement: false;
|
|
3365
|
+
hasRuntimeDefault: false;
|
|
3366
|
+
enumValues: undefined;
|
|
3367
|
+
baseColumn: never;
|
|
3368
|
+
identity: undefined;
|
|
3369
|
+
generated: undefined;
|
|
3370
|
+
}, {}, {
|
|
3371
|
+
pgColumnBuilderBrand: 'PgCustomColumnBuilderBrand';
|
|
3372
|
+
}>;
|
|
3373
|
+
sessionId: import("drizzle-orm/pg-core").PgColumn<{
|
|
3374
|
+
name: "sessionId";
|
|
3375
|
+
tableName: "wakes";
|
|
3376
|
+
dataType: "custom";
|
|
3377
|
+
columnType: "PgCustomColumn";
|
|
3378
|
+
data: string;
|
|
3379
|
+
driverParam: string;
|
|
3380
|
+
notNull: true;
|
|
3381
|
+
hasDefault: false;
|
|
3382
|
+
isPrimaryKey: false;
|
|
3383
|
+
isAutoincrement: false;
|
|
3384
|
+
hasRuntimeDefault: false;
|
|
3385
|
+
enumValues: undefined;
|
|
3386
|
+
baseColumn: never;
|
|
3387
|
+
identity: undefined;
|
|
3388
|
+
generated: undefined;
|
|
3389
|
+
}, {}, {
|
|
3390
|
+
pgColumnBuilderBrand: 'PgCustomColumnBuilderBrand';
|
|
3391
|
+
}>;
|
|
3392
|
+
reason: import("drizzle-orm/pg-core").PgColumn<{
|
|
3393
|
+
name: "reason";
|
|
3394
|
+
tableName: "wakes";
|
|
3395
|
+
dataType: "string";
|
|
3396
|
+
columnType: "PgText";
|
|
3397
|
+
data: string;
|
|
3398
|
+
driverParam: string;
|
|
3399
|
+
notNull: true;
|
|
3400
|
+
hasDefault: false;
|
|
3401
|
+
isPrimaryKey: false;
|
|
3402
|
+
isAutoincrement: false;
|
|
3403
|
+
hasRuntimeDefault: false;
|
|
3404
|
+
enumValues: [string, ...string[]];
|
|
3405
|
+
baseColumn: never;
|
|
3406
|
+
identity: undefined;
|
|
3407
|
+
generated: undefined;
|
|
3408
|
+
}, {}, {}>;
|
|
3409
|
+
dedupeKey: import("drizzle-orm/pg-core").PgColumn<{
|
|
3410
|
+
name: "dedupeKey";
|
|
3411
|
+
tableName: "wakes";
|
|
3412
|
+
dataType: "string";
|
|
3413
|
+
columnType: "PgText";
|
|
3414
|
+
data: string;
|
|
3415
|
+
driverParam: string;
|
|
3416
|
+
notNull: false;
|
|
3417
|
+
hasDefault: false;
|
|
3418
|
+
isPrimaryKey: false;
|
|
3419
|
+
isAutoincrement: false;
|
|
3420
|
+
hasRuntimeDefault: false;
|
|
3421
|
+
enumValues: [string, ...string[]];
|
|
3422
|
+
baseColumn: never;
|
|
3423
|
+
identity: undefined;
|
|
3424
|
+
generated: undefined;
|
|
3425
|
+
}, {}, {}>;
|
|
3426
|
+
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
3427
|
+
name: "createdAt";
|
|
3428
|
+
tableName: "wakes";
|
|
3429
|
+
dataType: "date";
|
|
3430
|
+
columnType: "PgTimestamp";
|
|
3431
|
+
data: Date;
|
|
3432
|
+
driverParam: string;
|
|
3433
|
+
notNull: true;
|
|
3434
|
+
hasDefault: true;
|
|
3435
|
+
isPrimaryKey: false;
|
|
3436
|
+
isAutoincrement: false;
|
|
3437
|
+
hasRuntimeDefault: false;
|
|
3438
|
+
enumValues: undefined;
|
|
3439
|
+
baseColumn: never;
|
|
3440
|
+
identity: undefined;
|
|
3441
|
+
generated: undefined;
|
|
3442
|
+
}, {}, {}>;
|
|
3443
|
+
};
|
|
3444
|
+
dialect: 'pg';
|
|
3445
|
+
}>;
|
|
3343
3446
|
/**
|
|
3344
3447
|
* Each sandbox's lifecycle stream (`nylorun.sandbox-event/1`), numbered from 0 per sandbox id.
|
|
3345
3448
|
* Written only by the record module; not published to the stream relay.
|
|
@@ -421,6 +421,22 @@ export const controlSignals = nylorun.table("control_signals", {
|
|
|
421
421
|
index("control_signals_created_at").on(t.createdAt),
|
|
422
422
|
check("control_signals_kind_check", sql `${t.kind} IN ('session.cancel', 'sessions.reset')`),
|
|
423
423
|
]);
|
|
424
|
+
/**
|
|
425
|
+
* The wake outbox (architecture §12.3): one row per wake a committed transaction asked for
|
|
426
|
+
* (`Tx.wake`), written in that transaction, so a commit never loses its wake. The row is
|
|
427
|
+
* deleted once Durable Session Execution has accepted the wake; until then the Tenant sweep
|
|
428
|
+
* delivers it again, with the same idempotency key (`dedupe_key`, or `wake:<id>` without
|
|
429
|
+
* one). `created_at` is the insert's own clock (`clock_timestamp()`).
|
|
430
|
+
*/
|
|
431
|
+
export const wakes = nylorun.table("wakes", {
|
|
432
|
+
id: textC().primaryKey(),
|
|
433
|
+
sessionId: textC().notNull(),
|
|
434
|
+
reason: text().notNull(),
|
|
435
|
+
dedupeKey: text(),
|
|
436
|
+
createdAt: timestamp({ withTimezone: true, mode: "date" })
|
|
437
|
+
.notNull()
|
|
438
|
+
.default(sql `clock_timestamp()`),
|
|
439
|
+
}, (t) => [index("wakes_created_at").on(t.createdAt)]);
|
|
424
440
|
// ---------------------------------------------------------------------------
|
|
425
441
|
// nylorun_streams: the record
|
|
426
442
|
/**
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
* `timestamptz`.
|
|
50
50
|
*/
|
|
51
51
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
52
|
+
import { randomUUID } from "node:crypto";
|
|
52
53
|
import { and, count, eq, getTableColumns, gte, inArray, isNotNull, isNull, lt, lte, ne, or, sql, } from "drizzle-orm";
|
|
53
54
|
import { appendEvent, appendSandboxEvent } from "../../record/index.js";
|
|
54
55
|
import { OwnershipLostError, PrincipalRoleConflict } from "../ownership.js";
|
|
@@ -57,7 +58,7 @@ import { database, driverError } from "./db.js";
|
|
|
57
58
|
import { expectedSchemaVersion, readSchemaVersion } from "./migrate.js";
|
|
58
59
|
import { createPostgresRecordReader } from "./record.js";
|
|
59
60
|
import { postgresRecordWriter, postgresSandboxRecordWriter } from "./record-writer.js";
|
|
60
|
-
import { artifacts, artifactVersions, artifactContent, definitionFiles, definitionFileUses, commands, definitions, effects, links, modelBudgets, modelUsage, principals, sandboxes, sandboxEvents, sandboxResources, sessionEvents, sessionLogHeads, sessions, toJson, toolCrossings, signingKeys, tenant, tenantSettings, vaultAudit, vaultCredentials, vaultIdempotency, vaults, } from "./schema.js";
|
|
61
|
+
import { artifacts, artifactVersions, artifactContent, definitionFiles, definitionFileUses, commands, definitions, effects, links, modelBudgets, modelUsage, principals, sandboxes, sandboxEvents, sandboxResources, sessionEvents, sessionLogHeads, sessions, toJson, toolCrossings, signingKeys, tenant, tenantSettings, vaultAudit, vaultCredentials, vaultIdempotency, vaults, wakes, } from "./schema.js";
|
|
61
62
|
export function createPostgresSessionStore(options) {
|
|
62
63
|
return new PostgresSessionStore(options);
|
|
63
64
|
}
|
|
@@ -110,7 +111,7 @@ class PostgresSessionStore {
|
|
|
110
111
|
let t;
|
|
111
112
|
// Wrapped so `begin` does not treat an array result as queries to await.
|
|
112
113
|
const run = this.db.transaction(async (db) => {
|
|
113
|
-
t = new PostgresTx(db, this.tenantId, this.now);
|
|
114
|
+
t = new PostgresTx(db, this.tenantId, this.now, this.deleteWake);
|
|
114
115
|
try {
|
|
115
116
|
return { value: await this.active.run(this, () => fn(t)) };
|
|
116
117
|
}
|
|
@@ -153,6 +154,10 @@ class PostgresSessionStore {
|
|
|
153
154
|
}
|
|
154
155
|
return result.value;
|
|
155
156
|
}
|
|
157
|
+
/** Deletes a delivered outbox request, outside any transaction (`Tx.wake`). */
|
|
158
|
+
deleteWake = async (id) => {
|
|
159
|
+
await this.db.delete(wakes).where(eq(wakes.id, id));
|
|
160
|
+
};
|
|
156
161
|
onCommit(listener) {
|
|
157
162
|
this.listeners.add(listener);
|
|
158
163
|
return () => this.listeners.delete(listener);
|
|
@@ -256,21 +261,27 @@ const SIGNING_KEY_STAMP = {
|
|
|
256
261
|
};
|
|
257
262
|
/** The audit columns a row has (`ord` only orders them). */
|
|
258
263
|
const { ord: _ord, ...AUDIT } = getTableColumns(vaultAudit);
|
|
259
|
-
const SESSION_TABLES = [sessions, commands, effects, links];
|
|
264
|
+
const SESSION_TABLES = [sessions, commands, effects, links, wakes];
|
|
265
|
+
/** An outbox row as it is delivered: the wake's own dedupe key, or one made from the row id. */
|
|
266
|
+
function outboxWake(id, reason, dedupeKey) {
|
|
267
|
+
return { reason, dedupeKey: dedupeKey ?? `wake:${id}` };
|
|
268
|
+
}
|
|
260
269
|
/** The inserted row's `column` (`ON CONFLICT … DO UPDATE`). */
|
|
261
270
|
const excluded = (column) => sql.raw(`excluded.${column}`);
|
|
262
271
|
class PostgresTx {
|
|
263
272
|
db;
|
|
264
273
|
tenantId;
|
|
265
274
|
now;
|
|
275
|
+
deleteWake;
|
|
266
276
|
closed = false;
|
|
267
277
|
events = [];
|
|
268
278
|
generations = [];
|
|
269
279
|
callbacks = [];
|
|
270
|
-
constructor(db, tenantId, now) {
|
|
280
|
+
constructor(db, tenantId, now, deleteWake) {
|
|
271
281
|
this.db = db;
|
|
272
282
|
this.tenantId = tenantId;
|
|
273
283
|
this.now = now;
|
|
284
|
+
this.deleteWake = deleteWake;
|
|
274
285
|
}
|
|
275
286
|
check() {
|
|
276
287
|
if (this.closed)
|
|
@@ -345,6 +356,45 @@ class PostgresTx {
|
|
|
345
356
|
this.check();
|
|
346
357
|
this.callbacks.push(fn);
|
|
347
358
|
}
|
|
359
|
+
// --- wake outbox ---------------------------------------------------------
|
|
360
|
+
async wake(sessionId, wake, deliver) {
|
|
361
|
+
this.check();
|
|
362
|
+
const id = randomUUID();
|
|
363
|
+
await this.db.insert(wakes).values({
|
|
364
|
+
id,
|
|
365
|
+
sessionId,
|
|
366
|
+
reason: wake.reason,
|
|
367
|
+
dedupeKey: wake.dedupeKey ?? null,
|
|
368
|
+
});
|
|
369
|
+
const delivered = outboxWake(id, wake.reason, wake.dedupeKey ?? null);
|
|
370
|
+
// A rejection goes to the store's error hook (as every callback's), and the row stays.
|
|
371
|
+
this.callbacks.push(async () => {
|
|
372
|
+
if ((await deliver(sessionId, delivered)) === false)
|
|
373
|
+
return;
|
|
374
|
+
await this.deleteWake(id);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
async pendingWakes(before, limit) {
|
|
378
|
+
this.check();
|
|
379
|
+
const rows = await this.db
|
|
380
|
+
.select()
|
|
381
|
+
.from(wakes)
|
|
382
|
+
.where(lt(wakes.createdAt, before))
|
|
383
|
+
.orderBy(wakes.createdAt, wakes.id)
|
|
384
|
+
.limit(limit);
|
|
385
|
+
return rows.map((row) => ({
|
|
386
|
+
id: row.id,
|
|
387
|
+
sessionId: row.sessionId,
|
|
388
|
+
wake: outboxWake(row.id, row.reason, row.dedupeKey),
|
|
389
|
+
createdAt: row.createdAt.toISOString(),
|
|
390
|
+
}));
|
|
391
|
+
}
|
|
392
|
+
async deleteWakes(ids) {
|
|
393
|
+
this.check();
|
|
394
|
+
if (ids.length === 0)
|
|
395
|
+
return;
|
|
396
|
+
await this.db.delete(wakes).where(inArray(wakes.id, [...ids]));
|
|
397
|
+
}
|
|
348
398
|
// --- ownership -----------------------------------------------------------
|
|
349
399
|
async takeOwnership(sessionId, claim) {
|
|
350
400
|
this.check();
|
package/dist/store/types.d.ts
CHANGED
|
@@ -25,16 +25,18 @@
|
|
|
25
25
|
* generation, `streams/basin.ts`), and the cursor is
|
|
26
26
|
* `base64url("<sessionId>:<seq>")` (see `record/cursor.ts`).
|
|
27
27
|
* 4. **No external I/O inside `fn`.** No model, tool, MCP, sandbox, Restate or
|
|
28
|
-
* S2 call, and no `fetch`, runs inside a transaction.
|
|
29
|
-
*
|
|
30
|
-
* commit (seam rule 1 and 2).
|
|
28
|
+
* S2 call, and no `fetch`, runs inside a transaction. A wake is written in the
|
|
29
|
+
* transaction (`Tx.wake`, the wake outbox) and delivered after commit, and events
|
|
30
|
+
* are delivered to commit listeners after commit (seam rule 1 and 2).
|
|
31
31
|
* 5. **No nested transactions.** Calling `store.tx` from inside `fn` rejects.
|
|
32
32
|
* A `Tx` must not be used after its `tx` call settles.
|
|
33
33
|
* 6. **Post-commit order.** After a commit, the store first calls every commit
|
|
34
34
|
* listener once with the transaction's events (in allocation order), then runs the `afterCommit` callbacks in registration
|
|
35
35
|
* order and awaits them. Listener and callback failures are reported to the
|
|
36
36
|
* store's error hook; they never reject `tx`, because the commit stands.
|
|
37
|
-
* Recovery from a lost post-commit step is the Tenant sweep's job
|
|
37
|
+
* Recovery from a lost post-commit step is the Tenant sweep's job: a wake's
|
|
38
|
+
* delivery is a callback too, and its outbox row is what the sweep delivers
|
|
39
|
+
* again (`Tx.wake`).
|
|
38
40
|
* 7. **Documents are values.** `get` and queries return fresh copies; mutating
|
|
39
41
|
* them changes nothing until `put`.
|
|
40
42
|
* 8. **Ownership columns are store-managed.** `owner`, `epoch` and
|
|
@@ -56,6 +58,7 @@
|
|
|
56
58
|
*/
|
|
57
59
|
import type { KeyRole } from "@nylorun/core/compatibility";
|
|
58
60
|
import type { RecordReader } from "../streams/relay/types.js";
|
|
61
|
+
import type { Wake } from "../execution/types.js";
|
|
59
62
|
import type { EventPayload, EventType, LiveEvent, SandboxEvent, SandboxEventPayload, SandboxEventType, SandboxKind, SandboxPodDesired, SandboxPodObserved, SessionEventOf } from "@nylorun/core/contracts";
|
|
60
63
|
import type { SandboxManifest } from "@nylorun/core/define";
|
|
61
64
|
import type { HostEffect } from "@nylorun/harness/run";
|
|
@@ -271,6 +274,24 @@ export interface SessionStore {
|
|
|
271
274
|
followSignals(onSignal: (signal: ControlSignal) => void, options?: FollowSignalsOptions): Promise<SignalFollower>;
|
|
272
275
|
close(): Promise<void>;
|
|
273
276
|
}
|
|
277
|
+
/** A wake as the outbox delivers it: its `dedupeKey` is the request's idempotency key. */
|
|
278
|
+
export type OutboxWake = Wake & {
|
|
279
|
+
dedupeKey: string;
|
|
280
|
+
};
|
|
281
|
+
/**
|
|
282
|
+
* Hands an outbox request to Durable Session Execution (`Tx.wake`). Resolving means the
|
|
283
|
+
* execution accepted it; `false` means it was not delivered and stays in the outbox, as a
|
|
284
|
+
* rejection does, without an error.
|
|
285
|
+
*/
|
|
286
|
+
export type WakeDelivery = (sessionId: string, wake: OutboxWake) => boolean | void | Promise<boolean | void>;
|
|
287
|
+
/** A wake outbox request not yet delivered (`Tx.pendingWakes`). */
|
|
288
|
+
export interface PendingWake {
|
|
289
|
+
id: string;
|
|
290
|
+
sessionId: string;
|
|
291
|
+
wake: OutboxWake;
|
|
292
|
+
/** ISO time it was written. */
|
|
293
|
+
createdAt: string;
|
|
294
|
+
}
|
|
274
295
|
/**
|
|
275
296
|
* A signal on the control bus (D21, D48): one process tells the others with the Tenant open.
|
|
276
297
|
* Signals are not events: they are never recorded in a session's log or streamed, and a
|
|
@@ -345,8 +366,28 @@ export interface Tx {
|
|
|
345
366
|
* cursor is final.
|
|
346
367
|
*/
|
|
347
368
|
event<T extends EventType>(sessionId: string, turnId: string | null, type: T, payload: EventPayload<T>): Promise<SessionEventOf<T>>;
|
|
348
|
-
/**
|
|
369
|
+
/**
|
|
370
|
+
* Runs `fn` after a successful commit (never on rollback): a sandbox signal or a host
|
|
371
|
+
* revocation. Nothing writes it down, so a crash after commit loses it. A wake goes
|
|
372
|
+
* through `wake`, which does.
|
|
373
|
+
*/
|
|
349
374
|
afterCommit(fn: () => void | Promise<void>): void;
|
|
375
|
+
/**
|
|
376
|
+
* Asks for an advance of `sessionId` once this transaction commits. The request is a row
|
|
377
|
+
* of the wake outbox written in this transaction, so it commits, or rolls back, with what
|
|
378
|
+
* caused it: a commit never loses its wake. After commit, in `afterCommit` order, the
|
|
379
|
+
* store calls `deliver` (`DurableExecution.wake`, through the Tenant) and deletes the row
|
|
380
|
+
* once it resolves to anything but `false`. A request `deliver` declines (`false`) or
|
|
381
|
+
* rejects, or one a crash cut off, stays in the outbox until the Tenant sweep delivers it
|
|
382
|
+
* (`pendingWakes`, `deleteWakes`). `deliver` gets the request's idempotency key as
|
|
383
|
+
* `dedupeKey`: the wake's own, or `wake:<row id>` without one, so a request delivered
|
|
384
|
+
* twice still causes one advance.
|
|
385
|
+
*/
|
|
386
|
+
wake(sessionId: string, wake: Wake, deliver: WakeDelivery): Promise<void>;
|
|
387
|
+
/** Outbox requests written before `before`, oldest first. */
|
|
388
|
+
pendingWakes(before: Date, limit: number): Promise<PendingWake[]>;
|
|
389
|
+
/** Deletes delivered outbox requests. */
|
|
390
|
+
deleteWakes(ids: readonly string[]): Promise<void>;
|
|
350
391
|
/**
|
|
351
392
|
* Locks the session and takes ownership when it has no owner or the owner's
|
|
352
393
|
* lease ended at or before `now`; then increments `epoch` and sets the lease
|
package/dist/tenant/advance.js
CHANGED
|
@@ -473,10 +473,7 @@ async function settle(ctx, lease, s, result, updates = [], cursor) {
|
|
|
473
473
|
await slimModelEffects(t, id, s.activeTurnId);
|
|
474
474
|
await t.put("sessions", id, current);
|
|
475
475
|
const segment = finished.segment + 1;
|
|
476
|
-
t.
|
|
477
|
-
reason: "rollover",
|
|
478
|
-
dedupeKey: `rollover:${s.activeTurnId}:${segment}`,
|
|
479
|
-
}));
|
|
476
|
+
await t.wake(id, { reason: "rollover", dedupeKey: `rollover:${s.activeTurnId}:${segment}` }, ctx.wake);
|
|
480
477
|
return siblings;
|
|
481
478
|
}
|
|
482
479
|
const flow = isWorkflowManifest(current.manifest);
|
package/dist/tenant/commands.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The session command service: `message`, `approve`/`respond` and `cancel`,
|
|
3
3
|
* with per-session idempotency keys. A command locks its session and commits its state
|
|
4
|
-
* change, events and wakes in one transaction (the store publishes the events and
|
|
5
|
-
* `
|
|
6
|
-
* linked agent sessions, one transaction each.
|
|
4
|
+
* change, events and wakes in one transaction (the store publishes the events and delivers the
|
|
5
|
+
* wakes from its outbox after commit, `Tx.wake`), then aborts a cancelled advance and cascades
|
|
6
|
+
* workflow cancels to the linked agent sessions, one transaction each.
|
|
7
7
|
*
|
|
8
|
-
* Wakes (`
|
|
8
|
+
* Wakes (`t.wake`, architecture §12.3) carry the command type as the reason and a dedupe
|
|
9
9
|
* key naming the cause: `<type>:<turnId>:<segment>` for `message`, `approve` and `respond`
|
|
10
10
|
* (every accepted one writes a new checkpoint segment), except that a workflow's `approve` and
|
|
11
11
|
* `respond` name `<type>:<turnId>:<interactionId>`: a flow resumes in the same segment.
|
|
@@ -231,7 +231,7 @@ export async function command(ctx, id, input, scope) {
|
|
|
231
231
|
reason: command.type,
|
|
232
232
|
dedupeKey: `${command.type}:${s.activeTurnId}:${cause}`,
|
|
233
233
|
};
|
|
234
|
-
t.
|
|
234
|
+
await t.wake(id, commandWake, ctx.wake);
|
|
235
235
|
event = await t.event(id, s.activeTurnId, `command.${command.type}`, command);
|
|
236
236
|
}
|
|
237
237
|
await t.put("sessions", id, s);
|
package/dist/tenant/context.d.ts
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Business code changes state only inside `ctx.store.tx(async (t) => …)` and follows the
|
|
7
7
|
* seam rules: events through `t.event(...)` (the relay appends them to Durable Streams after
|
|
8
|
-
* commit), and advances through `t.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* commit), and advances through `await t.wake(id, { reason, dedupeKey }, ctx.wake)`, which
|
|
9
|
+
* writes the wake to the outbox in the transaction and delivers it after commit. It never
|
|
10
|
+
* publishes or notifies itself; `runtime.ts` wires the streams with `wireStreams()`. No
|
|
11
|
+
* external I/O runs inside a tx.
|
|
11
12
|
*
|
|
12
13
|
* `wake` goes to `DurableExecution.wake`, which calls the Tenant's `advance` (`advance.ts`)
|
|
13
14
|
* under ownership (§10.6); `abortLocal` aborts an advance running on this process.
|
|
@@ -211,11 +212,13 @@ export interface TenantContext {
|
|
|
211
212
|
/** How long an advance's ownership lease lasts; the heartbeat renews it. */
|
|
212
213
|
readonly ownerLeaseMs: number;
|
|
213
214
|
/**
|
|
214
|
-
* Seam:
|
|
215
|
-
* `t.
|
|
216
|
-
*
|
|
215
|
+
* Seam: hand a wake to `DurableExecution.wake`, outside any tx. Business code passes it to
|
|
216
|
+
* `t.wake` (the outbox), which calls it after commit; the sweep calls it for outbox rows
|
|
217
|
+
* left undelivered and for orphaned sessions. Resolves `true` once the execution accepted
|
|
218
|
+
* the wake, `false` while the Tenant is closing: the wake is not sent, and its outbox row
|
|
219
|
+
* stays for the next sweep.
|
|
217
220
|
*/
|
|
218
|
-
wake(sessionId: string, wake: Wake): Promise<
|
|
221
|
+
wake(sessionId: string, wake: Wake): Promise<boolean>;
|
|
219
222
|
/**
|
|
220
223
|
* Seam: abort the advance of a session running in this process, if any. With `turnId`, only
|
|
221
224
|
* an advance of that turn (a cancel signal names the turn it cancelled).
|
package/dist/tenant/runtime.js
CHANGED
|
@@ -261,8 +261,9 @@ export class TenantRuntime {
|
|
|
261
261
|
ownerLeaseMs: config.ownerLeaseMs ?? DEFAULT_OWNER_LEASE_MS,
|
|
262
262
|
wake: async (sessionId, wake) => {
|
|
263
263
|
if (ctx.closing || ctx.closed)
|
|
264
|
-
return;
|
|
264
|
+
return false;
|
|
265
265
|
await execution.wake(config.tenantId, sessionId, wake);
|
|
266
|
+
return true;
|
|
266
267
|
},
|
|
267
268
|
abortLocal: (sessionId, turnId) => abortLocal(ctx, sessionId, "cancel", turnId),
|
|
268
269
|
...(hooks.pods ? { pods: hooks.pods } : {}),
|
package/dist/tenant/sweep.d.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import type { TenantContext } from "./context.js";
|
|
2
|
+
/**
|
|
3
|
+
* How old a wake outbox row is before the sweep delivers it: longer than a delivery after
|
|
4
|
+
* commit takes, so the sweep seldom sends a wake its commit is still sending. Sending one
|
|
5
|
+
* twice is harmless either way (the idempotency key).
|
|
6
|
+
*/
|
|
7
|
+
export declare const WAKE_GRACE_MS = 2000;
|
|
8
|
+
/** How often the orphan scan runs after the first pass of a Tenant opened on this process. */
|
|
9
|
+
export declare const ORPHAN_SCAN_MS = 60000;
|
|
2
10
|
export declare function sweep(ctx: TenantContext): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Deliver the wake outbox rows written before `before` (oldest first, one batch) and delete
|
|
13
|
+
* the delivered ones. A row the Tenant declines (it is closing) stays. Returns the ids of the
|
|
14
|
+
* rows delivered.
|
|
15
|
+
*/
|
|
16
|
+
export declare function deliverPendingWakes(ctx: Pick<TenantContext, "store" | "wake">, before?: Date): Promise<string[]>;
|
|
3
17
|
/** Settle pending workflow `agent` effects whose linked turn already finished. */
|
|
4
18
|
export declare function reconcileLinkedAgents(ctx: Pick<TenantContext, "store" | "wake">): Promise<void>;
|
|
5
19
|
/** Wake `running`/`runnable` sessions without a live owner. Returns the ids woken. */
|
package/dist/tenant/sweep.js
CHANGED
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Tenant sweep (architecture §12.3, "Timers"): one self-re-arming pass per Tenant, armed
|
|
3
3
|
* with `DurableExecution.armSweep` when the Tenant opens. It replaces every in-process timer
|
|
4
|
-
* and startup scan the Tenant runtime used to keep
|
|
4
|
+
* and startup scan the Tenant runtime used to keep. It reconciles the Session Store with what
|
|
5
|
+
* Durable Session Execution was asked to do; it never decides when a session advances:
|
|
5
6
|
*
|
|
6
|
-
* 1. **
|
|
7
|
+
* 1. **Wakes.** Wake outbox rows (`Tx.wake`) are delivered again and deleted: a delivery
|
|
8
|
+
* after commit that failed, or that a crash between the commit and the send cut off. The
|
|
9
|
+
* first pass on this process takes every row, so a previous process's are not left
|
|
10
|
+
* waiting; later passes take the rows older than `WAKE_GRACE_MS`. Each row carries the
|
|
11
|
+
* idempotency key it was first sent with, so a wake the execution did accept before the
|
|
12
|
+
* crash, or one still being sent after its commit, causes no second advance.
|
|
13
|
+
* 2. **Linked agents.** Pending workflow `agent` effects whose linked turn already settled
|
|
7
14
|
* are completed and the workflow is woken (a crash between the two, or data written by an
|
|
8
|
-
* older Runtime).
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
15
|
+
* older Runtime).
|
|
16
|
+
* 3. **Orphaned sessions.** On this process's first pass for the Tenant, then at most every
|
|
17
|
+
* `ORPHAN_SCAN_MS`: `running` or `runnable` sessions with no owner, or an owner whose lease
|
|
18
|
+
* expired, are woken with reason `recover`. Neither a lost wake (step 1) nor a Worker that
|
|
19
|
+
* died mid-advance (the execution retries that advance, which takes over, §11.4) needs it:
|
|
20
|
+
* it is the backstop for what the execution itself lost or ended without an advance, such
|
|
21
|
+
* as an in-process execution's queue when its process stopped, an advance for a Tenant not
|
|
22
|
+
* open on its Worker, an invocation an operator killed, or Restate's state wiped (§17.11).
|
|
23
|
+
* 4. **Sandboxes.** Idle sandboxes are stopped, records of compute no longer held are marked
|
|
13
24
|
* stopped, and sandboxes whose session or sandbox resource is gone are removed, by the
|
|
14
25
|
* workspace capability (`harness-api/workspace.ts`: here, or in the harness that serves
|
|
15
26
|
* workspaces). Idle MCP connections of the in-process harness are closed.
|
|
16
|
-
*
|
|
27
|
+
* 5. **Control signals.** Signals on the control bus older than `SIGNAL_RETENTION_MS` are
|
|
17
28
|
* deleted: followers read back only the last two minutes (`store/postgres/control.ts`).
|
|
18
|
-
*
|
|
29
|
+
* 6. **Hooks.** Callbacks registered with `ctx.onSweep`.
|
|
19
30
|
*
|
|
20
31
|
* Every step runs one transaction per session it changes (a linked agent and its workflow
|
|
21
32
|
* share one, child first), so the sweep follows the lock order in `store/types.ts`. A failing
|
|
@@ -26,13 +37,41 @@ import { pendingAgentEffects, reconcilePendingAgentEffect, } from "../core/flow-
|
|
|
26
37
|
const BATCH = 100;
|
|
27
38
|
/** How long a control signal is kept after it was written. */
|
|
28
39
|
const SIGNAL_RETENTION_MS = 60 * 60 * 1000;
|
|
40
|
+
/**
|
|
41
|
+
* How old a wake outbox row is before the sweep delivers it: longer than a delivery after
|
|
42
|
+
* commit takes, so the sweep seldom sends a wake its commit is still sending. Sending one
|
|
43
|
+
* twice is harmless either way (the idempotency key).
|
|
44
|
+
*/
|
|
45
|
+
export const WAKE_GRACE_MS = 2000;
|
|
46
|
+
/** How often the orphan scan runs after the first pass of a Tenant opened on this process. */
|
|
47
|
+
export const ORPHAN_SCAN_MS = 60_000;
|
|
48
|
+
/** Each open Tenant's sweep state on this process: when its orphan scan last ran. */
|
|
49
|
+
const passes = new WeakMap();
|
|
29
50
|
export async function sweep(ctx) {
|
|
30
51
|
if (ctx.closing || ctx.closed)
|
|
31
52
|
return;
|
|
32
53
|
const now = new Date();
|
|
54
|
+
const first = !passes.has(ctx);
|
|
55
|
+
const pass = passes.get(ctx) ?? {};
|
|
56
|
+
passes.set(ctx, pass);
|
|
57
|
+
const scanOrphans = pass.orphansAt === undefined || now.getTime() - pass.orphansAt >= ORPHAN_SCAN_MS;
|
|
33
58
|
const steps = [
|
|
59
|
+
[
|
|
60
|
+
"wakes",
|
|
61
|
+
() => deliverPendingWakes(ctx, first ? now : new Date(now.getTime() - WAKE_GRACE_MS)),
|
|
62
|
+
],
|
|
34
63
|
["linked", () => reconcileLinkedAgents(ctx)],
|
|
35
|
-
|
|
64
|
+
...(scanOrphans
|
|
65
|
+
? [
|
|
66
|
+
[
|
|
67
|
+
"orphans",
|
|
68
|
+
async () => {
|
|
69
|
+
await wakeOrphanedSessions(ctx, now);
|
|
70
|
+
pass.orphansAt = now.getTime();
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
]
|
|
74
|
+
: []),
|
|
36
75
|
[
|
|
37
76
|
"sandboxes",
|
|
38
77
|
() => ctx.sandbox.sweep({
|
|
@@ -68,6 +107,26 @@ export async function sweep(ctx) {
|
|
|
68
107
|
if (failure)
|
|
69
108
|
throw failure.error;
|
|
70
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Deliver the wake outbox rows written before `before` (oldest first, one batch) and delete
|
|
112
|
+
* the delivered ones. A row the Tenant declines (it is closing) stays. Returns the ids of the
|
|
113
|
+
* rows delivered.
|
|
114
|
+
*/
|
|
115
|
+
export async function deliverPendingWakes(ctx, before = new Date()) {
|
|
116
|
+
const pending = await ctx.store.tx((t) => t.pendingWakes(before, BATCH));
|
|
117
|
+
const delivered = [];
|
|
118
|
+
try {
|
|
119
|
+
for (const row of pending)
|
|
120
|
+
if ((await ctx.wake(row.sessionId, row.wake)) !== false)
|
|
121
|
+
delivered.push(row.id);
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
// Delivered ones go even when a later one fails, so they are not sent again.
|
|
125
|
+
if (delivered.length > 0)
|
|
126
|
+
await ctx.store.tx((t) => t.deleteWakes(delivered));
|
|
127
|
+
}
|
|
128
|
+
return delivered;
|
|
129
|
+
}
|
|
71
130
|
/** Settle pending workflow `agent` effects whose linked turn already finished. */
|
|
72
131
|
export async function reconcileLinkedAgents(ctx) {
|
|
73
132
|
const { store } = ctx;
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const RUNTIME_VERSION = "0.21.
|
|
1
|
+
export declare const RUNTIME_VERSION = "0.21.1-beta";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nylorun/runtime",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.1-beta",
|
|
4
4
|
"description": "Nylorun Runtime: the agent execution Host on Postgres, Restate and S2, and model providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
"@hono/node-server": "^2.1.3",
|
|
66
66
|
"@hono/zod-openapi": "^1.6.3",
|
|
67
67
|
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
68
|
-
"@nylorun/core": "0.
|
|
69
|
-
"@nylorun/harness": "0.
|
|
68
|
+
"@nylorun/core": "0.18.0-beta",
|
|
69
|
+
"@nylorun/harness": "0.25.0-beta",
|
|
70
70
|
"@restatedev/restate-sdk": "1.17.2",
|
|
71
71
|
"@s2-dev/streamstore": "0.28.0",
|
|
72
72
|
"aws4fetch": "1.0.20",
|