@byok-sdk/server 0.12.0 → 0.14.0

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.
@@ -112,3 +112,41 @@ export declare class RateLimiter {
112
112
  */
113
113
  private evictOldestIfAtCapacity;
114
114
  }
115
+ /**
116
+ * The counting `InboundRateLimiter` (`@byok-sdk/cloud`'s store port) this
117
+ * package composes the kernel with.
118
+ *
119
+ * Position matters and is not this adapter's choice: the kernel debits one
120
+ * token at step 0 of its inbound gate, BEFORE the type-allow check, for every
121
+ * envelope that reaches it. That is exactly the choke point the old
122
+ * `ConnectionHub.handleInbound` occupied, which is why both `envelopesIn` (one
123
+ * per envelope, every outcome) and `rateLimitEvents` (one per REJECTED
124
+ * envelope, never coalesced) are counted here and nowhere else.
125
+ *
126
+ * The `device.rate_limited` embedder event is the one thing that IS coalesced,
127
+ * matching the pre-fold rule: an episode fires exactly one event, and only a
128
+ * subsequent SUCCESSFUL consume by that device re-arms it. So a flood produces
129
+ * one event and N counter increments, and a device that recovers and floods
130
+ * again produces a second, distinct event.
131
+ */
132
+ export interface InboundRateLimiterCounters {
133
+ /** Every envelope handed to the kernel's inbound gate, whatever the gate then decides. */
134
+ envelopesIn: number;
135
+ /** Envelopes the bucket refused. Per envelope — see the episode rule above. */
136
+ rateLimitEvents: number;
137
+ }
138
+ export interface CountingInboundRateLimiter {
139
+ /** The port `CloudStores.rateLimiter` is composed with. */
140
+ readonly limiter: {
141
+ consume(tenant: string, deviceId: string): Promise<boolean>;
142
+ };
143
+ readonly counters: InboundRateLimiterCounters;
144
+ }
145
+ /**
146
+ * Adapt a {@link RateLimiter} to the kernel's `(tenant, deviceId)` port.
147
+ *
148
+ * The bucket key is `${tenant}:${deviceId}` rather than the bare device id: a
149
+ * device id is only unique within its tenant, and a shared key space would let
150
+ * one tenant's flood spend another's budget for a colliding id.
151
+ */
152
+ export declare function createCountingInboundRateLimiter(limiter: RateLimiter, onRateLimited: (deviceId: string, at: string) => void): CountingInboundRateLimiter;
@@ -0,0 +1,86 @@
1
+ import type { InboundCommitted, ByokCloudObserver } from '@byok-sdk/cloud';
2
+ import type { TaskState } from '@byok-sdk/protocol';
3
+ import type { DeviceConnections } from './connections';
4
+ import type { ByokServerEvent, ServerTaskEvent } from './types';
5
+ /** Per-task {@link ServerTaskEvent} retention before drop-oldest engages. */
6
+ export declare const DEFAULT_TASK_EVENT_BUFFER_LIMIT = 1000;
7
+ /** How long a terminal task's relay state is retained before reclamation, ms. */
8
+ export declare const DEFAULT_TASK_EVENT_RETENTION_MS: number;
9
+ export interface TaskEventRelayOptions {
10
+ readonly connections: DeviceConnections;
11
+ readonly taskEventBufferLimit?: number;
12
+ readonly taskEventRetentionMs?: number;
13
+ }
14
+ /**
15
+ * Post-commit fan-out: cloud kernel envelopes -> `ServerTaskEvent` /
16
+ * `ByokServerEvent`.
17
+ *
18
+ * The invariant this file exists to keep (WP3B §3): **the relay holds
19
+ * notifications, never state**. It never answers "what is this task", "what did
20
+ * it produce" or "who owns it" — every one of those is read back from the
21
+ * kernel's durable stores on demand. What it owns is per-task delivery
22
+ * plumbing: a bounded replayable queue, and one promise that settles when the
23
+ * task reaches a terminal so `TaskHandle.result()` has something to await
24
+ * before it reads the answer back.
25
+ *
26
+ * The two small facts it does carry are delivery bookkeeping, not a read model:
27
+ * `terminalSettled` (so the FIRST terminal is the one that settles the promise,
28
+ * matching the store's own first-terminal-wins rule, and a later stale terminal
29
+ * is not announced as a second one) and `claimedRuntime` (echoed onto the
30
+ * `task.state` event stream because the pre-fold feed carried it there; the
31
+ * authoritative copy is `TaskAttempt.claimedRuntime`).
32
+ *
33
+ * Dispatch first {@link provision}s a known task id, then activates it with
34
+ * {@link noteDispatched} after the kernel has accepted its offer. Envelopes
35
+ * that arrive in that narrow interval are buffered, not dropped. An envelope
36
+ * naming a task this server never dispatched is folded for nobody and allocates nothing —
37
+ * otherwise a device could grow this map without bound by guessing task ids,
38
+ * and the kernel's gate deliberately treats such an envelope as a harmless
39
+ * store no-op rather than a rejection.
40
+ *
41
+ * `onInboundCommitted` runs inline on the kernel's request path and must stay
42
+ * synchronous and cheap; anything needing a store read goes through
43
+ * {@link onTaskActivity}, which the composition sets and which owns its own
44
+ * failure handling.
45
+ */
46
+ export declare class TaskEventRelay implements ByokCloudObserver {
47
+ #private;
48
+ /**
49
+ * Called once per committed task-progress observation for a dispatched task.
50
+ * The composition uses it to run the implicit-approval-resume check, which
51
+ * needs a store read and therefore cannot happen inline here.
52
+ */
53
+ onTaskActivity: ((taskId: string, pendingRequestSourceEnvelopeId: string | undefined, activitySourceEnvelopeId: string, activityAt: string) => void) | undefined;
54
+ constructor(options: TaskEventRelayOptions);
55
+ /** The cross-task embedder feed backing `ByokServer.events.subscribe()`. */
56
+ serverEvents(): AsyncIterable<ByokServerEvent>;
57
+ /** Publish one cross-task event (rate-limit episodes, host-side transitions). */
58
+ emitServerEvent(event: ByokServerEvent): void;
59
+ /** Pre-register a task id before its offer enqueue can become observable. */
60
+ provision(taskId: string): void;
61
+ /** Forget an enqueue that failed before it produced an offer. */
62
+ abort(taskId: string): void;
63
+ /**
64
+ * Open the relay for a task this server just dispatched, and publish its
65
+ * `Offered` origin on both feeds. `at` is the offer envelope's own timestamp,
66
+ * so the feed and the snapshot agree on when the task began.
67
+ */
68
+ noteDispatched(taskId: string, at: string): void;
69
+ /** The per-task feed backing `TaskHandle.events()`, replayed from the start of what is retained. */
70
+ events(taskId: string): AsyncIterable<ServerTaskEvent>;
71
+ /** Settles when this task first reaches a terminal — the barrier `TaskHandle.result()` awaits. */
72
+ terminal(taskId: string): Promise<void>;
73
+ /**
74
+ * Host-side transitions the wire never carries: an accepted cancellation, and
75
+ * an approval the operator resolved through `TaskHandle.approve`/`reject`.
76
+ * Published here so the two feeds report the same task history whichever side
77
+ * moved it, and (for a terminal) so `result()` is not left waiting on a device
78
+ * message that a cancelled task will never send.
79
+ */
80
+ noteHostTransition(taskId: string, state: TaskState, at: string): void;
81
+ onInboundCommitted(input: InboundCommitted): void;
82
+ /** Publish an implicit resolution the composition inferred from later task traffic. */
83
+ emitImplicitApprovalResolved(taskId: string, at: string): void;
84
+ /** Close every feed and drop every timer. Safe to call more than once. */
85
+ stop(): void;
86
+ }
@@ -0,0 +1,64 @@
1
+ import type { DeviceRecord, PendingApproval, TaskAttempt, TerminalResult } from '@byok-sdk/cloud';
2
+ import type { TaskState } from '@byok-sdk/protocol';
3
+ import type { DeviceConnection } from './connections';
4
+ import type { MachineInfo, TaskResult, TaskSnapshot } from './types';
5
+ /**
6
+ * Projections from the cloud kernel's read model onto this package's public
7
+ * shapes. Pure functions, deliberately: every input is a value the caller has
8
+ * already read from a store, so there is exactly one place the mapping lives
9
+ * and no way for a second copy of it to drift.
10
+ */
11
+ /**
12
+ * What this server calls a task the kernel calls a `TaskAttempt`.
13
+ *
14
+ * The order of the gates is the whole contract, and it mirrors the kernel's own
15
+ * (`ByokCloud.readTaskResult`):
16
+ *
17
+ * 1. an accepted host cancellation OUTRANKS everything a runtime reports later.
18
+ * `cancel()` is authoritative the moment the kernel records it, so the task
19
+ * reads `Cancelled` immediately and a late `task.complete` — which the wire
20
+ * still answers as a success — cannot move it;
21
+ * 2. a terminal attempt is its own terminal;
22
+ * 3. an unresolved approval on the task's timeline is `AwaitApproval`. The
23
+ * kernel has no such attempt STATUS on purpose (ADR-028: no execution state
24
+ * in the cloud) — the pause is derived from the durable approval timeline,
25
+ * which is the same single authority `approveTask`'s staleness gate reads;
26
+ * 4. otherwise the attempt's own coarse status.
27
+ */
28
+ export declare function toTaskState(attempt: TaskAttempt, pending: PendingApproval | undefined): TaskState;
29
+ /**
30
+ * The kernel's typed terminal read model, narrowed to this package's
31
+ * {@link TaskResult}.
32
+ *
33
+ * Deliberately LOSSY in one direction: `taskId`, `recordedAt`, `agentRef`,
34
+ * `usage` and `terminalCause` exist on `TerminalResult` and have no place here.
35
+ * They are all reachable through the kernel (`readTaskResult`), and copying
36
+ * them into a second shape would make this projection something a caller has to
37
+ * keep in sync rather than a narrowing of one authority.
38
+ */
39
+ export declare function toTaskResult(terminal: TerminalResult | undefined): TaskResult | undefined;
40
+ /**
41
+ * The dispatch-time facts the kernel does not persist.
42
+ *
43
+ * `TaskAttempt` records ownership and disposition, not the request that opened
44
+ * it — it carries no `createdAt` and no `sessionRef`. Both are needed by the
45
+ * public snapshot, and both are known exactly once, at dispatch, so this
46
+ * process keeps them keyed by task id rather than inventing a durable column or
47
+ * quietly reporting the attempt's last-mutation time as its creation time.
48
+ * Deliberately not TTL-reclaimed: it is the exact peer of the in-memory attempt
49
+ * store this composition is built on, and it must not forget a task that store
50
+ * still answers for.
51
+ */
52
+ export interface DispatchFacts {
53
+ readonly createdAt: string;
54
+ readonly sessionRef?: string;
55
+ }
56
+ export declare function toTaskSnapshot(attempt: TaskAttempt, pending: PendingApproval | undefined, terminal: TerminalResult | undefined, dispatched: DispatchFacts | undefined): TaskSnapshot;
57
+ /**
58
+ * A device row joined with what this process observed of its connection.
59
+ *
60
+ * The identity half is durable and tenant-owned; the connection half is an
61
+ * in-process observation that dies with the server — see `connections.ts` for
62
+ * why `conn.hello`'s runtime discovery block is deliberately not persisted.
63
+ */
64
+ export declare function toMachineInfo(device: DeviceRecord, connection: DeviceConnection | undefined): MachineInfo;
@@ -16,12 +16,12 @@ export declare function closeSqliteDatabaseAfterInitializationFailure(db: Databa
16
16
  * Thrown when `node:sqlite` isn't available in the running Node.js binary.
17
17
  * `node:sqlite` shipped in Node.js 22.5.0 (https://nodejs.org/api/sqlite.html)
18
18
  * and remains marked experimental there (an `ExperimentalWarning` on stderr
19
- * is expected and harmless — not an error). The SQLite-backed reference
20
- * stores in this package (`SqliteTaskStore`, `SqliteBlobStore`) deliberately
19
+ * is expected and harmless — not an error). The SQLite-backed embedded store
20
+ * composition deliberately
21
21
  * depend on nothing else — no `better-sqlite3` or other native module —
22
22
  * because staying at zero native dependencies is required to keep
23
23
  * `@byok-sdk/server` trivially packageable across platforms. The tradeoff is
24
- * that these stores simply don't work below Node 22.5; this error says so
24
+ * that this composition simply doesn't work below Node 22.5; this error says so
25
25
  * clearly and up front, instead of letting a cryptic `Cannot find module
26
26
  * 'node:sqlite'` surface from deep inside a query.
27
27
  */
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,140 @@
1
+ import { type Clock, type ContentHash, type CoreStores, type MailboxAppendInput, type MailboxCursorState, type MailboxMessage, type MailboxPage, type MailboxReadQuery, type MailboxRecordDeliveryInput, type MailboxAdvanceCursorInput, type MailboxRetentionInput, type MailboxRetentionResult, type MailboxStore, type ObjectCommitInput, type ObjectListQuery, type ObjectManifestEntry, type ObjectManifestInput, type ObjectReferenceInput, type ObjectStore, type StorageReservation, type TenantId } from '@byok-sdk/core';
2
+ import { type AgentMessageAdmission, type AgentRef, type BlobContentProxy, type CloudBlobStore, type BlobObservation, type BlobReadResult, type BlobWriteResult, type CloudCrypto, type CloudStores, type TaskAttempt, type TaskAttemptListQuery, type TaskAttemptPage, type TaskAttemptStatus, type TaskAttemptStore, type TaskCancellationMutation, type TaskCancellationRequest, type TaskCancellationStore } from '@byok-sdk/cloud';
3
+ import { type RuntimeCapabilities, type RuntimeId } from '@byok-sdk/protocol';
4
+ import type { DatabaseSync } from 'node:sqlite';
5
+ export interface SqliteEmbeddedStoreOptions {
6
+ readonly path: string;
7
+ readonly urlTtlMs?: number;
8
+ }
9
+ export interface SqliteEmbeddedStores {
10
+ readonly core: CoreStores;
11
+ readonly cloud: CloudStores;
12
+ readonly blobContentProxy: BlobContentProxy;
13
+ close(): Promise<void>;
14
+ }
15
+ /** One serialized owner for the synchronous SQLite handle and every transaction on it. */
16
+ declare class SqliteCoordinator {
17
+ #private;
18
+ readonly db: DatabaseSync;
19
+ constructor(path: string);
20
+ run<T>(operation: (db: DatabaseSync) => T | Promise<T>): Promise<T>;
21
+ transaction<T>(operation: (db: DatabaseSync) => T | Promise<T>): Promise<T>;
22
+ close(): Promise<void>;
23
+ }
24
+ export declare class SqliteTaskAttemptStore implements TaskAttemptStore {
25
+ #private;
26
+ private readonly coordinator;
27
+ private readonly clock;
28
+ constructor(coordinator: SqliteCoordinator, clock: Clock);
29
+ open(tenant: TenantId, input: {
30
+ taskId: string;
31
+ deviceId: string;
32
+ agentRef?: AgentRef;
33
+ }): Promise<TaskAttempt>;
34
+ reserveAgentOffer(tenant: TenantId, input: {
35
+ taskId: string;
36
+ deviceId: string;
37
+ agentRef: AgentRef;
38
+ }): Promise<{
39
+ attempt: TaskAttempt;
40
+ created: boolean;
41
+ }>;
42
+ reserveAgentMessage(tenant: TenantId, input: {
43
+ taskId: string;
44
+ deviceId: string;
45
+ messageId: string;
46
+ payloadBody: string;
47
+ }): Promise<'reserved' | 'pending' | 'rejected'>;
48
+ readAgentMessage(tenant: TenantId, input: {
49
+ taskId: string;
50
+ deviceId: string;
51
+ messageId: string;
52
+ payloadBody: string;
53
+ }): Promise<AgentMessageAdmission | undefined>;
54
+ finalizeAgentMessage(tenant: TenantId, input: {
55
+ taskId: string;
56
+ deviceId: string;
57
+ messageId: string;
58
+ payloadBody: string;
59
+ terminalBody: string;
60
+ }): Promise<AgentMessageAdmission | undefined>;
61
+ get(tenant: TenantId, taskId: string): Promise<TaskAttempt | undefined>;
62
+ getMany(tenant: TenantId, taskIds: readonly string[]): Promise<readonly TaskAttempt[]>;
63
+ list(tenant: TenantId, query: TaskAttemptListQuery): Promise<TaskAttemptPage>;
64
+ claim(tenant: TenantId, input: {
65
+ taskId: string;
66
+ deviceId: string;
67
+ runtime?: RuntimeId;
68
+ capabilities?: RuntimeCapabilities;
69
+ }): Promise<TaskAttempt | undefined>;
70
+ recordStatus(tenant: TenantId, input: {
71
+ taskId: string;
72
+ status: TaskAttemptStatus;
73
+ agentRef?: AgentRef;
74
+ terminalCause?: string;
75
+ }): Promise<TaskAttempt | undefined>;
76
+ }
77
+ export declare class SqliteMailboxStore implements MailboxStore {
78
+ private readonly coordinator;
79
+ private readonly clock;
80
+ constructor(coordinator: SqliteCoordinator, clock: Clock);
81
+ append(tenant: TenantId, input: MailboxAppendInput): Promise<MailboxMessage>;
82
+ readAfter(tenant: TenantId, query: MailboxReadQuery): Promise<MailboxPage>;
83
+ recordDelivery(tenant: TenantId, input: MailboxRecordDeliveryInput): Promise<MailboxCursorState>;
84
+ advanceCursor(tenant: TenantId, input: MailboxAdvanceCursorInput): Promise<MailboxCursorState>;
85
+ readCursor(tenant: TenantId, deviceId: string): Promise<MailboxCursorState>;
86
+ collectRetired(tenant: TenantId, input: MailboxRetentionInput): Promise<MailboxRetentionResult>;
87
+ }
88
+ export declare class SqliteTaskCancellationStore implements TaskCancellationStore {
89
+ private readonly coordinator;
90
+ private readonly clock;
91
+ constructor(coordinator: SqliteCoordinator, clock: Clock);
92
+ request(tenant: TenantId, input: TaskCancellationRequest): Promise<TaskCancellationMutation | undefined>;
93
+ }
94
+ export declare class SqliteObjectStore implements ObjectStore {
95
+ #private;
96
+ private readonly coordinator;
97
+ private readonly clock;
98
+ constructor(coordinator: SqliteCoordinator, clock: Clock);
99
+ putManifest(tenant: TenantId, input: ObjectManifestInput): Promise<ObjectManifestEntry>;
100
+ commit(tenant: TenantId, input: ObjectCommitInput): Promise<ObjectManifestEntry>;
101
+ get(tenant: TenantId, hash: ContentHash): Promise<ObjectManifestEntry | undefined>;
102
+ list(tenant: TenantId, query: ObjectListQuery): Promise<readonly ObjectManifestEntry[]>;
103
+ addReference(tenant: TenantId, input: ObjectReferenceInput): Promise<ObjectManifestEntry>;
104
+ removeReference(tenant: TenantId, input: ObjectReferenceInput): Promise<ObjectManifestEntry>;
105
+ markDeletePending(tenant: TenantId, hash: ContentHash): Promise<ObjectManifestEntry>;
106
+ markDeleted(tenant: TenantId, hash: ContentHash): Promise<ObjectManifestEntry>;
107
+ }
108
+ declare class SqliteBlobRegistry {
109
+ readonly coordinator: SqliteCoordinator;
110
+ readonly clock: Clock;
111
+ readonly crypto: CloudCrypto;
112
+ readonly secret: Uint8Array;
113
+ readonly urlTtlMs: number;
114
+ constructor(coordinator: SqliteCoordinator, clock: Clock, crypto: CloudCrypto, urlTtlMs: number);
115
+ signUrl(blobId: string, action: 'put' | 'get'): Promise<string>;
116
+ }
117
+ export declare class SqliteCloudBlobStore implements CloudBlobStore {
118
+ private readonly registry;
119
+ constructor(registry: SqliteBlobRegistry);
120
+ createUpload(tenant: TenantId, reservation: StorageReservation): Promise<{
121
+ blobId: string;
122
+ uploadUrl: string;
123
+ }>;
124
+ observeUpload(tenant: TenantId, blobId: string, reservation: StorageReservation): Promise<BlobObservation | undefined>;
125
+ getDownloadUrl(tenant: TenantId, blobId: string): Promise<string | undefined>;
126
+ }
127
+ export declare class SqliteBlobContentProxy implements BlobContentProxy {
128
+ private readonly registry;
129
+ constructor(registry: SqliteBlobRegistry);
130
+ verifySignedUrl(blobId: string, action: 'put' | 'get', sig: string, exp: number): Promise<boolean>;
131
+ expectedUploadBytes(blobId: string): Promise<bigint | undefined>;
132
+ writeContent(blobId: string, data: Uint8Array): Promise<BlobWriteResult>;
133
+ readContent(blobId: string): Promise<BlobReadResult | undefined>;
134
+ }
135
+ /** Mixed embedded composition: exactly six durable interfaces, every other port unchanged in-memory. */
136
+ export declare function createSqliteEmbeddedStores(options: SqliteEmbeddedStoreOptions, dependencies: {
137
+ readonly clock: Clock;
138
+ readonly crypto: CloudCrypto;
139
+ }): SqliteEmbeddedStores;
140
+ export {};
@@ -0,0 +1,65 @@
1
+ import { type Clock, type CoreStores, type TenantId } from '@byok-sdk/core';
2
+ import { type BlobContentProxy, type CloudCrypto, type CloudStores } from '@byok-sdk/cloud';
3
+ import { type RateLimiterOptions } from './rate-limiter';
4
+ import type { DeviceConnections } from './connections';
5
+ import type { ByokServerStorage } from './types';
6
+ /**
7
+ * The store composition this façade hands the cloud kernel.
8
+ *
9
+ * Same parts as `@byok-sdk/cloud`'s own in-memory composition, with three
10
+ * decorators layered on ports the kernel already calls, and nothing else:
11
+ *
12
+ * - `rateLimiter` becomes this package's token bucket, which is also where
13
+ * `envelopesIn` and `rateLimitEvents` are counted (the kernel debits it at
14
+ * gate step 0, for every envelope, before any other decision — the exact
15
+ * choke point `ConnectionHub.handleInbound` used to be);
16
+ * - `dedup` counts the already-seen answers that back `dedupDrops`;
17
+ * - `mailbox` reports each device-scoped read, which is how a device that only
18
+ * ever long-polls still counts as present.
19
+ *
20
+ * Decorators rather than kernel changes, deliberately: each counter is a fact
21
+ * about a call this composition already makes, so it is observable from the
22
+ * outside, and the kernel keeps no observability surface it would then have to
23
+ * keep honest for every other deployment.
24
+ */
25
+ /** Per-product blob size ceiling (§7): 100MB unless overridden. */
26
+ export declare const DEFAULT_MAX_BLOB_SIZE_BYTES: number;
27
+ /**
28
+ * The one tenant an embedded server serves.
29
+ *
30
+ * An embedded `createByokServer` is a single product's coordinator, not a
31
+ * multi-tenant control plane: there is no surface on it that could name a
32
+ * second tenant and no request that could carry one. Deriving the id from
33
+ * `productId` keeps that visible — one product, one tenant, and a device paired
34
+ * into this instance can only ever land in it.
35
+ */
36
+ export declare function serverTenantId(productId: string): TenantId;
37
+ /** Live counter reads; every field is derived from a call the kernel already makes. */
38
+ export interface FacadeCounters {
39
+ /** Every envelope the kernel's inbound gate was handed, whatever it decided. */
40
+ readonly envelopesIn: number;
41
+ /** Inbound envelopes recognized as an already-seen `(device, envelope id)` pair. */
42
+ readonly dedupDrops: number;
43
+ /** Inbound envelopes the bucket refused — one per envelope, never coalesced. */
44
+ readonly rateLimitEvents: number;
45
+ }
46
+ export interface FacadeStoreComposition {
47
+ readonly core: CoreStores;
48
+ readonly cloud: CloudStores;
49
+ readonly blobContentProxy: BlobContentProxy;
50
+ readonly clock: Clock;
51
+ readonly crypto: CloudCrypto;
52
+ readonly counters: FacadeCounters;
53
+ close(): Promise<void>;
54
+ }
55
+ export interface FacadeStoreOptions {
56
+ readonly tenant: TenantId;
57
+ readonly maxBlobSizeBytes: number;
58
+ readonly storage?: ByokServerStorage;
59
+ readonly rateLimit?: RateLimiterOptions;
60
+ /** Where a device's own liveness observations are recorded. */
61
+ readonly connections: DeviceConnections;
62
+ /** Fired once per rate-limit EPISODE, not per refused envelope. */
63
+ readonly onRateLimited: (deviceId: string, at: string) => void;
64
+ }
65
+ export declare function composeFacadeStores(options: FacadeStoreOptions): FacadeStoreComposition;
@@ -0,0 +1,61 @@
1
+ import { type ByokCloud, type SteerRejectionCode, type TenantId } from '@byok-sdk/cloud';
2
+ import type { RuntimeId, TaskState } from '@byok-sdk/protocol';
3
+ import type { TaskEventRelay } from './relay';
4
+ import type { TaskHandle, TaskResult } from './types';
5
+ /**
6
+ * Thrown by {@link TaskHandle.steer} when the kernel refuses the steer.
7
+ *
8
+ * This package keeps its OWN class rather than re-exporting the kernel's, and
9
+ * that is deliberate rather than a leftover. The two carry the same `taskId`,
10
+ * `code` and `runtime`, and differ in exactly one field: the kernel reports
11
+ * `status: TaskAttemptStatus` — its coarse, execution-free attempt disposition
12
+ * (`running`, `offered`, …) — while this surface reports `state: TaskState`,
13
+ * the WIRE vocabulary every other member of this package speaks
14
+ * (`TaskSnapshot.state`, `ServerTaskEvent`, `HubStats.taskCountsByState`).
15
+ * `AwaitApproval` is the reason they cannot be the same field: it is derived
16
+ * from the durable approval timeline and has no attempt status at all
17
+ * (ADR-028), so a `TaskAttemptStatus -> TaskState` mapping inside the kernel
18
+ * would have to report `Running` for a task this façade calls `AwaitApproval`.
19
+ *
20
+ * So the state here is not translated from the kernel's field — it is READ,
21
+ * through the very projection `byok.tasks.get(taskId)` answers with, at the
22
+ * moment of the refusal. One authority, two readers, no second mapping.
23
+ *
24
+ * Deviation from design packet §1.2, which had this class move to
25
+ * `@byok-sdk/cloud` with the server re-exporting it: the wire `TaskState`
26
+ * vocabulary is host-facing and lives here, so the class that carries it does
27
+ * too. `SteerRejectionCode` and `StaleApprovalError` are unaffected and stay
28
+ * kernel re-exports.
29
+ */
30
+ export declare class SteerRejectedError extends Error {
31
+ readonly taskId: string;
32
+ readonly code: SteerRejectionCode;
33
+ /** The task's state at the moment the steer was refused. */
34
+ readonly state: TaskState;
35
+ /** `TaskSnapshot.claimedRuntime` — `undefined` when nothing was ever recorded, which is itself a reason `steer_unsupported_runtime` can fire. */
36
+ readonly runtime: RuntimeId | undefined;
37
+ constructor(taskId: string, code: SteerRejectionCode, state: TaskState, runtime: RuntimeId | undefined);
38
+ }
39
+ export interface TaskHandleDeps {
40
+ readonly tenant: TenantId;
41
+ readonly cloud: ByokCloud;
42
+ readonly relay: TaskEventRelay;
43
+ /**
44
+ * The task's current `TaskSnapshot.state`, or `undefined` when the attempt is
45
+ * gone. The snapshot projection itself — never a second derivation.
46
+ */
47
+ readonly readState: (taskId: string) => Promise<TaskState | undefined>;
48
+ /** The read-back this handle's `result()` answers with. */
49
+ readonly readResult: (taskId: string) => Promise<TaskResult | undefined>;
50
+ }
51
+ /**
52
+ * One in-flight task's control surface.
53
+ *
54
+ * The §3 invariant it exists to keep: **the handle is not a second authority.**
55
+ * Every mutation is a kernel call, and `result()` is a READ-BACK — it waits for
56
+ * the relay's terminal barrier and then asks the store what the terminal was,
57
+ * so `handle.result()` and `byok.tasks.get(taskId).result` are physically the
58
+ * same fact rather than two copies that can disagree. Nothing about the task is
59
+ * cached here.
60
+ */
61
+ export declare function createTaskHandle(taskId: string, deps: TaskHandleDeps): TaskHandle;