@nylorun/runtime 0.9.0-beta → 0.11.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 +81 -0
- package/README.md +110 -37
- package/dist/adapters/execution/restate.d.ts +130 -0
- package/dist/adapters/execution/restate.js +442 -0
- package/dist/adapters/media.d.ts +2 -2
- package/dist/adapters/media.js +2 -2
- package/dist/adapters/observe.d.ts +2 -1
- package/dist/adapters/observe.js +2 -3
- package/dist/adapters/sandbox/virtual.js +1 -1
- package/dist/adapters/streams/s2.d.ts +18 -0
- package/dist/adapters/streams/s2.js +361 -0
- package/dist/assets.d.ts +2 -2
- package/dist/assets.js +5 -3
- package/dist/core/executors.d.ts +15 -3
- package/dist/core/executors.js +40 -5
- package/dist/core/flow-host.d.ts +225 -0
- package/dist/core/flow-host.js +518 -0
- package/dist/core/limits.d.ts +40 -0
- package/dist/core/limits.js +76 -0
- package/dist/core/runtime.d.ts +5 -90
- package/dist/core/runtime.js +3 -1447
- package/dist/core/sandbox-routes.d.ts +40 -0
- package/dist/core/sandbox-routes.js +106 -0
- package/dist/core/turn-manifest.d.ts +50 -0
- package/dist/core/turn-manifest.js +66 -0
- package/dist/execution/memory.d.ts +54 -0
- package/dist/execution/memory.js +217 -0
- package/dist/execution/types.d.ts +125 -0
- package/dist/execution/types.js +52 -0
- package/dist/host/config-for.d.ts +17 -0
- package/dist/host/config-for.js +27 -0
- package/dist/host/config.d.ts +16 -0
- package/dist/host/create-host.d.ts +56 -0
- package/dist/host/create-host.js +420 -0
- package/dist/host/environment.d.ts +13 -0
- package/dist/host/environment.js +53 -0
- package/dist/host/execution.d.ts +61 -0
- package/dist/host/execution.js +69 -0
- package/dist/host/http.d.ts +39 -0
- package/dist/host/http.js +142 -0
- package/dist/host/index.d.ts +8 -0
- package/dist/host/index.js +7 -0
- package/dist/host/logger.d.ts +3 -0
- package/dist/host/logger.js +18 -0
- package/dist/host/main.d.ts +3 -0
- package/dist/host/main.js +201 -0
- package/dist/host/stack-config.d.ts +72 -0
- package/dist/host/stack-config.js +193 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +4 -2
- package/dist/infra/database.d.ts +16 -0
- package/dist/infra/database.js +31 -0
- package/dist/infra/execution.d.ts +39 -0
- package/dist/infra/execution.js +68 -0
- package/dist/infra/index.d.ts +42 -0
- package/dist/infra/index.js +56 -0
- package/dist/infra/readiness.d.ts +33 -0
- package/dist/infra/readiness.js +53 -0
- package/dist/infra/streams.d.ts +17 -0
- package/dist/infra/streams.js +33 -0
- package/dist/infra/workers.d.ts +39 -0
- package/dist/infra/workers.js +31 -0
- package/dist/mcp/connect.d.ts +1 -0
- package/dist/mcp/connect.js +2 -0
- package/dist/mcp/pool.d.ts +3 -1
- package/dist/mcp/pool.js +2 -1
- package/dist/mcp/stdio.d.ts +2 -0
- package/dist/mcp/stdio.js +3 -1
- package/dist/model/auth-store.d.ts +1 -1
- package/dist/model/auth-store.js +2 -2
- package/dist/model/models.d.ts +4 -0
- package/dist/model/models.js +22 -26
- package/dist/model/pi-model.d.ts +2 -2
- package/dist/model/pi-model.js +6 -6
- package/dist/model/settings.d.ts +5 -1
- package/dist/model/settings.js +6 -2
- package/dist/sandbox/manager.d.ts +26 -8
- package/dist/sandbox/manager.js +95 -47
- package/dist/sandbox/select.d.ts +5 -4
- package/dist/sandbox/select.js +18 -13
- package/dist/sandbox/share.d.ts +23 -0
- package/dist/sandbox/share.js +58 -0
- package/dist/sandbox/types.d.ts +2 -2
- package/dist/store/canonical.d.ts +5 -0
- package/dist/store/canonical.js +17 -0
- package/dist/store/cursor.d.ts +9 -0
- package/dist/store/cursor.js +18 -0
- package/dist/store/memory.d.ts +57 -0
- package/dist/store/memory.js +643 -0
- package/dist/store/ownership.d.ts +19 -0
- package/dist/store/ownership.js +33 -0
- package/dist/store/postgres/connect.d.ts +25 -0
- package/dist/store/postgres/connect.js +25 -0
- package/dist/store/postgres/locking.d.ts +16 -0
- package/dist/store/postgres/locking.js +22 -0
- package/dist/store/postgres/migrations/001_initial.d.ts +27 -0
- package/dist/store/postgres/migrations/001_initial.js +196 -0
- package/dist/store/postgres/migrations/index.d.ts +49 -0
- package/dist/store/postgres/migrations/index.js +75 -0
- package/dist/store/postgres/names.d.ts +11 -0
- package/dist/store/postgres/names.js +42 -0
- package/dist/store/postgres/store.d.ts +11 -0
- package/dist/store/postgres/store.js +790 -0
- package/dist/store/postgres/tenants.d.ts +90 -0
- package/dist/store/postgres/tenants.js +207 -0
- package/dist/store/types.d.ts +440 -0
- package/dist/store/types.js +10 -0
- package/dist/streams/basin.d.ts +23 -0
- package/dist/streams/basin.js +51 -0
- package/dist/streams/memory.d.ts +25 -0
- package/dist/streams/memory.js +124 -0
- package/dist/streams/relay.d.ts +37 -0
- package/dist/streams/relay.js +254 -0
- package/dist/streams/types.d.ts +115 -0
- package/dist/streams/types.js +86 -0
- package/dist/tenant/actions.d.ts +153 -0
- package/dist/tenant/actions.js +191 -0
- package/dist/tenant/advance.d.ts +15 -0
- package/dist/tenant/advance.js +439 -0
- package/dist/tenant/auth.d.ts +11 -0
- package/dist/tenant/auth.js +34 -0
- package/dist/tenant/commands.d.ts +3 -0
- package/dist/tenant/commands.js +384 -0
- package/dist/tenant/context.d.ts +140 -0
- package/dist/tenant/context.js +40 -0
- package/dist/tenant/effects.d.ts +20 -0
- package/dist/tenant/effects.js +539 -0
- package/dist/tenant/envelope.d.ts +7 -0
- package/dist/tenant/envelope.js +22 -0
- package/dist/tenant/ephemeral.d.ts +42 -0
- package/dist/tenant/ephemeral.js +224 -0
- package/dist/tenant/http.d.ts +32 -0
- package/dist/tenant/http.js +51 -0
- package/dist/tenant/live.d.ts +92 -0
- package/dist/tenant/live.js +290 -0
- package/dist/tenant/logger.d.ts +9 -0
- package/dist/tenant/logger.js +25 -0
- package/dist/tenant/model-setting.d.ts +16 -0
- package/dist/tenant/model-setting.js +12 -0
- package/dist/tenant/module.d.ts +38 -0
- package/dist/tenant/module.js +319 -0
- package/dist/tenant/paths.d.ts +28 -0
- package/dist/tenant/paths.js +91 -0
- package/dist/tenant/pool.d.ts +10 -0
- package/dist/tenant/pool.js +54 -0
- package/dist/tenant/principals.d.ts +13 -0
- package/dist/tenant/principals.js +37 -0
- package/dist/tenant/quarantine-error.d.ts +6 -0
- package/dist/tenant/quarantine-error.js +9 -0
- package/dist/tenant/quarantine.d.ts +27 -0
- package/dist/tenant/quarantine.js +72 -0
- package/dist/tenant/reset.d.ts +22 -0
- package/dist/tenant/reset.js +81 -0
- package/dist/tenant/routes-tenant.d.ts +12 -0
- package/dist/tenant/routes-tenant.js +126 -0
- package/dist/tenant/routes.d.ts +9 -0
- package/dist/tenant/routes.js +142 -0
- package/dist/tenant/runtime.d.ts +81 -0
- package/dist/tenant/runtime.js +298 -0
- package/dist/tenant/scheduler.d.ts +25 -0
- package/dist/tenant/scheduler.js +84 -0
- package/dist/tenant/session.d.ts +60 -0
- package/dist/tenant/session.js +94 -0
- package/dist/tenant/sessions.d.ts +34 -0
- package/dist/tenant/sessions.js +149 -0
- package/dist/tenant/status.d.ts +37 -0
- package/dist/tenant/status.js +130 -0
- package/dist/tenant/store-memory.d.ts +16 -0
- package/dist/tenant/store-memory.js +111 -0
- package/dist/tenant/store-pg.d.ts +15 -0
- package/dist/tenant/store-pg.js +140 -0
- package/dist/tenant/streams.d.ts +126 -0
- package/dist/tenant/streams.js +419 -0
- package/dist/tenant/sweep.d.ts +15 -0
- package/dist/tenant/sweep.js +122 -0
- package/dist/tenant/types.d.ts +205 -0
- package/dist/tenant/types.js +23 -0
- package/dist/tenant/worker.d.ts +84 -0
- package/dist/tenant/worker.js +189 -0
- package/dist/vault/kek.d.ts +5 -2
- package/dist/vault/kek.js +7 -10
- package/dist/vault/service.d.ts +37 -31
- package/dist/vault/service.js +439 -359
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +14 -11
- package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
- package/dist/adapters/sandbox/microsandbox.js +0 -238
- package/dist/core/main.js +0 -35
- package/dist/core/store.d.ts +0 -30
- package/dist/core/store.js +0 -202
- /package/dist/{core/main.d.ts → host/config.js} +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Action } from "@nylorun/core/contracts";
|
|
2
|
+
import type { SandboxManager } from "../sandbox/manager.js";
|
|
3
|
+
import type { SandboxToolOutcome } from "../sandbox/tools.js";
|
|
4
|
+
import { type SessionSandboxRef } from "../sandbox/share.js";
|
|
5
|
+
export declare class SandboxRouteError extends Error {
|
|
6
|
+
readonly status: number;
|
|
7
|
+
constructor(status: number, message: string);
|
|
8
|
+
}
|
|
9
|
+
export type SandboxRouteSession = SessionSandboxRef & {
|
|
10
|
+
readonly activeTurnId: string | null;
|
|
11
|
+
readonly status: string;
|
|
12
|
+
};
|
|
13
|
+
/** A session together with a synchronous lookup over its sandbox owner chain. */
|
|
14
|
+
export type SandboxRouteRead = {
|
|
15
|
+
readonly session: SandboxRouteSession;
|
|
16
|
+
readonly lookup: (id: string) => SessionSandboxRef | undefined;
|
|
17
|
+
};
|
|
18
|
+
export type SandboxRouteDeps = {
|
|
19
|
+
readonly sandbox: SandboxManager;
|
|
20
|
+
/** The session (rejects when missing) and its owner chain, read in one transaction. */
|
|
21
|
+
readonly session: (id: string) => Promise<SandboxRouteRead>;
|
|
22
|
+
readonly getAction: (id: string) => Promise<Action | undefined>;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* `POST /v1/sessions/:id/sandbox/:tool` — application principal.
|
|
26
|
+
* Refused while the session has an active agent turn.
|
|
27
|
+
*/
|
|
28
|
+
export declare function handleSessionSandboxTool(deps: SandboxRouteDeps, sessionId: string, toolName: string, body: unknown, signal: AbortSignal): Promise<SandboxToolOutcome>;
|
|
29
|
+
/**
|
|
30
|
+
* `POST /v1/actions/:id/sandbox/:tool` — executor, authorized by the live claim.
|
|
31
|
+
* Body must include claimId and generation matching the claimed action.
|
|
32
|
+
*/
|
|
33
|
+
export declare function handleActionSandboxTool(deps: SandboxRouteDeps, actionId: string, toolName: string, body: unknown, signal: AbortSignal): Promise<SandboxToolOutcome>;
|
|
34
|
+
/** Validate PutSession.sandbox attach: same owner, identical specs. */
|
|
35
|
+
export declare function validateSandboxAttach(body: {
|
|
36
|
+
ownerUserId: string;
|
|
37
|
+
sandbox?: {
|
|
38
|
+
session: string;
|
|
39
|
+
};
|
|
40
|
+
}, newManifest: SessionSandboxRef["manifest"], lookup: (id: string) => SessionSandboxRef | undefined): string | undefined;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP handlers for session-scoped and claim-scoped sandbox tool endpoints.
|
|
3
|
+
* Wired from TenantRuntime; keeps route bodies out of the hot runtime file.
|
|
4
|
+
*/
|
|
5
|
+
import { isSandboxToolName, } from "@nylorun/core/define";
|
|
6
|
+
import { capabilityForSandbox, owningSandboxSessionId, sandboxSpecOf, sandboxSpecsEqual, } from "../sandbox/share.js";
|
|
7
|
+
export class SandboxRouteError extends Error {
|
|
8
|
+
status;
|
|
9
|
+
constructor(status, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.name = "SandboxRouteError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function resolveSpec(session, lookup) {
|
|
16
|
+
const ownerId = owningSandboxSessionId(session, lookup);
|
|
17
|
+
const owner = ownerId === session.id ? session : lookup(ownerId) ?? session;
|
|
18
|
+
const spec = sandboxSpecOf(owner.manifest) ?? sandboxSpecOf(session.manifest);
|
|
19
|
+
if (!spec)
|
|
20
|
+
throw new SandboxRouteError(404, "Session has no sandbox");
|
|
21
|
+
return spec;
|
|
22
|
+
}
|
|
23
|
+
function parseTool(tool) {
|
|
24
|
+
if (!isSandboxToolName(tool))
|
|
25
|
+
throw new SandboxRouteError(400, `Unknown sandbox tool '${tool}'`);
|
|
26
|
+
return tool;
|
|
27
|
+
}
|
|
28
|
+
function toolInput(body) {
|
|
29
|
+
if (body === undefined || body === null)
|
|
30
|
+
return {};
|
|
31
|
+
if (typeof body !== "object" || Array.isArray(body))
|
|
32
|
+
throw new SandboxRouteError(400, "Sandbox tool body must be an object");
|
|
33
|
+
const record = body;
|
|
34
|
+
const { claimId: _c, generation: _g, requestId: _r, ...input } = record;
|
|
35
|
+
return input;
|
|
36
|
+
}
|
|
37
|
+
async function runTool(deps, { session, lookup }, tool, input, signal) {
|
|
38
|
+
const spec = resolveSpec(session, lookup);
|
|
39
|
+
const ownerId = owningSandboxSessionId(session, lookup);
|
|
40
|
+
const ref = {
|
|
41
|
+
id: ownerId,
|
|
42
|
+
activeTurnId: session.activeTurnId,
|
|
43
|
+
manifest: session.manifest,
|
|
44
|
+
};
|
|
45
|
+
return deps.sandbox.run(ref, capabilityForSandbox(spec), tool, input, signal);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `POST /v1/sessions/:id/sandbox/:tool` — application principal.
|
|
49
|
+
* Refused while the session has an active agent turn.
|
|
50
|
+
*/
|
|
51
|
+
export async function handleSessionSandboxTool(deps, sessionId, toolName, body, signal) {
|
|
52
|
+
const read = await deps.session(sessionId);
|
|
53
|
+
if (read.session.activeTurnId)
|
|
54
|
+
throw new SandboxRouteError(409, "Sandbox is unavailable while the session has an active agent turn");
|
|
55
|
+
const tool = parseTool(toolName);
|
|
56
|
+
return runTool(deps, read, tool, toolInput(body), signal);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* `POST /v1/actions/:id/sandbox/:tool` — executor, authorized by the live claim.
|
|
60
|
+
* Body must include claimId and generation matching the claimed action.
|
|
61
|
+
*/
|
|
62
|
+
export async function handleActionSandboxTool(deps, actionId, toolName, body, signal) {
|
|
63
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
64
|
+
throw new SandboxRouteError(400, "Sandbox tool body must be an object");
|
|
65
|
+
const record = body;
|
|
66
|
+
const claimId = record.claimId;
|
|
67
|
+
const generation = record.generation;
|
|
68
|
+
if (typeof claimId !== "string" || claimId.length === 0)
|
|
69
|
+
throw new SandboxRouteError(400, "claimId is required");
|
|
70
|
+
if (typeof generation !== "number" || !Number.isInteger(generation) || generation < 1)
|
|
71
|
+
throw new SandboxRouteError(400, "generation must be a positive integer");
|
|
72
|
+
const action = await deps.getAction(actionId);
|
|
73
|
+
if (!action)
|
|
74
|
+
throw new SandboxRouteError(404, "Action not found");
|
|
75
|
+
if (action.status !== "claimed" ||
|
|
76
|
+
action.claimId !== claimId ||
|
|
77
|
+
action.generation !== generation ||
|
|
78
|
+
!action.leaseExpiresAt ||
|
|
79
|
+
Date.parse(action.leaseExpiresAt) <= Date.now())
|
|
80
|
+
throw new SandboxRouteError(409, "Stale or expired claim");
|
|
81
|
+
const read = await deps.session(action.sessionId);
|
|
82
|
+
if (read.session.activeTurnId !== action.turnId)
|
|
83
|
+
throw new SandboxRouteError(409, "Action unavailable");
|
|
84
|
+
const tool = parseTool(toolName);
|
|
85
|
+
return runTool(deps, read, tool, toolInput(body), signal);
|
|
86
|
+
}
|
|
87
|
+
/** Validate PutSession.sandbox attach: same owner, identical specs. */
|
|
88
|
+
export function validateSandboxAttach(body, newManifest, lookup) {
|
|
89
|
+
const share = body.sandbox?.session;
|
|
90
|
+
if (!share)
|
|
91
|
+
return undefined;
|
|
92
|
+
const owner = lookup(share);
|
|
93
|
+
if (!owner)
|
|
94
|
+
throw new SandboxRouteError(404, "Sandbox session not found");
|
|
95
|
+
if (owner.ownerUserId !== body.ownerUserId)
|
|
96
|
+
throw new SandboxRouteError(403, "Sandbox session must belong to the same owner");
|
|
97
|
+
const ownerSpec = sandboxSpecOf(owner.manifest);
|
|
98
|
+
const newSpec = sandboxSpecOf(newManifest);
|
|
99
|
+
if (!ownerSpec)
|
|
100
|
+
throw new SandboxRouteError(400, "Target session has no sandbox to share");
|
|
101
|
+
if (!newSpec)
|
|
102
|
+
throw new SandboxRouteError(400, "Session must declare a sandbox identical to the shared session");
|
|
103
|
+
if (!sandboxSpecsEqual(ownerSpec, newSpec))
|
|
104
|
+
throw new SandboxRouteError(409, "Sandbox specs must be identical to share a sandbox");
|
|
105
|
+
return owningSandboxSessionId(owner, lookup);
|
|
106
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type AgentManifest } from "@nylorun/core/contracts";
|
|
2
|
+
export type TurnManifestStore = {
|
|
3
|
+
/** Look up a previously pinned turn variant by hash. */
|
|
4
|
+
get(hash: string): AgentManifest | undefined;
|
|
5
|
+
/** Persist a validated turn variant for replay / Studio. */
|
|
6
|
+
put(hash: string, manifest: AgentManifest): void;
|
|
7
|
+
};
|
|
8
|
+
export type TurnManifestOk = {
|
|
9
|
+
readonly ok: true;
|
|
10
|
+
readonly manifest: AgentManifest;
|
|
11
|
+
readonly hash: string;
|
|
12
|
+
};
|
|
13
|
+
export type TurnManifestErr = {
|
|
14
|
+
readonly ok: false;
|
|
15
|
+
readonly code: "loop.invalid-agent";
|
|
16
|
+
readonly message: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Parse `message.manifest`, require it to be a variant of the session pin,
|
|
20
|
+
* hash it, and store it. Applies to that turn only (caller must not latch).
|
|
21
|
+
*/
|
|
22
|
+
export declare function pinTurnManifest(args: {
|
|
23
|
+
readonly candidate: unknown;
|
|
24
|
+
readonly pinned: AgentManifest;
|
|
25
|
+
readonly store: TurnManifestStore;
|
|
26
|
+
}): TurnManifestOk | TurnManifestErr;
|
|
27
|
+
/** Resolve the manifest a turn runs with: optional message field, else the pin. */
|
|
28
|
+
export declare function resolveMessageManifest(args: {
|
|
29
|
+
readonly pinned: AgentManifest;
|
|
30
|
+
readonly pinnedHash: string;
|
|
31
|
+
readonly messageManifest?: unknown;
|
|
32
|
+
readonly store: TurnManifestStore;
|
|
33
|
+
}): TurnManifestOk | TurnManifestErr;
|
|
34
|
+
/**
|
|
35
|
+
* At turn start: if state's hash differs from the turn's, set `state.manifestHash`
|
|
36
|
+
* to the turn's when both are the pin or validated variants (`loops.md` §4.2).
|
|
37
|
+
* Leaves state unchanged when either hash is unrelated (runDurable still rejects).
|
|
38
|
+
*/
|
|
39
|
+
export declare function rebaseTurnState<S extends {
|
|
40
|
+
manifestHash?: string;
|
|
41
|
+
}>(args: {
|
|
42
|
+
readonly state: S | undefined;
|
|
43
|
+
readonly turnManifestHash: string;
|
|
44
|
+
readonly isAllowedHash: (hash: string) => boolean;
|
|
45
|
+
}): S | undefined;
|
|
46
|
+
/** Allowed hashes: the session pin plus every stored turn variant. */
|
|
47
|
+
export declare function allowedManifestHashes(args: {
|
|
48
|
+
readonly pinnedHash: string;
|
|
49
|
+
readonly variantHashes: Iterable<string>;
|
|
50
|
+
}): Set<string>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { AgentManifestSchema, } from "@nylorun/core/contracts";
|
|
2
|
+
import { hashManifest } from "@nylorun/core/compatibility";
|
|
3
|
+
import { isVariantOf } from "@nylorun/core/define";
|
|
4
|
+
/**
|
|
5
|
+
* Parse `message.manifest`, require it to be a variant of the session pin,
|
|
6
|
+
* hash it, and store it. Applies to that turn only (caller must not latch).
|
|
7
|
+
*/
|
|
8
|
+
export function pinTurnManifest(args) {
|
|
9
|
+
const parsed = AgentManifestSchema.safeParse(args.candidate);
|
|
10
|
+
if (!parsed.success) {
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
code: "loop.invalid-agent",
|
|
14
|
+
message: "Turn manifest failed AgentManifestSchema validation",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const manifest = parsed.data;
|
|
18
|
+
if (!isVariantOf(manifest, args.pinned)) {
|
|
19
|
+
return {
|
|
20
|
+
ok: false,
|
|
21
|
+
code: "loop.invalid-agent",
|
|
22
|
+
message: "Turn manifest is not a variant of the session's pinned manifest",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const hash = hashManifest(manifest);
|
|
26
|
+
args.store.put(hash, manifest);
|
|
27
|
+
return { ok: true, manifest, hash };
|
|
28
|
+
}
|
|
29
|
+
/** Resolve the manifest a turn runs with: optional message field, else the pin. */
|
|
30
|
+
export function resolveMessageManifest(args) {
|
|
31
|
+
if (args.messageManifest === undefined) {
|
|
32
|
+
return {
|
|
33
|
+
ok: true,
|
|
34
|
+
manifest: args.pinned,
|
|
35
|
+
hash: args.pinnedHash,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return pinTurnManifest({
|
|
39
|
+
candidate: args.messageManifest,
|
|
40
|
+
pinned: args.pinned,
|
|
41
|
+
store: args.store,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* At turn start: if state's hash differs from the turn's, set `state.manifestHash`
|
|
46
|
+
* to the turn's when both are the pin or validated variants (`loops.md` §4.2).
|
|
47
|
+
* Leaves state unchanged when either hash is unrelated (runDurable still rejects).
|
|
48
|
+
*/
|
|
49
|
+
export function rebaseTurnState(args) {
|
|
50
|
+
const state = args.state;
|
|
51
|
+
if (!state?.manifestHash)
|
|
52
|
+
return state;
|
|
53
|
+
if (state.manifestHash === args.turnManifestHash)
|
|
54
|
+
return state;
|
|
55
|
+
if (!args.isAllowedHash(state.manifestHash) ||
|
|
56
|
+
!args.isAllowedHash(args.turnManifestHash))
|
|
57
|
+
return state;
|
|
58
|
+
return { ...state, manifestHash: args.turnManifestHash };
|
|
59
|
+
}
|
|
60
|
+
/** Allowed hashes: the session pin plus every stored turn variant. */
|
|
61
|
+
export function allowedManifestHashes(args) {
|
|
62
|
+
const set = new Set([args.pinnedHash]);
|
|
63
|
+
for (const hash of args.variantHashes)
|
|
64
|
+
set.add(hash);
|
|
65
|
+
return set;
|
|
66
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type DurableExecution, type Wake, type WorkerHandlers } from "./types.js";
|
|
2
|
+
export interface MemoryExecutionOptions {
|
|
3
|
+
/** Delay between sweep passes of one Tenant. Default 5000. */
|
|
4
|
+
sweepIntervalMs?: number;
|
|
5
|
+
/** First retry delay after `advance` throws; doubles per attempt. Default 50. */
|
|
6
|
+
retryDelayMs?: number;
|
|
7
|
+
/** Attempts before giving up on a throwing `advance` and reporting it. Default 5. */
|
|
8
|
+
maxAttempts?: number;
|
|
9
|
+
/** How long a dedupe key merges repeated wakes. Default 24 hours. */
|
|
10
|
+
dedupeRetentionMs?: number;
|
|
11
|
+
/** Receives handler failures that are not retried. */
|
|
12
|
+
onError?: (error: unknown) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* In-process `DurableExecution`. Not durable: wakes, timers and sweeps live in
|
|
16
|
+
* memory. It keeps the seam's ordering guarantees within one process, which is
|
|
17
|
+
* what unit tests and the Runtime before Restate (Wave 2) need.
|
|
18
|
+
*/
|
|
19
|
+
export declare class MemoryExecution implements DurableExecution {
|
|
20
|
+
private handlers?;
|
|
21
|
+
private readonly keys;
|
|
22
|
+
/** Dedupe keys seen, with the time each was first seen; insertion order is age order. */
|
|
23
|
+
private readonly seen;
|
|
24
|
+
private readonly timers;
|
|
25
|
+
private readonly sweeps;
|
|
26
|
+
private readonly delayed;
|
|
27
|
+
private readonly inflight;
|
|
28
|
+
private readonly sweepIntervalMs;
|
|
29
|
+
private readonly retryDelayMs;
|
|
30
|
+
private readonly maxAttempts;
|
|
31
|
+
private readonly dedupeRetentionMs;
|
|
32
|
+
private readonly onError;
|
|
33
|
+
constructor(options?: MemoryExecutionOptions);
|
|
34
|
+
wake(tenantId: string, sessionId: string, wake: Wake): Promise<void>;
|
|
35
|
+
timer(tenantId: string, key: string, at: Date): Promise<void>;
|
|
36
|
+
armSweep(tenantId: string): Promise<void>;
|
|
37
|
+
disarmSweep(tenantId: string): Promise<void>;
|
|
38
|
+
start(handlers: WorkerHandlers): Promise<void>;
|
|
39
|
+
stop(): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Resolves once no advance is running or queued and no busy or retry
|
|
42
|
+
* re-wake is pending. Timers and sweeps are not waited for. Test helper.
|
|
43
|
+
*/
|
|
44
|
+
idle(): Promise<void>;
|
|
45
|
+
private enqueue;
|
|
46
|
+
private drain;
|
|
47
|
+
/** Drops dedupe keys older than the retention window. */
|
|
48
|
+
private forgetDedupeKeys;
|
|
49
|
+
private advanceOnce;
|
|
50
|
+
private armTimer;
|
|
51
|
+
private scheduleSweep;
|
|
52
|
+
private later;
|
|
53
|
+
private track;
|
|
54
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { WAKE_REASONS, sessionKey, } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* In-process `DurableExecution`. Not durable: wakes, timers and sweeps live in
|
|
4
|
+
* memory. It keeps the seam's ordering guarantees within one process, which is
|
|
5
|
+
* what unit tests and the Runtime before Restate (Wave 2) need.
|
|
6
|
+
*/
|
|
7
|
+
export class MemoryExecution {
|
|
8
|
+
handlers;
|
|
9
|
+
keys = new Map();
|
|
10
|
+
/** Dedupe keys seen, with the time each was first seen; insertion order is age order. */
|
|
11
|
+
seen = new Map();
|
|
12
|
+
timers = new Map();
|
|
13
|
+
sweeps = new Map();
|
|
14
|
+
delayed = new Set();
|
|
15
|
+
inflight = new Set();
|
|
16
|
+
sweepIntervalMs;
|
|
17
|
+
retryDelayMs;
|
|
18
|
+
maxAttempts;
|
|
19
|
+
dedupeRetentionMs;
|
|
20
|
+
onError;
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
this.sweepIntervalMs = options.sweepIntervalMs ?? 5000;
|
|
23
|
+
this.retryDelayMs = options.retryDelayMs ?? 50;
|
|
24
|
+
this.maxAttempts = options.maxAttempts ?? 5;
|
|
25
|
+
this.dedupeRetentionMs = options.dedupeRetentionMs ?? 24 * 60 * 60_000;
|
|
26
|
+
this.onError =
|
|
27
|
+
options.onError ??
|
|
28
|
+
((error) => queueMicrotask(() => {
|
|
29
|
+
throw error;
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
async wake(tenantId, sessionId, wake) {
|
|
33
|
+
if (!WAKE_REASONS.includes(wake.reason))
|
|
34
|
+
throw new Error(`Unknown wake reason: ${String(wake.reason)}`);
|
|
35
|
+
const key = sessionKey(tenantId, sessionId);
|
|
36
|
+
if (wake.dedupeKey !== undefined) {
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
this.forgetDedupeKeys(now);
|
|
39
|
+
const dedupe = `${key}\u0000${wake.dedupeKey}`;
|
|
40
|
+
if (this.seen.has(dedupe))
|
|
41
|
+
return;
|
|
42
|
+
this.seen.set(dedupe, now);
|
|
43
|
+
}
|
|
44
|
+
this.enqueue(tenantId, sessionId);
|
|
45
|
+
}
|
|
46
|
+
async timer(tenantId, key, at) {
|
|
47
|
+
const id = `${tenantId}\u0000${key}`;
|
|
48
|
+
const existing = this.timers.get(id);
|
|
49
|
+
if (existing?.handle)
|
|
50
|
+
clearTimeout(existing.handle);
|
|
51
|
+
const timer = { tenantId, key, at: at.getTime() };
|
|
52
|
+
this.timers.set(id, timer);
|
|
53
|
+
if (this.handlers)
|
|
54
|
+
this.armTimer(id, timer);
|
|
55
|
+
}
|
|
56
|
+
async armSweep(tenantId) {
|
|
57
|
+
if (this.sweeps.has(tenantId))
|
|
58
|
+
return;
|
|
59
|
+
const sweep = { running: false };
|
|
60
|
+
this.sweeps.set(tenantId, sweep);
|
|
61
|
+
if (this.handlers)
|
|
62
|
+
this.scheduleSweep(tenantId, sweep, 0);
|
|
63
|
+
}
|
|
64
|
+
async disarmSweep(tenantId) {
|
|
65
|
+
const sweep = this.sweeps.get(tenantId);
|
|
66
|
+
if (sweep?.handle)
|
|
67
|
+
clearTimeout(sweep.handle);
|
|
68
|
+
this.sweeps.delete(tenantId);
|
|
69
|
+
}
|
|
70
|
+
async start(handlers) {
|
|
71
|
+
if (this.handlers)
|
|
72
|
+
throw new Error("DurableExecution already started");
|
|
73
|
+
this.handlers = handlers;
|
|
74
|
+
for (const state of this.keys.values())
|
|
75
|
+
if (state.queued && !state.running)
|
|
76
|
+
this.drain(state);
|
|
77
|
+
for (const [id, timer] of this.timers)
|
|
78
|
+
this.armTimer(id, timer);
|
|
79
|
+
for (const [tenantId, sweep] of this.sweeps)
|
|
80
|
+
this.scheduleSweep(tenantId, sweep, 0);
|
|
81
|
+
}
|
|
82
|
+
async stop() {
|
|
83
|
+
this.handlers = undefined;
|
|
84
|
+
for (const handle of this.delayed)
|
|
85
|
+
clearTimeout(handle);
|
|
86
|
+
this.delayed.clear();
|
|
87
|
+
for (const timer of this.timers.values())
|
|
88
|
+
if (timer.handle)
|
|
89
|
+
clearTimeout(timer.handle);
|
|
90
|
+
for (const sweep of this.sweeps.values())
|
|
91
|
+
if (sweep.handle)
|
|
92
|
+
clearTimeout(sweep.handle);
|
|
93
|
+
for (const state of this.keys.values())
|
|
94
|
+
state.controller?.abort();
|
|
95
|
+
while (this.inflight.size > 0)
|
|
96
|
+
await Promise.allSettled(this.inflight);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Resolves once no advance is running or queued and no busy or retry
|
|
100
|
+
* re-wake is pending. Timers and sweeps are not waited for. Test helper.
|
|
101
|
+
*/
|
|
102
|
+
async idle() {
|
|
103
|
+
while (this.delayed.size > 0 ||
|
|
104
|
+
this.inflight.size > 0 ||
|
|
105
|
+
[...this.keys.values()].some((s) => s.running || (s.queued && this.handlers)))
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
107
|
+
}
|
|
108
|
+
enqueue(tenantId, sessionId) {
|
|
109
|
+
const key = sessionKey(tenantId, sessionId);
|
|
110
|
+
let state = this.keys.get(key);
|
|
111
|
+
if (!state)
|
|
112
|
+
this.keys.set(key, (state = {
|
|
113
|
+
tenantId,
|
|
114
|
+
sessionId,
|
|
115
|
+
running: false,
|
|
116
|
+
queued: false,
|
|
117
|
+
attempts: 0,
|
|
118
|
+
}));
|
|
119
|
+
state.queued = true;
|
|
120
|
+
if (!state.running && this.handlers)
|
|
121
|
+
this.drain(state);
|
|
122
|
+
}
|
|
123
|
+
drain(state) {
|
|
124
|
+
state.running = true;
|
|
125
|
+
this.track((async () => {
|
|
126
|
+
try {
|
|
127
|
+
while (state.queued && this.handlers) {
|
|
128
|
+
state.queued = false;
|
|
129
|
+
await this.advanceOnce(state, this.handlers);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
state.running = false;
|
|
134
|
+
// Idle keys hold nothing (a retry keeps its attempt count); forget them so
|
|
135
|
+
// long-lived processes stay small.
|
|
136
|
+
const key = sessionKey(state.tenantId, state.sessionId);
|
|
137
|
+
if (!state.queued && state.attempts === 0 && this.keys.get(key) === state)
|
|
138
|
+
this.keys.delete(key);
|
|
139
|
+
}
|
|
140
|
+
})());
|
|
141
|
+
}
|
|
142
|
+
/** Drops dedupe keys older than the retention window. */
|
|
143
|
+
forgetDedupeKeys(now) {
|
|
144
|
+
for (const [dedupe, seenAt] of this.seen) {
|
|
145
|
+
if (now - seenAt < this.dedupeRetentionMs)
|
|
146
|
+
break;
|
|
147
|
+
this.seen.delete(dedupe);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async advanceOnce(state, handlers) {
|
|
151
|
+
const controller = new AbortController();
|
|
152
|
+
state.controller = controller;
|
|
153
|
+
try {
|
|
154
|
+
const result = await handlers.advance(state.tenantId, state.sessionId, controller.signal);
|
|
155
|
+
state.attempts = 0;
|
|
156
|
+
if (result.status === "busy")
|
|
157
|
+
this.later(result.retryAfterMs, () => this.enqueue(state.tenantId, state.sessionId));
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
state.attempts += 1;
|
|
161
|
+
if (state.attempts >= this.maxAttempts || controller.signal.aborted) {
|
|
162
|
+
state.attempts = 0;
|
|
163
|
+
this.onError(error);
|
|
164
|
+
}
|
|
165
|
+
else
|
|
166
|
+
this.later(this.retryDelayMs * 2 ** (state.attempts - 1), () => this.enqueue(state.tenantId, state.sessionId));
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
state.controller = undefined;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
armTimer(id, timer) {
|
|
173
|
+
timer.handle = setTimeout(() => {
|
|
174
|
+
if (this.timers.get(id) !== timer)
|
|
175
|
+
return;
|
|
176
|
+
this.timers.delete(id);
|
|
177
|
+
const handlers = this.handlers;
|
|
178
|
+
if (!handlers)
|
|
179
|
+
return;
|
|
180
|
+
if (!handlers.fire) {
|
|
181
|
+
this.onError(new Error("WorkerHandlers.fire is required for timers"));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
this.track(handlers.fire(timer.tenantId, timer.key).catch(this.onError));
|
|
185
|
+
}, Math.max(0, timer.at - Date.now()));
|
|
186
|
+
}
|
|
187
|
+
scheduleSweep(tenantId, sweep, delay) {
|
|
188
|
+
if (sweep.running || sweep.handle)
|
|
189
|
+
return;
|
|
190
|
+
sweep.handle = setTimeout(() => {
|
|
191
|
+
sweep.handle = undefined;
|
|
192
|
+
const handlers = this.handlers;
|
|
193
|
+
if (!handlers || this.sweeps.get(tenantId) !== sweep)
|
|
194
|
+
return;
|
|
195
|
+
sweep.running = true;
|
|
196
|
+
this.track(handlers
|
|
197
|
+
.sweep(tenantId)
|
|
198
|
+
.catch(this.onError)
|
|
199
|
+
.finally(() => {
|
|
200
|
+
sweep.running = false;
|
|
201
|
+
if (this.handlers && this.sweeps.get(tenantId) === sweep)
|
|
202
|
+
this.scheduleSweep(tenantId, sweep, this.sweepIntervalMs);
|
|
203
|
+
}));
|
|
204
|
+
}, delay);
|
|
205
|
+
}
|
|
206
|
+
later(ms, fn) {
|
|
207
|
+
const handle = setTimeout(() => {
|
|
208
|
+
this.delayed.delete(handle);
|
|
209
|
+
fn();
|
|
210
|
+
}, Math.max(0, ms));
|
|
211
|
+
this.delayed.add(handle);
|
|
212
|
+
}
|
|
213
|
+
track(promise) {
|
|
214
|
+
this.inflight.add(promise);
|
|
215
|
+
void promise.finally(() => this.inflight.delete(promise));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Durable Session Execution seam (architecture §12.3).
|
|
3
|
+
*
|
|
4
|
+
* Durable Session Execution decides when to look at a session again. It never
|
|
5
|
+
* holds session state: every outcome lives in the Session Store, and `advance`
|
|
6
|
+
* is safe to repeat (§10.1). Restate is the supported implementation
|
|
7
|
+
* (`adapters/execution/restate.ts`); `execution/memory.ts` is the in-process
|
|
8
|
+
* implementation used by unit tests, `startEphemeralRuntime` and a Host
|
|
9
|
+
* started without Restate endpoints.
|
|
10
|
+
*
|
|
11
|
+
* ## Guarantees every implementation keeps
|
|
12
|
+
*
|
|
13
|
+
* - **One advance per key at a time.** The key is `<tenantId>:<sessionId>`.
|
|
14
|
+
* Two `advance` calls for one key never overlap, on one process or many.
|
|
15
|
+
* Different keys may run concurrently.
|
|
16
|
+
* - **At least once after a wake.** When `wake` resolves, an `advance` for the
|
|
17
|
+
* key will start after that point (possibly merged with other wakes). Wakes
|
|
18
|
+
* are sent after the Session Store transaction that caused them commits
|
|
19
|
+
* (`Tx.afterCommit`), never inside it.
|
|
20
|
+
* - **Dedupe.** Wakes carrying the same `dedupeKey` for the same key cause at
|
|
21
|
+
* most one advance for that cause (within the implementation's retention
|
|
22
|
+
* window, at least 24 hours for Restate).
|
|
23
|
+
* - **Busy re-wake.** An advance that returns `busy` is re-run for the same key
|
|
24
|
+
* after `retryAfterMs`.
|
|
25
|
+
* - **Retries on infrastructure errors.** A handler that throws is retried with
|
|
26
|
+
* backoff. Session outcomes never throw: a turn that fails, pauses, waits or
|
|
27
|
+
* becomes `uncertain` is settled in the Session Store and `advance` returns
|
|
28
|
+
* `done`.
|
|
29
|
+
* - **Sweeps.** `armSweep` arms one self-re-arming sweep per Tenant; arming an
|
|
30
|
+
* armed Tenant is a no-op. The sweep expires claims, re-wakes orphaned
|
|
31
|
+
* `runnable` sessions and relays outbox rows left behind.
|
|
32
|
+
*/
|
|
33
|
+
/** Why a session is woken (§12.3, "Where wakes come from"). */
|
|
34
|
+
export type WakeReason =
|
|
35
|
+
/** Session commands. */
|
|
36
|
+
"message" | "approve" | "respond"
|
|
37
|
+
/** An executor posted an Action result. */
|
|
38
|
+
| "action_result"
|
|
39
|
+
/** A linked agent turn completed, failed or was cancelled. */
|
|
40
|
+
| "linked"
|
|
41
|
+
/** Queued flow effects, pending agent effects reconciled. */
|
|
42
|
+
| "flow"
|
|
43
|
+
/** The Tenant sweep and `busy` re-wakes. */
|
|
44
|
+
| "recover";
|
|
45
|
+
export declare const WAKE_REASONS: readonly WakeReason[];
|
|
46
|
+
export interface Wake {
|
|
47
|
+
reason: WakeReason;
|
|
48
|
+
/**
|
|
49
|
+
* Merges repeated wakes for the same cause, e.g. the command's idempotency
|
|
50
|
+
* key. Scoped to the session key. Absent means never deduped.
|
|
51
|
+
*/
|
|
52
|
+
dedupeKey?: string;
|
|
53
|
+
}
|
|
54
|
+
export type AdvanceResult = {
|
|
55
|
+
status: "done";
|
|
56
|
+
}
|
|
57
|
+
/** Another Worker holds a live lease; run again for this key after `retryAfterMs`. */
|
|
58
|
+
| {
|
|
59
|
+
status: "busy";
|
|
60
|
+
retryAfterMs: number;
|
|
61
|
+
};
|
|
62
|
+
/** What a Worker runs when Durable Session Execution calls it. */
|
|
63
|
+
export interface WorkerHandlers {
|
|
64
|
+
/**
|
|
65
|
+
* Advances one session (§10.5). `signal` aborts when the Worker stops or the
|
|
66
|
+
* session is cancelled through the control stream. Throw only on
|
|
67
|
+
* infrastructure errors.
|
|
68
|
+
*/
|
|
69
|
+
advance(tenantId: string, sessionId: string, signal: AbortSignal): Promise<AdvanceResult>;
|
|
70
|
+
/** One pass of the Tenant sweep. Re-armed by the implementation afterwards. */
|
|
71
|
+
sweep(tenantId: string): Promise<void>;
|
|
72
|
+
/** A durable timer set with `timer` fired. Required when `timer` is used. */
|
|
73
|
+
fire?(tenantId: string, key: string): Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
export interface DurableExecution {
|
|
76
|
+
/** At-least-once. `dedupeKey` merges repeated wakes for the same cause. */
|
|
77
|
+
wake(tenantId: string, sessionId: string, wake: Wake): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Durable one-shot timer that calls `WorkerHandlers.fire(tenantId, key)` at
|
|
80
|
+
* or after `at`. Setting a key again replaces the earlier time where the
|
|
81
|
+
* implementation can; `fire` must be idempotent either way.
|
|
82
|
+
*/
|
|
83
|
+
timer(tenantId: string, key: string, at: Date): Promise<void>;
|
|
84
|
+
/** Arms the Tenant's self-re-arming sweep. Idempotent. */
|
|
85
|
+
armSweep(tenantId: string): Promise<void>;
|
|
86
|
+
/** Stops re-arming the Tenant's sweep (Tenant deleted). A pass already running finishes. */
|
|
87
|
+
disarmSweep(tenantId: string): Promise<void>;
|
|
88
|
+
/** Starts delivering to `handlers` (serving the Worker endpoint, for Restate). */
|
|
89
|
+
start(handlers: WorkerHandlers): Promise<void>;
|
|
90
|
+
/** Stops delivering, aborts running advances' signals and waits for them. */
|
|
91
|
+
stop(): Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Resolves when the backing service answers, rejects otherwise (readiness,
|
|
94
|
+
* `infra/execution.ts`). Absent for in-process implementations.
|
|
95
|
+
*/
|
|
96
|
+
probe?(signal: AbortSignal): Promise<void>;
|
|
97
|
+
/**
|
|
98
|
+
* Invocations for `tenantId` that need an operator: paused after exhausting
|
|
99
|
+
* retries, or backing off after failures (Tenant status). Absent for
|
|
100
|
+
* in-process implementations.
|
|
101
|
+
*/
|
|
102
|
+
stuckInvocations?(tenantId: string): Promise<StuckInvocation[]>;
|
|
103
|
+
}
|
|
104
|
+
/** An invocation that needs an operator (`DurableExecution.stuckInvocations`). */
|
|
105
|
+
export interface StuckInvocation {
|
|
106
|
+
id: string;
|
|
107
|
+
/** `paused` or `backing-off`. */
|
|
108
|
+
status: string;
|
|
109
|
+
/** Service name without any prefix, e.g. `NylorunSession`. */
|
|
110
|
+
service: string;
|
|
111
|
+
handler: string;
|
|
112
|
+
/** Object key: `<tenantId>:<sessionId>`, `<tenantId>` or `<tenantId>:<timer key>`. */
|
|
113
|
+
key: string;
|
|
114
|
+
tenantId?: string;
|
|
115
|
+
retryCount: number;
|
|
116
|
+
lastFailure?: string;
|
|
117
|
+
modifiedAt?: string;
|
|
118
|
+
}
|
|
119
|
+
/** The key one advance at a time is serialized on. */
|
|
120
|
+
export declare function sessionKey(tenantId: string, sessionId: string): string;
|
|
121
|
+
/** Splits a key made by `sessionKey`. Tenant ids never contain `:`. */
|
|
122
|
+
export declare function parseSessionKey(key: string): {
|
|
123
|
+
tenantId: string;
|
|
124
|
+
sessionId: string;
|
|
125
|
+
};
|