@nylorun/runtime 0.14.0-beta → 0.15.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.
- package/CHANGELOG.md +104 -0
- package/README.md +75 -33
- package/dist/adapters/execution/restate.js +2 -2
- package/dist/adapters/replication/pgoutput.d.ts +5 -0
- package/dist/adapters/replication/pgoutput.js +4 -3
- package/dist/admin-openapi.json +184 -685
- package/dist/api/components.d.ts +308 -68
- package/dist/api/components.js +4 -5
- package/dist/api/gate/keys.d.ts +14 -0
- package/dist/api/gate/keys.js +59 -0
- package/dist/api/gate/routes.d.ts +34 -0
- package/dist/api/gate/routes.js +235 -0
- package/dist/api/http/define.js +0 -8
- package/dist/api/http/routes/access.js +25 -18
- package/dist/api/http/routes/tenant.d.ts +2 -1
- package/dist/api/http/routes/tenant.js +77 -5
- package/dist/api/http/routes/vaults.js +2 -2
- package/dist/api/openapi.js +4 -5
- package/dist/gates/contract.d.ts +51 -0
- package/dist/gates/contract.js +37 -0
- package/dist/gates/handler.d.ts +21 -0
- package/dist/gates/handler.js +55 -0
- package/dist/gates/http-client.d.ts +12 -0
- package/dist/gates/http-client.js +160 -0
- package/dist/gates/in-process.d.ts +32 -0
- package/dist/gates/in-process.js +42 -0
- package/dist/gates/inflight.d.ts +39 -0
- package/dist/gates/inflight.js +70 -0
- package/dist/gates/mcp-handler.d.ts +35 -0
- package/dist/gates/mcp-handler.js +142 -0
- package/dist/gates/meter.d.ts +24 -0
- package/dist/gates/meter.js +165 -0
- package/dist/gates/model-gate.d.ts +45 -0
- package/dist/gates/model-gate.js +1 -0
- package/dist/gates/tenant-vaults.d.ts +64 -0
- package/dist/gates/tenant-vaults.js +94 -0
- package/dist/gates/tool-calls.d.ts +51 -0
- package/dist/gates/tool-calls.js +64 -0
- package/dist/gates/tool-client.d.ts +12 -0
- package/dist/gates/tool-client.js +201 -0
- package/dist/gates/tool-contract.d.ts +116 -0
- package/dist/gates/tool-contract.js +64 -0
- package/dist/gates/tool-gate.d.ts +45 -0
- package/dist/gates/tool-gate.js +10 -0
- package/dist/host/admin-api.d.ts +7 -6
- package/dist/host/admin-api.js +9 -113
- package/dist/host/app.d.ts +1 -1
- package/dist/host/app.js +20 -24
- package/dist/host/config-for.d.ts +1 -1
- package/dist/host/config-for.js +2 -2
- package/dist/host/create-host.d.ts +5 -5
- package/dist/host/create-host.js +5 -15
- package/dist/host/execution.d.ts +20 -18
- package/dist/host/execution.js +2 -2
- package/dist/host/gates.d.ts +58 -0
- package/dist/host/gates.js +127 -0
- package/dist/host/http.d.ts +6 -1
- package/dist/host/http.js +17 -0
- package/dist/host/index.d.ts +1 -1
- package/dist/host/main.js +151 -71
- package/dist/host/stack-config.d.ts +71 -19
- package/dist/host/stack-config.js +192 -18
- package/dist/infra/database.d.ts +2 -2
- package/dist/infra/execution.d.ts +5 -5
- package/dist/infra/execution.js +8 -6
- package/dist/infra/index.d.ts +1 -1
- package/dist/infra/workers.d.ts +17 -17
- package/dist/infra/workers.js +9 -9
- package/dist/keys/client.d.ts +9 -0
- package/dist/keys/client.js +70 -0
- package/dist/keys/contract.d.ts +24 -0
- package/dist/keys/contract.js +12 -0
- package/dist/keys/keys.d.ts +41 -0
- package/dist/keys/keys.js +50 -0
- package/dist/mcp/connect.d.ts +43 -3
- package/dist/mcp/connect.js +12 -5
- package/dist/mcp/pool.d.ts +21 -3
- package/dist/mcp/pool.js +15 -2
- package/dist/openapi.json +887 -694
- package/dist/record/append.d.ts +51 -0
- package/dist/record/append.js +20 -0
- package/dist/{store/event.js → record/envelope.js} +1 -1
- package/dist/record/index.d.ts +11 -0
- package/dist/record/index.js +11 -0
- package/dist/store/postgres/connect.d.ts +15 -12
- package/dist/store/postgres/connect.js +24 -12
- package/dist/store/postgres/db.d.ts +16 -0
- package/dist/store/postgres/db.js +28 -0
- package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
- package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
- package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
- package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
- package/dist/store/postgres/migrate.d.ts +68 -0
- package/dist/store/postgres/migrate.js +183 -0
- package/dist/store/postgres/record-writer.d.ts +4 -0
- package/dist/store/postgres/record-writer.js +34 -0
- package/dist/store/postgres/record.d.ts +2 -8
- package/dist/store/postgres/record.js +50 -54
- package/dist/store/postgres/schema.d.ts +3096 -0
- package/dist/store/postgres/schema.js +363 -0
- package/dist/store/postgres/store.d.ts +2 -4
- package/dist/store/postgres/store.js +481 -567
- package/dist/store/postgres/tenant.d.ts +56 -0
- package/dist/store/postgres/tenant.js +154 -0
- package/dist/store/types.d.ts +43 -87
- package/dist/store/types.js +0 -1
- package/dist/streams/control.d.ts +1 -1
- package/dist/streams/control.js +6 -2
- package/dist/streams/relay/types.d.ts +2 -1
- package/dist/streams/relay/types.js +2 -1
- package/dist/streams/types.d.ts +5 -0
- package/dist/tenant/advance.d.ts +7 -2
- package/dist/tenant/advance.js +95 -16
- package/dist/tenant/cause.d.ts +25 -0
- package/dist/tenant/cause.js +45 -0
- package/dist/tenant/commands.js +4 -2
- package/dist/tenant/context.d.ts +26 -3
- package/dist/tenant/delivery-token.js +17 -18
- package/dist/tenant/delivery.d.ts +1 -1
- package/dist/tenant/delivery.js +18 -13
- package/dist/tenant/effects.d.ts +15 -1
- package/dist/tenant/effects.js +93 -17
- package/dist/tenant/ephemeral.d.ts +16 -7
- package/dist/tenant/ephemeral.js +45 -111
- package/dist/tenant/history.d.ts +64 -0
- package/dist/tenant/history.js +120 -0
- package/dist/tenant/model-setting.d.ts +2 -2
- package/dist/tenant/module.d.ts +14 -28
- package/dist/tenant/module.js +131 -271
- package/dist/tenant/paths.d.ts +11 -16
- package/dist/tenant/paths.js +24 -68
- package/dist/tenant/pool.d.ts +1 -1
- package/dist/tenant/pool.js +1 -1
- package/dist/tenant/principals.d.ts +23 -11
- package/dist/tenant/principals.js +61 -49
- package/dist/tenant/runtime.d.ts +19 -1
- package/dist/tenant/runtime.js +40 -26
- package/dist/tenant/scheduler.d.ts +4 -2
- package/dist/tenant/scheduler.js +4 -4
- package/dist/tenant/session-streams.d.ts +2 -0
- package/dist/tenant/session-streams.js +8 -3
- package/dist/tenant/status.d.ts +3 -0
- package/dist/tenant/status.js +1 -1
- package/dist/tenant/store-pg.d.ts +9 -6
- package/dist/tenant/store-pg.js +60 -125
- package/dist/tenant/streams.d.ts +9 -25
- package/dist/tenant/streams.js +7 -22
- package/dist/tenant/tokens.js +16 -14
- package/dist/tenant/types.d.ts +41 -92
- package/dist/tenant/types.js +4 -15
- package/dist/tenant/worker.js +4 -4
- package/dist/vault/host-model.d.ts +31 -0
- package/dist/vault/host-model.js +57 -0
- package/dist/vault/service.d.ts +26 -15
- package/dist/vault/service.js +70 -109
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -5
- package/dist/store/memory.d.ts +0 -69
- package/dist/store/memory.js +0 -810
- package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
- package/dist/store/postgres/migrations/001_initial.js +0 -196
- package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
- package/dist/store/postgres/migrations/002_session_owner.js +0 -13
- package/dist/store/postgres/migrations/003_subject_tokens.d.ts +0 -8
- package/dist/store/postgres/migrations/003_subject_tokens.js +0 -42
- package/dist/store/postgres/migrations/004_publishable_keys.d.ts +0 -6
- package/dist/store/postgres/migrations/004_publishable_keys.js +0 -18
- package/dist/store/postgres/migrations/005_action_endpoints.d.ts +0 -6
- package/dist/store/postgres/migrations/005_action_endpoints.js +0 -32
- package/dist/store/postgres/migrations/006_drop_executors.d.ts +0 -11
- package/dist/store/postgres/migrations/006_drop_executors.js +0 -32
- package/dist/store/postgres/migrations/007_event_time.d.ts +0 -7
- package/dist/store/postgres/migrations/007_event_time.js +0 -14
- package/dist/store/postgres/migrations/008_durable_streams.d.ts +0 -13
- package/dist/store/postgres/migrations/008_durable_streams.js +0 -35
- package/dist/store/postgres/migrations/index.d.ts +0 -53
- package/dist/store/postgres/migrations/index.js +0 -96
- package/dist/store/postgres/migrations/shared/index.d.ts +0 -31
- package/dist/store/postgres/migrations/shared/index.js +0 -92
- package/dist/store/postgres/names.d.ts +0 -11
- package/dist/store/postgres/names.js +0 -42
- package/dist/store/postgres/tenants.d.ts +0 -90
- package/dist/store/postgres/tenants.js +0 -228
- package/dist/streams/relay/memory.d.ts +0 -69
- package/dist/streams/relay/memory.js +0 -176
- package/dist/tenant/envelope.d.ts +0 -7
- package/dist/tenant/envelope.js +0 -22
- package/dist/tenant/quarantine-error.d.ts +0 -6
- package/dist/tenant/quarantine-error.js +0 -9
- package/dist/tenant/quarantine.d.ts +0 -27
- package/dist/tenant/quarantine.js +0 -72
- package/dist/tenant/store-memory.d.ts +0 -16
- package/dist/tenant/store-memory.js +0 -111
- /package/dist/{store → record}/cursor.d.ts +0 -0
- /package/dist/{store → record}/cursor.js +0 -0
- /package/dist/{store/event.d.ts → record/envelope.d.ts} +0 -0
package/dist/tenant/store-pg.js
CHANGED
|
@@ -1,140 +1,75 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Opens the Host's Tenant on its Postgres database (tenancy.md §4–§5, plan P9).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* the Tenant
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* The database holds one Tenant (`store/postgres/tenant.ts`): opening migrates it, creates
|
|
5
|
+
* the Tenant on first start (from the Host's configuration: id, name, Studio and derived
|
|
6
|
+
* principals), and opens the Tenant Runtime on its Session Store. What stays on the Host root
|
|
7
|
+
* is the Tenant directory `tenant/`: the vault key (`vault-kek`), `plugin-data/`, `logs/`,
|
|
8
|
+
* and the private `home/`, `tmp/` and `sandboxes/`.
|
|
9
9
|
*
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* throws that quarantine. A failure outside the Tenant (Postgres unreachable, its sweep
|
|
16
|
-
* not armed) is `TenantUnavailableError`, which the module does not cache.
|
|
17
|
-
* - `trash` drops the schema and removes the directory. There is no `trash/` copy: the key
|
|
18
|
-
* is useless without the schema.
|
|
10
|
+
* - A failure in the Tenant (an old layout, a database newer than this Runtime, a failed
|
|
11
|
+
* migration, an unreadable Tenant row, vault ciphertext without its key) rejects with that
|
|
12
|
+
* `TenantOpenError`; the Host reports it and fails readiness.
|
|
13
|
+
* - A failure outside the Tenant (Postgres unreachable, its sweep not armed in Restate) is
|
|
14
|
+
* `TenantUnavailableError`; the Tenant module retries.
|
|
19
15
|
*/
|
|
20
|
-
import { mkdirSync
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
16
|
+
import { mkdirSync } from "node:fs";
|
|
17
|
+
import { openTenantDatabase, } from "../store/postgres/tenant.js";
|
|
18
|
+
import { TenantOpenError } from "./cause.js";
|
|
23
19
|
import { tenantPaths } from "./paths.js";
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
/** Repair instructions for a Postgres Tenant, naming its schema. */
|
|
27
|
-
function repairFor(code, tenantId) {
|
|
28
|
-
const status = `nylo tenant status ${tenantId}`;
|
|
29
|
-
const schema = tenantSchemaName(tenantId);
|
|
30
|
-
switch (code) {
|
|
31
|
-
case "schema-too-new":
|
|
32
|
-
return `${status} — schema ${schema} was migrated by a newer Runtime; run that version or newer`;
|
|
33
|
-
case "migration-failed":
|
|
34
|
-
return `${status} — inspect the Runtime log for the failed migration of ${schema}, fix it, then restart the Runtime`;
|
|
35
|
-
case "envelope-invalid":
|
|
36
|
-
return `${status} — the tenant table of schema ${schema} is missing or invalid; restore the schema from a Postgres backup`;
|
|
37
|
-
case "kek-missing":
|
|
38
|
-
return `${status} — restore vault-kek under the Tenant directory (ciphertext cannot be opened without it)`;
|
|
39
|
-
default:
|
|
40
|
-
return `${status} — inspect the Runtime log and Postgres, then restart the Runtime`;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
function unavailable(tenantId, cause) {
|
|
20
|
+
import { TenantUnavailableError, } from "./types.js";
|
|
21
|
+
function unavailable(cause) {
|
|
44
22
|
return cause instanceof TenantUnavailableError
|
|
45
23
|
? cause
|
|
46
|
-
: new TenantUnavailableError(
|
|
47
|
-
}
|
|
48
|
-
function quarantined(reason, tenantId) {
|
|
49
|
-
return new QuarantineError({ ...reason, repair: repairFor(reason.code, tenantId) });
|
|
24
|
+
: new TenantUnavailableError({ cause });
|
|
50
25
|
}
|
|
51
|
-
export function
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
26
|
+
export function createPostgresTenantOpener(options) {
|
|
27
|
+
const { logger } = options;
|
|
28
|
+
return async () => {
|
|
29
|
+
const opened = await openTenantDatabase({
|
|
30
|
+
sql: options.sql,
|
|
31
|
+
create: options.create,
|
|
32
|
+
...(options.migrations ? { migrations: options.migrations } : {}),
|
|
33
|
+
onError: (error) => logger?.error("post-commit step failed", {
|
|
34
|
+
message: error instanceof Error ? error.message : String(error),
|
|
35
|
+
}),
|
|
36
|
+
}).catch((error) => {
|
|
37
|
+
if (error instanceof TenantOpenError)
|
|
38
|
+
throw error;
|
|
39
|
+
throw unavailable(error);
|
|
40
|
+
});
|
|
41
|
+
const { store, envelope } = opened;
|
|
42
|
+
const tenantId = envelope.id;
|
|
43
|
+
if (opened.created)
|
|
44
|
+
logger?.info("tenant created", { tenantId, name: envelope.name });
|
|
45
|
+
else if (options.create.tenantId !== undefined && options.create.tenantId !== tenantId)
|
|
46
|
+
logger?.warn("tenant id configured differs", {
|
|
47
|
+
tenantId,
|
|
48
|
+
configured: options.create.tenantId,
|
|
49
|
+
message: "The database already holds a Tenant; the configured id applies only to a new database",
|
|
50
|
+
});
|
|
51
|
+
if (opened.migrated.from !== opened.migrated.to)
|
|
52
|
+
logger?.info("tenant schema migrated", {
|
|
53
|
+
tenantId,
|
|
54
|
+
from: opened.migrated.from,
|
|
55
|
+
to: opened.migrated.to,
|
|
56
|
+
});
|
|
67
57
|
try {
|
|
68
|
-
|
|
58
|
+
const config = options.configFor(tenantId);
|
|
59
|
+
const paths = tenantPaths(options.hostRoot);
|
|
60
|
+
mkdirSync(paths.root, { recursive: true, mode: 0o700 });
|
|
61
|
+
return await options.openRuntime({ ...config, tenantId }, { store, envelope });
|
|
69
62
|
}
|
|
70
63
|
catch (error) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
throw new TenantNotFoundError(id);
|
|
76
|
-
throw quarantined(error.toQuarantine(), id);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return {
|
|
80
|
-
enumerate: () => catalog.listTenantIds(),
|
|
81
|
-
readEnvelope,
|
|
82
|
-
async create(envelope, bootstrap) {
|
|
83
|
-
makeDirectory(envelope.id);
|
|
84
|
-
try {
|
|
85
|
-
const result = await catalog.createTenant({ envelope, principals: bootstrap });
|
|
86
|
-
return result.status;
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
// An existing Tenant with other bootstrap material; the module reports the conflict.
|
|
90
|
-
if (error instanceof TenantConflictError)
|
|
91
|
-
return "exists";
|
|
64
|
+
await store.close().catch(() => undefined);
|
|
65
|
+
if (error instanceof TenantOpenError) {
|
|
66
|
+
// The Tenant's own failure (`kek-missing`): report which Tenant it is.
|
|
67
|
+
error.envelope ??= envelope;
|
|
92
68
|
throw error;
|
|
93
69
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.openTenant(id)
|
|
99
|
-
.catch((error) => {
|
|
100
|
-
throw unavailable(id, error);
|
|
101
|
-
});
|
|
102
|
-
if (result.status === "not-found")
|
|
103
|
-
throw new TenantNotFoundError(id);
|
|
104
|
-
if (result.status === "quarantined")
|
|
105
|
-
throw quarantined(result.reason, id);
|
|
106
|
-
if (result.migrated.from !== result.migrated.to)
|
|
107
|
-
options.logger?.info("tenant schema migrated", {
|
|
108
|
-
tenantId: id,
|
|
109
|
-
from: result.migrated.from,
|
|
110
|
-
to: result.migrated.to,
|
|
111
|
-
});
|
|
112
|
-
try {
|
|
113
|
-
makeDirectory(id);
|
|
114
|
-
const config = options.configFor(id);
|
|
115
|
-
return await options.openRuntime({ ...config, tenantId: id }, { store: result.store, envelope: result.envelope });
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
await result.store.close().catch(() => undefined);
|
|
119
|
-
// The Runtime's own quarantines (`kek-missing`) get this store's repair wording.
|
|
120
|
-
const reason = error?.quarantine;
|
|
121
|
-
if (reason)
|
|
122
|
-
throw quarantined(reason, id);
|
|
123
|
-
if (error instanceof QuarantineError)
|
|
124
|
-
throw error;
|
|
125
|
-
// Anything else failed outside the Tenant (Postgres, arming its sweep in Restate):
|
|
126
|
-
// the next use tries again rather than quarantining it.
|
|
127
|
-
throw unavailable(id, error);
|
|
128
|
-
}
|
|
129
|
-
},
|
|
130
|
-
async trash(id) {
|
|
131
|
-
await catalog.deleteTenant(id);
|
|
132
|
-
rmSync(paths(id).root, { recursive: true, force: true });
|
|
133
|
-
},
|
|
134
|
-
async removePartial(id) {
|
|
135
|
-
if (await catalog.tenantExists(id))
|
|
136
|
-
return;
|
|
137
|
-
rmSync(paths(id).root, { recursive: true, force: true });
|
|
138
|
-
},
|
|
70
|
+
// Anything else failed outside the Tenant (Postgres, arming its sweep in Restate): the
|
|
71
|
+
// next use tries again.
|
|
72
|
+
throw unavailable(error);
|
|
73
|
+
}
|
|
139
74
|
};
|
|
140
75
|
}
|
package/dist/tenant/streams.d.ts
CHANGED
|
@@ -4,16 +4,15 @@
|
|
|
4
4
|
*
|
|
5
5
|
* - **Record.** Every event is written to the record in its transaction (`Tx.event`). The
|
|
6
6
|
* stream relay (`streams/relay/`) feeds the Tenant's basin from it. On a Host with S2, one
|
|
7
|
-
* process-wide relay reads
|
|
7
|
+
* process-wide relay reads the record over logical replication (`host/main.ts`);
|
|
8
8
|
* otherwise (tests, a Host without S2) this module runs a relay of the Tenant's own commits,
|
|
9
9
|
* which reconciles the record with the streams when it starts.
|
|
10
10
|
* - **Basin generations.** The Tenant's session streams live in its current basin generation
|
|
11
11
|
* (`streams/basin.ts`). A reset moves it to the next one (`tenantReset`): the old basin gets
|
|
12
12
|
* a `sessions.reset` signal, every process moves its readers, and the old basin is deleted
|
|
13
13
|
* after a grace period (again when the Tenant opens, until it is gone).
|
|
14
|
-
* - **Basin.** The current basin is created
|
|
15
|
-
*
|
|
16
|
-
* repaired on first use: a failed `ensureTenant` retries in the background with backoff,
|
|
14
|
+
* - **Basin.** The current basin is created when the Tenant opens. A missing basin (S2 down
|
|
15
|
+
* then, or deleted since) is repaired on first use: a failed `ensureTenant` retries in the background with backoff,
|
|
17
16
|
* and `streamsStatus` reports the basin's state.
|
|
18
17
|
* - **Readers.** History and session SSE read the session's stream
|
|
19
18
|
* (`tenant/session-streams.ts`). This module runs one `tenant/control` reader per Tenant per
|
|
@@ -35,7 +34,7 @@ export interface WireStreamsOptions {
|
|
|
35
34
|
/** Close `streams` with the wiring (streams created for this Tenant alone). */
|
|
36
35
|
ownsStreams?: boolean;
|
|
37
36
|
/**
|
|
38
|
-
* The Host runs the stream relay for
|
|
37
|
+
* The Host runs the stream relay for the Tenant (logical replication). Otherwise the
|
|
39
38
|
* Tenant relays its own commits.
|
|
40
39
|
*/
|
|
41
40
|
hostRelay?: boolean;
|
|
@@ -73,12 +72,12 @@ export interface StreamsWiring {
|
|
|
73
72
|
*/
|
|
74
73
|
export declare function wireStreams(ctx: TenantContext, options: WireStreamsOptions): Promise<StreamsWiring>;
|
|
75
74
|
/**
|
|
76
|
-
* Appends `session.cancel` for `sessionId` to `tenant/control`, so the
|
|
77
|
-
* advance aborts it. Call it from `t.afterCommit` after a cancel
|
|
78
|
-
* latency only (the advance checks the Session Store before every
|
|
79
|
-
* logged, never thrown.
|
|
75
|
+
* Appends `session.cancel` for `sessionId` and its cancelled turn to `tenant/control`, so the
|
|
76
|
+
* process running that turn's advance aborts it. Call it from `t.afterCommit` after a cancel
|
|
77
|
+
* commits. A lost signal costs latency only (the advance checks the Session Store before every
|
|
78
|
+
* effect), so failures are logged, never thrown.
|
|
80
79
|
*/
|
|
81
|
-
export declare function signalSessionCancel(ctx: TenantContext, sessionId: string): void;
|
|
80
|
+
export declare function signalSessionCancel(ctx: TenantContext, sessionId: string, turnId: string | null): void;
|
|
82
81
|
/**
|
|
83
82
|
* After a reset deleted the Tenant's sessions and moved it to a new basin generation: tells
|
|
84
83
|
* every process with the Tenant open (`sessions.reset` on the old basin's control stream),
|
|
@@ -100,20 +99,5 @@ export interface StreamsStatus {
|
|
|
100
99
|
export declare function streamsStatus(ctx: TenantContext, options?: {
|
|
101
100
|
probeTimeoutMs?: number;
|
|
102
101
|
}): Promise<StreamsStatus>;
|
|
103
|
-
/**
|
|
104
|
-
* Creates a new Tenant's basin. Call it when the Tenant is created, after its schema commits;
|
|
105
|
-
* retries transient failures, then throws. A failure need not fail the creation: opening the
|
|
106
|
-
* Tenant repairs a missing basin.
|
|
107
|
-
*/
|
|
108
|
-
export declare function createTenantStreams(streams: DurableStreams, tenantId: string, options?: {
|
|
109
|
-
attempts?: number;
|
|
110
|
-
}): Promise<void>;
|
|
111
|
-
/**
|
|
112
|
-
* Deletes every basin of a Tenant and every stream in them. Call it when the Tenant is
|
|
113
|
-
* deleted. Idempotent; retries transient failures, then throws.
|
|
114
|
-
*/
|
|
115
|
-
export declare function deleteTenantStreams(streams: DurableStreams, tenantId: string, options?: {
|
|
116
|
-
attempts?: number;
|
|
117
|
-
}): Promise<void>;
|
|
118
102
|
/** Close: stops the readers and relay; the observers were ended by `endAllStreams`. */
|
|
119
103
|
export declare function closeStreams(ctx: TenantContext): Promise<void>;
|
package/dist/tenant/streams.js
CHANGED
|
@@ -58,7 +58,7 @@ export async function wireStreams(ctx, options) {
|
|
|
58
58
|
const startControl = () => follow(streams, currentBasin(ctx), CONTROL_STREAM, AbortSignal.any([stop.signal, control.signal]), (body) => {
|
|
59
59
|
const signal = body;
|
|
60
60
|
if (signal?.type === "session.cancel" && typeof signal.sessionId === "string")
|
|
61
|
-
ctx.abortLocal(signal.sessionId);
|
|
61
|
+
ctx.abortLocal(signal.sessionId, typeof signal.turnId === "string" ? signal.turnId : undefined);
|
|
62
62
|
else if (signal?.type === "sessions.reset")
|
|
63
63
|
void checkSessionStreams(ctx).catch(report("session stream check failed"));
|
|
64
64
|
else if (signal?.type === "subject.revoked" &&
|
|
@@ -157,16 +157,16 @@ function commitSource(store, tenantId) {
|
|
|
157
157
|
};
|
|
158
158
|
}
|
|
159
159
|
/**
|
|
160
|
-
* Appends `session.cancel` for `sessionId` to `tenant/control`, so the
|
|
161
|
-
* advance aborts it. Call it from `t.afterCommit` after a cancel
|
|
162
|
-
* latency only (the advance checks the Session Store before every
|
|
163
|
-
* logged, never thrown.
|
|
160
|
+
* Appends `session.cancel` for `sessionId` and its cancelled turn to `tenant/control`, so the
|
|
161
|
+
* process running that turn's advance aborts it. Call it from `t.afterCommit` after a cancel
|
|
162
|
+
* commits. A lost signal costs latency only (the advance checks the Session Store before every
|
|
163
|
+
* effect), so failures are logged, never thrown.
|
|
164
164
|
*/
|
|
165
|
-
export function signalSessionCancel(ctx, sessionId) {
|
|
165
|
+
export function signalSessionCancel(ctx, sessionId, turnId) {
|
|
166
166
|
const streams = ctx.sessionStreams.wiring?.streams;
|
|
167
167
|
if (!streams)
|
|
168
168
|
return;
|
|
169
|
-
void signalCancel(streams, currentBasin(ctx), sessionId).catch((error) => ctx.config.logger.warn("cancel signal failed", {
|
|
169
|
+
void signalCancel(streams, currentBasin(ctx), sessionId, turnId ?? undefined).catch((error) => ctx.config.logger.warn("cancel signal failed", {
|
|
170
170
|
sessionId,
|
|
171
171
|
message: messageOf(error),
|
|
172
172
|
}));
|
|
@@ -209,21 +209,6 @@ async function probe(streams, timeoutMs) {
|
|
|
209
209
|
return false;
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
|
-
/**
|
|
213
|
-
* Creates a new Tenant's basin. Call it when the Tenant is created, after its schema commits;
|
|
214
|
-
* retries transient failures, then throws. A failure need not fail the creation: opening the
|
|
215
|
-
* Tenant repairs a missing basin.
|
|
216
|
-
*/
|
|
217
|
-
export async function createTenantStreams(streams, tenantId, options = {}) {
|
|
218
|
-
await withRetries(() => streams.ensureTenant(tenantId), options.attempts ?? 3);
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Deletes every basin of a Tenant and every stream in them. Call it when the Tenant is
|
|
222
|
-
* deleted. Idempotent; retries transient failures, then throws.
|
|
223
|
-
*/
|
|
224
|
-
export async function deleteTenantStreams(streams, tenantId, options = {}) {
|
|
225
|
-
await withRetries(() => streams.deleteTenant(tenantId, { allGenerations: true }), options.attempts ?? 5);
|
|
226
|
-
}
|
|
227
212
|
/** Close: stops the readers and relay; the observers were ended by `endAllStreams`. */
|
|
228
213
|
export async function closeStreams(ctx) {
|
|
229
214
|
await ctx.sessionStreams.wiring?.close();
|
package/dist/tenant/tokens.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `401 token_expired`, so clients always react the same way: get a new token.
|
|
9
9
|
*/
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
|
-
import { errors, jwtVerify
|
|
11
|
+
import { errors, jwtVerify } from "jose";
|
|
12
12
|
import { isSubject, SUBJECT_TOKEN_AUDIENCE, SUBJECT_TOKEN_TYPE, subjectTokenIssuer, TOKEN_SCOPES, TOKEN_TTL_MIN_SECONDS, } from "@nylorun/core/contracts";
|
|
13
13
|
import { agentAllowed, readPolicy, resolveRole } from "./access-policy.js";
|
|
14
14
|
import { fail, failOpaque, HttpError } from "./http.js";
|
|
@@ -29,9 +29,8 @@ function rejected(ctx, reason) {
|
|
|
29
29
|
return failOpaque();
|
|
30
30
|
}
|
|
31
31
|
export async function mintToken(ctx, body) {
|
|
32
|
-
const kek = ctx.signingKeys.kek();
|
|
33
32
|
const tenantId = ctx.config.tenantId;
|
|
34
|
-
const { access, epoch,
|
|
33
|
+
const { access, epoch, ttl } = await ctx.store.tx(async (t) => {
|
|
35
34
|
const policy = await readPolicy(t);
|
|
36
35
|
const role = Object.hasOwn(policy.roles, body.role)
|
|
37
36
|
? policy.roles[body.role]
|
|
@@ -61,11 +60,9 @@ export async function mintToken(ctx, body) {
|
|
|
61
60
|
return {
|
|
62
61
|
access: resolveRole(policy, body.role, scopes, body.agents),
|
|
63
62
|
epoch: await t.subjectEpoch(body.subject),
|
|
64
|
-
row: await ctx.signingKeys.ensure(t, kek),
|
|
65
63
|
ttl,
|
|
66
64
|
};
|
|
67
65
|
});
|
|
68
|
-
const signer = await ctx.signingKeys.privateKey(row, kek);
|
|
69
66
|
const iat = Math.floor(Date.now() / 1000);
|
|
70
67
|
const jti = randomUUID();
|
|
71
68
|
const scopes = [...access.scopes];
|
|
@@ -76,15 +73,20 @@ export async function mintToken(ctx, body) {
|
|
|
76
73
|
...(body.agents ? { agt: [...body.agents] } : {}),
|
|
77
74
|
epc: epoch,
|
|
78
75
|
};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
76
|
+
// Signed by the keys service (F4.2): this process never holds the private key.
|
|
77
|
+
const { token, keyId } = await ctx.keys.sign({
|
|
78
|
+
typ: SUBJECT_TOKEN_TYPE,
|
|
79
|
+
claims: {
|
|
80
|
+
...claims,
|
|
81
|
+
iss: subjectTokenIssuer(tenantId),
|
|
82
|
+
aud: SUBJECT_TOKEN_AUDIENCE,
|
|
83
|
+
sub: body.subject,
|
|
84
|
+
iat,
|
|
85
|
+
exp: iat + ttl,
|
|
86
|
+
jti,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
const signer = { id: keyId };
|
|
88
90
|
ctx.config.logger.info("subject token minted", {
|
|
89
91
|
jti,
|
|
90
92
|
kid: signer.id,
|
package/dist/tenant/types.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
import type {
|
|
2
|
+
import type { HostAggregate, HostTenant, TenantEnvelope } from "@nylorun/core/contracts";
|
|
3
3
|
import type { FlowLimits } from "../core/limits.js";
|
|
4
4
|
import type { SessionStore } from "../store/types.js";
|
|
5
5
|
import type { SandboxBackend } from "../sandbox/types.js";
|
|
6
|
+
import type { TenantCause } from "./cause.js";
|
|
6
7
|
import type { OutboundPolicy } from "./outbound.js";
|
|
7
8
|
import type { TenantWorker } from "./worker.js";
|
|
8
9
|
export type TenantMode = "shared" | "ephemeral" | "test";
|
|
9
10
|
/**
|
|
10
|
-
* The Tenant directory on the Host root. The Tenant's data lives in
|
|
11
|
-
*
|
|
11
|
+
* The Tenant directory on the Host root (`<Host root>/tenant/`). The Tenant's data lives in
|
|
12
|
+
* its database; this holds what stays on the Host: the vault key, plugin data, logs, and the
|
|
12
13
|
* private home, tmp and sandbox directories.
|
|
13
14
|
*/
|
|
14
15
|
export interface TenantPaths {
|
|
@@ -23,10 +24,6 @@ export interface TenantPaths {
|
|
|
23
24
|
}
|
|
24
25
|
export type TenantModelConfig = {
|
|
25
26
|
kind: "vault";
|
|
26
|
-
} | {
|
|
27
|
-
kind: "gateway";
|
|
28
|
-
url: string;
|
|
29
|
-
model: string;
|
|
30
27
|
} | {
|
|
31
28
|
kind: "fixture";
|
|
32
29
|
} | {
|
|
@@ -52,7 +49,7 @@ export interface TenantConfig {
|
|
|
52
49
|
ms?: number;
|
|
53
50
|
};
|
|
54
51
|
/** Retries and timeouts for model calls (Model Calls §5, §6). Defaults in `piModel`. */
|
|
55
|
-
modelCall?: import("../model
|
|
52
|
+
modelCall?: import("../gates/model-gate.js").ModelCallSettings;
|
|
56
53
|
childEnv: Readonly<Record<string, string>>;
|
|
57
54
|
/** Action claim lease. Default 30 s. */
|
|
58
55
|
/** How the Runtime may call Action endpoints (Host settings). Default: http and private addresses allowed. */
|
|
@@ -111,115 +108,67 @@ export interface TenantHandle {
|
|
|
111
108
|
drain(activeWork: "drain" | "cancel", timeoutMs?: number): Promise<void>;
|
|
112
109
|
close(): Promise<void>;
|
|
113
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* The Host's Tenant for a request: open, or unavailable. `cause` says why when opening it
|
|
113
|
+
* failed; without one the Host is starting or closing. Either way the request gets the opaque
|
|
114
|
+
* 404.
|
|
115
|
+
*/
|
|
114
116
|
export type TenantResolution = {
|
|
115
117
|
kind: "open";
|
|
116
118
|
handle: TenantHandle;
|
|
117
119
|
} | {
|
|
118
|
-
kind: "
|
|
119
|
-
|
|
120
|
-
kind: "quarantined";
|
|
121
|
-
quarantine: Quarantine;
|
|
120
|
+
kind: "unavailable";
|
|
121
|
+
cause?: TenantCause;
|
|
122
122
|
};
|
|
123
|
-
export interface Quarantine {
|
|
124
|
-
code: "kek-missing" | "corrupt" | "schema-too-new" | "migration-failed" | "envelope-invalid" | "open-timeout" | "open-failed";
|
|
125
|
-
message: string;
|
|
126
|
-
repair: string;
|
|
127
|
-
}
|
|
128
|
-
export interface BootstrapPrincipal {
|
|
129
|
-
principalId: string;
|
|
130
|
-
credentialHash: string;
|
|
131
|
-
idempotencyKey: string;
|
|
132
|
-
/** When set, also registers application principal `studio` with this hash. */
|
|
133
|
-
studioCredentialHash?: string;
|
|
134
|
-
/** Application principals whose keys the admin key derives (feature `derived-principals`). */
|
|
135
|
-
derivedPrincipals?: readonly DerivedPrincipal[];
|
|
136
|
-
}
|
|
137
|
-
export interface DerivedPrincipal {
|
|
138
|
-
id: string;
|
|
139
|
-
credentialHash: string;
|
|
140
|
-
}
|
|
141
123
|
/**
|
|
142
|
-
* The deep module (§8)
|
|
124
|
+
* The deep module (§8): the one Tenant a Host serves (tenancy.md §5). HTTP, the Worker and
|
|
125
|
+
* tests use only this.
|
|
143
126
|
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
127
|
+
* `start` opens the Tenant; the Host has it open from readiness to shutdown. There is no
|
|
128
|
+
* Tenant list, no opening on demand of other Tenants and no quarantine cache: when opening
|
|
129
|
+
* fails for a reason in the Tenant (a `TenantCause`), the Host fails readiness and reports
|
|
130
|
+
* the cause until it is restarted. A failure outside the Tenant (`TenantUnavailableError`:
|
|
131
|
+
* Postgres unreachable) is retried in the background and by the next request.
|
|
148
132
|
*/
|
|
149
133
|
export interface TenantModule {
|
|
150
|
-
/**
|
|
134
|
+
/** Opens the Tenant. Never rejects: a failure is recorded and reported by `tenant()`. */
|
|
151
135
|
start(): Promise<void>;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
resolve(id: string): Promise<TenantResolution>;
|
|
136
|
+
/** The Tenant is open (`/ready`). */
|
|
137
|
+
readonly ready: boolean;
|
|
155
138
|
/**
|
|
156
|
-
* The Tenant
|
|
157
|
-
*
|
|
158
|
-
* module is closed.
|
|
139
|
+
* The open Tenant, waiting for an open in progress. Rejects with `TenantUnavailableError`
|
|
140
|
+
* when it cannot be reached for a reason outside it.
|
|
159
141
|
*/
|
|
160
|
-
|
|
161
|
-
create(input: {
|
|
162
|
-
tenantId: string;
|
|
163
|
-
name: string;
|
|
164
|
-
} & BootstrapPrincipal): Promise<{
|
|
165
|
-
envelope: TenantEnvelope;
|
|
166
|
-
created: boolean;
|
|
167
|
-
}>;
|
|
142
|
+
resolve(): Promise<TenantResolution>;
|
|
168
143
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
144
|
+
* The Tenant's Worker handlers when `id` is the open Tenant's, for `TenantWorkers.resolve`.
|
|
145
|
+
* Undefined otherwise, or once the module is closed.
|
|
171
146
|
*/
|
|
172
|
-
|
|
173
|
-
status
|
|
174
|
-
|
|
147
|
+
worker(id: string): Promise<TenantWorker | undefined>;
|
|
148
|
+
/** The Tenant as `/v1/admin/status` reports it. */
|
|
149
|
+
tenant(): HostTenant;
|
|
175
150
|
summarize(): Promise<HostAggregate>;
|
|
176
151
|
close(): Promise<void>;
|
|
177
152
|
}
|
|
178
153
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
154
|
+
* Opens the Host's Tenant (`store-pg.ts`): migrates its database, creates the Tenant when it
|
|
155
|
+
* has none, and opens the Tenant Runtime. Rejects with a `TenantOpenError` (`cause.ts`) when
|
|
156
|
+
* the Tenant itself cannot be opened, and with `TenantUnavailableError` when something
|
|
157
|
+
* outside it failed.
|
|
181
158
|
*/
|
|
182
|
-
export
|
|
183
|
-
/** Ids of every Tenant the store holds. */
|
|
184
|
-
enumerate(): Promise<readonly string[]>;
|
|
185
|
-
/** Throws a quarantine error when unreadable, `TenantNotFoundError` when gone. */
|
|
186
|
-
readEnvelope(id: string): Promise<TenantEnvelope>;
|
|
187
|
-
/**
|
|
188
|
-
* Creates the Tenant, or returns `exists` when it already exists (whatever its bootstrap
|
|
189
|
-
* material; the module compares it with `bootstrapMatches`). The stored envelope's
|
|
190
|
-
* `schemaVersion` is the store's own; read it back with `readEnvelope`.
|
|
191
|
-
*/
|
|
192
|
-
create(envelope: TenantEnvelope, bootstrap: BootstrapPrincipal): Promise<"created" | "exists">;
|
|
193
|
-
bootstrapMatches(id: string, bootstrap: BootstrapPrincipal): Promise<boolean>;
|
|
194
|
-
/**
|
|
195
|
-
* Opens the Tenant Runtime; runs migration. Throws `TenantNotFoundError` when the Tenant
|
|
196
|
-
* does not exist, `TenantUnavailableError` when something outside the Tenant failed, and
|
|
197
|
-
* any other error (a quarantine error, ideally) when the Tenant itself cannot be opened.
|
|
198
|
-
*/
|
|
199
|
-
open(id: string): Promise<TenantHandle>;
|
|
200
|
-
/** Removes the Tenant: its data and its Tenant directory. */
|
|
201
|
-
trash(id: string, now: Date): Promise<void>;
|
|
202
|
-
/** Removes what a failed `create` left, never an existing Tenant. */
|
|
203
|
-
removePartial(id: string): Promise<void>;
|
|
204
|
-
}
|
|
159
|
+
export type TenantOpener = () => Promise<TenantHandle>;
|
|
205
160
|
/**
|
|
206
|
-
* Thrown
|
|
207
|
-
*
|
|
208
|
-
*
|
|
161
|
+
* Thrown when the Tenant cannot be reached for a reason outside it (the database or Durable
|
|
162
|
+
* Session Execution is unavailable). Not a cause: the next use tries again. The Host answers
|
|
163
|
+
* 503.
|
|
209
164
|
*/
|
|
210
165
|
export declare class TenantUnavailableError extends Error {
|
|
211
|
-
readonly tenantId: string;
|
|
212
166
|
readonly status = 503;
|
|
213
|
-
constructor(
|
|
167
|
+
constructor(options?: {
|
|
214
168
|
cause?: unknown;
|
|
215
169
|
});
|
|
216
170
|
}
|
|
217
|
-
/**
|
|
218
|
-
export declare class TenantNotFoundError extends Error {
|
|
219
|
-
readonly tenantId: string;
|
|
220
|
-
constructor(tenantId: string);
|
|
221
|
-
}
|
|
222
|
-
/** What a Tenant store hands the Tenant Runtime it opens. */
|
|
171
|
+
/** What the Tenant opener hands the Tenant Runtime it opens. */
|
|
223
172
|
export interface OpenedTenant {
|
|
224
173
|
/**
|
|
225
174
|
* The Tenant's opened Session Store. The Tenant Runtime owns it from here on and closes it
|
|
@@ -229,5 +178,5 @@ export interface OpenedTenant {
|
|
|
229
178
|
/** The Tenant envelope as its store reports it. */
|
|
230
179
|
envelope: TenantEnvelope;
|
|
231
180
|
}
|
|
232
|
-
/** Opens the Tenant Runtime on
|
|
181
|
+
/** Opens the Tenant Runtime on an opened store (injected, so tests can fake it). */
|
|
233
182
|
export type OpenTenantRuntime = (config: TenantConfig, opened: OpenedTenant) => Promise<TenantHandle>;
|
package/dist/tenant/types.js
CHANGED
|
@@ -1,23 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Thrown
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Thrown when the Tenant cannot be reached for a reason outside it (the database or Durable
|
|
3
|
+
* Session Execution is unavailable). Not a cause: the next use tries again. The Host answers
|
|
4
|
+
* 503.
|
|
5
5
|
*/
|
|
6
6
|
export class TenantUnavailableError extends Error {
|
|
7
|
-
tenantId;
|
|
8
7
|
status = 503;
|
|
9
|
-
constructor(
|
|
8
|
+
constructor(options) {
|
|
10
9
|
super("Tenant is temporarily unavailable", options);
|
|
11
|
-
this.tenantId = tenantId;
|
|
12
10
|
this.name = "TenantUnavailableError";
|
|
13
11
|
}
|
|
14
12
|
}
|
|
15
|
-
/** Thrown by `TenantStore.open` for a Tenant that does not exist. */
|
|
16
|
-
export class TenantNotFoundError extends Error {
|
|
17
|
-
tenantId;
|
|
18
|
-
constructor(tenantId) {
|
|
19
|
-
super(`Tenant ${tenantId} not found`);
|
|
20
|
-
this.tenantId = tenantId;
|
|
21
|
-
this.name = "TenantNotFoundError";
|
|
22
|
-
}
|
|
23
|
-
}
|
package/dist/tenant/worker.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Worker side of Durable Session Execution (architecture §12.3): what a `DurableExecution`
|
|
3
|
-
* calls for
|
|
4
|
-
* this process.
|
|
3
|
+
* calls for the Tenant, and the registry its handlers dispatch through by Tenant id.
|
|
5
4
|
*
|
|
6
5
|
* - `TenantWorker` is one open Tenant's handlers: `advance(sessionId, signal)` (§10.5, in
|
|
7
6
|
* `advance.ts`), `sweep()` (the Tenant sweep, in `sweep.ts`) and, optionally,
|
|
@@ -9,12 +8,13 @@
|
|
|
9
8
|
* - `TenantWorkers` is the registry. Its `handlers` are the `WorkerHandlers` an execution is
|
|
10
9
|
* started with; they dispatch by `tenantId` to the registered `TenantWorker`. A Tenant
|
|
11
10
|
* registers when it opens and unregisters when it closes. A call for a Tenant that is not
|
|
12
|
-
* open here asks the optional `resolve` hook (the Host
|
|
11
|
+
* open here asks the optional `resolve` hook (the Host's, which waits for its Tenant to
|
|
12
|
+
* open, and knows no other); without one,
|
|
13
13
|
* the advance and the delivery return `done` and the sweep does nothing. Nothing is lost:
|
|
14
14
|
* the Tenant's sweep re-wakes its orphaned sessions once it is open again, and an Action
|
|
15
15
|
* that was not delivered stays pending.
|
|
16
16
|
* - `TenantExecution` pairs an execution with its registry. The Host creates one per process
|
|
17
|
-
* (`host/execution.ts`) and passes it to
|
|
17
|
+
* (`host/execution.ts`) and passes it to its Tenant; a Tenant opened without one
|
|
18
18
|
* (tests, ephemeral) gets its own in-process `MemoryExecution`.
|
|
19
19
|
*
|
|
20
20
|
* ## Advance deadline
|