@synapse-sdk/client 0.1.0 → 0.3.3

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.
@@ -0,0 +1,820 @@
1
+ import type { Card, ClientType, Delivery, FecParams, Role, ServiceRef, StoreMode, StoreOnDeath, StoreRow, TimeDomain, TopicRef, TrackDesc, Tlv, JsonSchema } from "@synapse-sdk/core";
2
+ import type { SynapseError, TasksRun } from "@synapse-sdk/core";
3
+ export type { Card, ClientType, Delivery, FecParams, Role, ServiceRef, StoreMode, StoreOnDeath, StoreRow, TimeDomain, TopicRef, TrackDesc };
4
+ /** What the reconnect policy is asked before every attempt. */
5
+ export interface RetryContext {
6
+ attempt: number;
7
+ }
8
+ /** How long to wait before the next connect attempt; `null` = give up (SDK §2, WP §10.4). */
9
+ export interface ReconnectPolicy {
10
+ /** The first delay; also the healthy threshold: a session that lived at least this long resets the backoff. */
11
+ initialMs: number;
12
+ nextRetryDelayMs(ctx: RetryContext): number | null;
13
+ }
14
+ export type Transport = "auto" | "webtransport" | "websocket";
15
+ export interface ConnectOptions {
16
+ /**
17
+ * The runtime, `https://host:4470` (the path to dial is learned from the runtime at every attempt). Through a relay: the relay with
18
+ * the runtime's path, `https://relay:4443/runtimes/<runtime_id>`, and `token`. A `jwt=` in the URL is taken as the token.
19
+ * Absent: `SYNAPSE_ENDPOINT` (Bun/Node; the runtime sets it for every task it starts); a browser has no environment,
20
+ * so there it is `invalid_input`.
21
+ */
22
+ url?: string;
23
+ /** The relay token that lets this node through a relay; none for a runtime dialed directly. Never printed. */
24
+ token?: string;
25
+ /** This node's type (WP §3.1): bounds the role it may exercise; the runtime grants the role. */
26
+ type: ClientType;
27
+ /** This node's name `[a-z0-9_-]{1,64}`; minted (`<type>-<6 hex>`) when absent and kept across reconnects. */
28
+ name?: string;
29
+ /** Shared across one human's clients so they are promoted and demoted together (WP §3.5); a `web`/`hmi` node without one mints one. */
30
+ userId?: string;
31
+ /** Reserved for signed credentials. */
32
+ credential?: string;
33
+ /** Pin of the broker's self-signed certificate (SHA-256 hex). Absent: `SYNAPSE_BROKER_CERT_SHA256`, then the broker's helper, then unpinned. */
34
+ brokerCertSha256?: string;
35
+ /** Time-domain id override (WP §7.3); default per runtime. */
36
+ domId?: string;
37
+ /** Standing project intent (WP §3.5), sent with the connect request. */
38
+ project?: "operator";
39
+ /** `null` = no reconnect: a dropped session ends the node `closed`. Default: 100 ms → 10 s, ×2, jitter, unlimited. */
40
+ reconnect?: ReconnectPolicy | null;
41
+ transport?: Transport;
42
+ connectTimeoutMs?: number;
43
+ /** Datagram budget the FEC symbol size is derived from (WP §6.2); default 1024 in browsers, 1150 in Bun/Node. */
44
+ maxDatagram?: number;
45
+ /** Bun/Node: close cleanly on SIGINT/SIGTERM (default true). */
46
+ signals?: boolean;
47
+ }
48
+ /** Where the node should be connected: the argument of `setTarget` (fields left out keep the node's current ones). */
49
+ export interface ConnectTarget {
50
+ url: string;
51
+ type?: ClientType;
52
+ name?: string;
53
+ userId?: string;
54
+ credential?: string;
55
+ brokerCertSha256?: string;
56
+ /** The relay token for *this* target; left out = no token (a token for another runtime would be refused). */
57
+ token?: string;
58
+ }
59
+ export type ConnectionState = {
60
+ status: "connecting";
61
+ attempt: number;
62
+ } | {
63
+ status: "connected";
64
+ url: string;
65
+ sinceMs: number;
66
+ } | {
67
+ status: "reconnecting";
68
+ attempt: number;
69
+ reason: string;
70
+ error: SynapseError;
71
+ nextRetryAtMs: number;
72
+ } | {
73
+ status: "closing";
74
+ } | {
75
+ status: "closed";
76
+ reason: string;
77
+ } | {
78
+ status: "failed";
79
+ error: SynapseError;
80
+ };
81
+ /** This node's identity and the current session's hello facts; empty strings between sessions. */
82
+ export interface Info {
83
+ name: string;
84
+ type: ClientType;
85
+ userId?: string;
86
+ prefix: string;
87
+ role: Role;
88
+ dom: TimeDomain;
89
+ brokerDom: string;
90
+ brokerVersion: string; /** The robot the runtime serves (fixed for the runtime's life; empty between sessions). */
91
+ robot: string; /** The runtime's name in its cluster, from the current session's hello (empty between sessions). */
92
+ runtimeName: string; /** The runtime's id, from the current session's hello (empty between sessions): kept across the runtime's restarts. */
93
+ runtimeId: string;
94
+ url: string;
95
+ }
96
+ /** A `promoted`/`demoted` notice on the session (WP §3.5). */
97
+ export type Notice = {
98
+ kind: "promoted";
99
+ role: Role;
100
+ project: string | null;
101
+ } | {
102
+ kind: "demoted";
103
+ role: Role;
104
+ project: string | null;
105
+ };
106
+ export interface PublishOptions {
107
+ /** The frame's `t_us` (this node's clock): the capture time, if you have one; default now. */
108
+ tUs?: number;
109
+ flags?: number;
110
+ }
111
+ /** Where a publisher stands with the runtime on the current session. */
112
+ export type Verdict = {
113
+ status: "unbound";
114
+ } | {
115
+ status: "pending";
116
+ epoch: number;
117
+ } | {
118
+ status: "admitted";
119
+ epoch: number;
120
+ };
121
+ export interface PublisherStats {
122
+ published: number;
123
+ droppedDisconnected: number;
124
+ fec?: {
125
+ blocks: number;
126
+ plain: number;
127
+ superseded: number;
128
+ };
129
+ }
130
+ export interface Publisher<TIn> extends AsyncDisposable {
131
+ readonly topic: string;
132
+ readonly verdict: Verdict;
133
+ /** Resolves once admitted and announced on the current session; `not_connected` once the node ended. A refusal is asked again, paced and warned about. */
134
+ ready(): Promise<void>;
135
+ /**
136
+ * Synchronous: writes to the installed track, or drops and counts while unbound (SDK §12.4). A
137
+ * msgpack topic takes a value; a raw topic takes bytes, sent as given; on a `video/*` topic the
138
+ * bytes are one access unit (Annex-B for `video/h264`), sent as a `MEDIA` frame whose
139
+ * `KEY`/`CONFIG`/`RECOVERY` flags are read off the bitstream.
140
+ */
141
+ publish(value: TIn, opts?: PublishOptions): void;
142
+ stats(): PublisherStats;
143
+ close(): void;
144
+ }
145
+ export interface Envelope<T> {
146
+ data: T;
147
+ tUs: number;
148
+ seq: number;
149
+ /** The publisher's name. */
150
+ publisher: string;
151
+ /** The publisher's time-domain id. */
152
+ dom: string;
153
+ flags: number;
154
+ type: number;
155
+ tlvs: Tlv[];
156
+ /** Flag `KEY`: an IDR, a decoder may start here (video). */
157
+ key: boolean;
158
+ /** Flag `CONFIG`: the access unit carries SPS/PPS (video). */
159
+ config: boolean;
160
+ /** Flag `RECOVERY`: a recovery point, a gradual start point (video). */
161
+ recovery: boolean;
162
+ }
163
+ export interface SubscribeOptions {
164
+ /** Deliver at most one message per interval, newest wins (skipped counted `coalesced`). */
165
+ throttleMs?: number;
166
+ /** `queueDepth` 1 (default): a read returns the newest message of each publisher with news; 0: every message, in order. */
167
+ queueDepth?: 0 | 1;
168
+ /** MoQ priority 0..7 (default 3). */
169
+ priority?: number;
170
+ /** `stream` topics: how far behind the live edge the relay may serve (ms; default 0 = live edge). */
171
+ maxAgeMs?: number;
172
+ /** Drop a message older than this on arrival (same-domain publishers only). */
173
+ deadlineMs?: number;
174
+ /** `fec` publishers: override the reassembly give-up budget. */
175
+ fecDeadlineMs?: number;
176
+ /** `fec` publishers: send WP §6.4 reports (default true). */
177
+ fecFeedback?: boolean;
178
+ }
179
+ export interface SubStats {
180
+ read: number;
181
+ superseded: number;
182
+ staleDrops: number;
183
+ mismatch: number;
184
+ fecGiveup: number;
185
+ repaired: number;
186
+ coalesced: number;
187
+ }
188
+ export interface Subscription<T> extends AsyncIterable<Envelope<T>[]>, AsyncDisposable {
189
+ readonly topic: string;
190
+ readonly stats: SubStats;
191
+ /** The next batch (never empty): latest per publisher (`queueDepth` 1) or everything ready (0). `undefined` once the node ended. */
192
+ next(): Promise<Envelope<T>[] | undefined>;
193
+ unsubscribe(): void;
194
+ }
195
+ export type ViewStatus = "pending" | "live" | "stale";
196
+ export interface ViewState<S> {
197
+ data: S | undefined;
198
+ status: ViewStatus;
199
+ connection: ConnectionState["status"];
200
+ /** This node's clock when the value changed, µs. */
201
+ lastChangeAtUs: number | undefined;
202
+ }
203
+ export interface ViewOptions<T, S> extends SubscribeOptions {
204
+ /** A value older than this (by its publisher's stamp, on our clock) reads as `stale`. */
205
+ staleTime?: number;
206
+ select?: (v: T) => S;
207
+ }
208
+ /** A topic's value now, as a cell: `get()` any time, `subscribe` for changes (the framework bridge), `changed()` for the next one; subscribed from creation until `close()`. */
209
+ export interface View<S> extends AsyncDisposable {
210
+ get(): ViewState<S>;
211
+ subscribe(listener: () => void): () => void;
212
+ ready(): Promise<void>;
213
+ changed(): Promise<ViewState<S>>;
214
+ close(): void;
215
+ }
216
+ export type RunStatus = "pending" | "running" | "succeeded" | "failed" | "canceled";
217
+ export interface CallOptions<U = unknown> {
218
+ /** End-to-end deadline enforced by the broker (WP §8.4). */
219
+ deadlineMs?: number;
220
+ /** Local: fail `idle_timeout` (and cancel) when no frame arrives for this long. */
221
+ idleTimeoutMs?: number;
222
+ signal?: AbortSignal;
223
+ onUpdate?: (u: U) => void;
224
+ }
225
+ /** A started call (WP §8.3): its updates, its result, and cancel. */
226
+ /** `await run` is `await run.result`. */
227
+ export interface Run<U, R> extends AsyncDisposable, PromiseLike<R> {
228
+ readonly id: string;
229
+ readonly status: RunStatus;
230
+ /** The next progress update; `undefined` once the terminal frame arrived (then `result`). */
231
+ nextUpdate(): Promise<U | undefined>;
232
+ updates(): AsyncIterable<U>;
233
+ readonly result: Promise<R>;
234
+ /** Cooperative cancel: tells the broker, resolves on its `canceled` frame. */
235
+ cancel(reason?: string): Promise<void>;
236
+ }
237
+ export type CallEvent<U, R> = {
238
+ type: "update";
239
+ data: U;
240
+ } | {
241
+ type: "result";
242
+ data: R;
243
+ };
244
+ export interface ServeContext<U> {
245
+ readonly call: string;
246
+ readonly caller: string;
247
+ readonly role: Role;
248
+ readonly deadlineMs: number | undefined;
249
+ /** Aborted when the caller cancels or the registration ends; `reason` names why. */
250
+ readonly signal: AbortSignal;
251
+ update(u: U): void;
252
+ }
253
+ export type ServeHandler<I, U, R> = (input: I, ctx: ServeContext<U>) => R | Promise<R>;
254
+ export interface ServeOptions {
255
+ minRole?: Role;
256
+ description?: string;
257
+ }
258
+ export type RegVerdict = {
259
+ status: "unbound";
260
+ } | {
261
+ status: "pending";
262
+ epoch: number;
263
+ } | {
264
+ status: "registered";
265
+ epoch: number;
266
+ };
267
+ export interface Registration extends AsyncDisposable {
268
+ readonly name: string;
269
+ readonly verdict: RegVerdict;
270
+ /** The current binding id (fresh per session and renewal); `undefined` while unbound. */
271
+ readonly bind: string | undefined;
272
+ ready(): Promise<void>;
273
+ unregister(): void;
274
+ }
275
+ export interface StoreOptions {
276
+ access?: StoreMode;
277
+ onDeath?: StoreOnDeath;
278
+ }
279
+ export interface StoreStatus {
280
+ /** The value is current: connected, nothing of ours pending, every change the runtime sent applied. */
281
+ synced: boolean;
282
+ connected: boolean;
283
+ pending: number;
284
+ mode: StoreMode | undefined;
285
+ owner: string | undefined;
286
+ mine: boolean;
287
+ minRole: Role | undefined;
288
+ onDeath: StoreOnDeath | undefined;
289
+ absent: boolean;
290
+ /** Diagnostics: the last applied version and the bound incarnation. */
291
+ version: number;
292
+ epoch: string | undefined;
293
+ /** `destroyed`, `connection_lost` or `client`. */
294
+ ended: string | undefined;
295
+ }
296
+ export type StoreChange = {
297
+ kind: "put";
298
+ key: string;
299
+ value: unknown;
300
+ previousValue: unknown | undefined;
301
+ by: string | undefined;
302
+ } | {
303
+ kind: "delete";
304
+ key: string;
305
+ previousValue: unknown;
306
+ by: string | undefined;
307
+ };
308
+ /**
309
+ * A namespace as a value kept current (SDK §9): `value`/`get` read it, `set`/`delete` resolve once the
310
+ * value shows the write, `synced` says whether it is current, `watch()` follows changes. The runtime
311
+ * decides every write (`not_owner`, `unauthorized`, `store_gone`).
312
+ */
313
+ export interface StoreHandle extends AsyncDisposable {
314
+ readonly name: string;
315
+ /** The whole value, what the runtime has (the same object while nothing changed). */
316
+ readonly value: Record<string, unknown>;
317
+ get(key: string): unknown;
318
+ readonly synced: boolean;
319
+ readonly status: StoreStatus;
320
+ /** `set(key, value)`, or `set({ ... })` for several keys as one atomic write; resolves once `value` shows it. */
321
+ set(key: string, value: unknown): Promise<void>;
322
+ set(values: Record<string, unknown>): Promise<void>;
323
+ delete(key: string): Promise<void>;
324
+ clear(): Promise<void>;
325
+ /** Ends the namespace for everyone (the owner, or an operator); every handle ends `destroyed`. */
326
+ destroy(): Promise<void>;
327
+ /** The whole value after each change; with `key`, that key's value each time it changes (`undefined` after a delete). */
328
+ watch(key?: string): AsyncIterable<unknown>;
329
+ /** The framework bridge (React): called on every change of the value or the status. */
330
+ subscribe(listener: () => void): () => void;
331
+ close(): void;
332
+ }
333
+ export type Vec3 = [number, number, number];
334
+ /** A rotation as a quaternion `[x, y, z, w]`. */
335
+ export type Quat = [number, number, number, number];
336
+ /** A transform relative to the parent: translation, rotation, scale (each absent = identity). */
337
+ export interface Xform {
338
+ t?: Vec3;
339
+ q?: Quat;
340
+ s?: Vec3;
341
+ }
342
+ /** Which plane an entity lives in: `authored` (the project's), `calibration` (the robot's), `live` (never persisted). */
343
+ export type SceneLayer = "authored" | "calibration" | "live";
344
+ /** Each flag holds only when it is `true`. `locked` refuses patch/reparent/rename/despawn (a flags-only patch passes). */
345
+ export interface SceneFlags {
346
+ locked?: boolean;
347
+ bound?: boolean;
348
+ static?: boolean;
349
+ derived?: boolean;
350
+ }
351
+ /** Written by the runtime on every change of the entity: revision, the runtime's clock (µs), who wrote it. */
352
+ export interface SceneMeta {
353
+ rev: number;
354
+ t: number;
355
+ by: string | null;
356
+ }
357
+ /**
358
+ * One piece of an entity's `visual` or `collision` list (pieces are the geometry unit), placed by its
359
+ * own `xform` inside the entity. The shapes and their fields are the scene spec's §1.2.
360
+ */
361
+ export type Piece = ({
362
+ kind: "box";
363
+ size: Vec3;
364
+ } | {
365
+ kind: "sphere";
366
+ radius: number;
367
+ } | {
368
+ kind: "cylinder";
369
+ radius: number;
370
+ length: number;
371
+ } | {
372
+ kind: "capsule";
373
+ radius: number;
374
+ length: number;
375
+ } | {
376
+ kind: "blob";
377
+ blob: string;
378
+ }) & {
379
+ xform?: Xform;
380
+ color?: string;
381
+ [key: string]: unknown;
382
+ };
383
+ export interface Physics {
384
+ dynamic?: boolean;
385
+ collide?: boolean;
386
+ mass?: number;
387
+ material?: {
388
+ friction?: number;
389
+ restitution?: number;
390
+ [key: string]: unknown;
391
+ };
392
+ [key: string]: unknown;
393
+ }
394
+ /** A robot's declaration: its description (URDF), the topic of its joint states, its control, the frames materialized into the tree. */
395
+ export interface RobotComponent {
396
+ description?: string;
397
+ joints_topic?: string;
398
+ control?: unknown;
399
+ frames?: string[];
400
+ floating_base?: boolean;
401
+ [key: string]: unknown;
402
+ }
403
+ /** A named mounting point of an entity; materialized as a derived frame. */
404
+ export interface Hardpoint {
405
+ name: string;
406
+ mate_type?: string | null;
407
+ xform?: Xform;
408
+ [key: string]: unknown;
409
+ }
410
+ /**
411
+ * An entity of the scene. The fields below are the ones the runtime and the early users read;
412
+ * every other key passes through untouched.
413
+ */
414
+ export interface SceneEntity {
415
+ /** A ULID; never changes. */
416
+ id: string;
417
+ kind: string;
418
+ /** Unique across the authored and calibration planes. */
419
+ name?: string;
420
+ /** The parent's id; `null` is the world root. */
421
+ parent: string | null;
422
+ /** The place among its siblings (lowercase hex; siblings sort by `(order, id)`); absent on live entities. */
423
+ order?: string;
424
+ layer: SceneLayer;
425
+ xform?: Xform;
426
+ flags?: SceneFlags;
427
+ meta?: SceneMeta;
428
+ lifetime_ms?: number;
429
+ visual?: Piece[];
430
+ collision?: Piece[];
431
+ physics?: Physics;
432
+ robot?: RobotComponent;
433
+ /** Joint name → position; on a robot the authored ones are its rest configuration, the live plane overrides them. */
434
+ joints?: Record<string, number>;
435
+ /** Presentation/status the live plane sets per entity. */
436
+ state?: Record<string, unknown>;
437
+ hardpoints?: Hardpoint[];
438
+ /** The entity's tasks (`instance` → declaration): what the runtime runs for it while the project's tasks run. */
439
+ tasks?: Record<string, TaskDeclaration>;
440
+ /** Passed through untouched; the runtime does not read it (`tasks` supersedes it). */
441
+ behaviors?: Record<string, unknown>;
442
+ possession?: Record<string, unknown>;
443
+ labels?: string[];
444
+ props?: Record<string, unknown>;
445
+ asset?: {
446
+ id: string;
447
+ prefab?: string;
448
+ [key: string]: unknown;
449
+ };
450
+ [key: string]: unknown;
451
+ }
452
+ /**
453
+ * An id's live row (`scene.entity(id, { plane: "live" })`), each key present only when the row has it:
454
+ * `entity` is a live-layer entity's upsert row; `pose`, `joints`, `state` are the lanes; `stamp` is the
455
+ * newest lane stamp (the runtime's clock).
456
+ */
457
+ export interface SceneEntityLive {
458
+ entity?: SceneEntity;
459
+ pose?: {
460
+ t: Vec3;
461
+ q: Quat;
462
+ };
463
+ joints?: Record<string, number>;
464
+ state?: Record<string, unknown>;
465
+ stamp?: number;
466
+ }
467
+ /** One node of the hierarchy: ids and ordering only (a pose, rename or patch does not change it). */
468
+ export interface SceneTreeNode {
469
+ id: string;
470
+ children: SceneTreeNode[];
471
+ }
472
+ /**
473
+ * The hierarchy now, as its roots: siblings in `(order, id)` order (an entity without an `order`
474
+ * after the ordered ones); an entity whose parent is not in the scene is a root.
475
+ */
476
+ export type SceneTree = SceneTreeNode[];
477
+ /**
478
+ * A value of the scene, kept current: `get()` the value now (the same object until it changes),
479
+ * `subscribe` for each change (the framework bridge), `changed()` for the next one. It reads the
480
+ * handle's copy of the scene (no subscription of its own) and ends with the handle.
481
+ */
482
+ export interface SceneView<T> {
483
+ get(): T;
484
+ subscribe(listener: () => void): () => void;
485
+ /** Resolves with the value after its next change (with the value as it is when the handle closes). */
486
+ changed(): Promise<T>;
487
+ }
488
+ /** `attached`: the scene of the active project is in; `detached`: none is (no project active, a session gap, between incarnations); `closed`: for good. */
489
+ export type SceneState = "attached" | "detached" | "closed";
490
+ export interface SceneStatus {
491
+ state: SceneState;
492
+ /** Attached, connected, no write of ours in flight, every authored change the runtime sent applied. */
493
+ synced: boolean;
494
+ /** Authoring batches awaiting the runtime's answer. */
495
+ pending: number;
496
+ /** The authored plane's version and hash as this handle has them (0 and "" while detached). */
497
+ version: number;
498
+ hash: string;
499
+ /** `live()` batches dropped because the handle was not attached. */
500
+ detachedDrops: number;
501
+ }
502
+ /** An authored change as the runtime applied it (the `watchChanges` feed). */
503
+ export type SceneDiffOp = {
504
+ op: "spawn";
505
+ entity: SceneEntity;
506
+ } | {
507
+ op: "despawn";
508
+ id: string;
509
+ removed: string[];
510
+ old_parent: string | null;
511
+ old_order: string | null;
512
+ } | {
513
+ op: "patch";
514
+ id: string;
515
+ set: Partial<SceneEntity>;
516
+ rev: number;
517
+ } | {
518
+ op: "reparent";
519
+ id: string;
520
+ parent: string | null;
521
+ order: string;
522
+ xform?: Xform;
523
+ old_parent: string | null;
524
+ old_order: string | null;
525
+ rev: number;
526
+ } | {
527
+ op: "rename";
528
+ id: string;
529
+ name: string;
530
+ old_name: string | null;
531
+ rev: number;
532
+ };
533
+ /** An op of an authoring batch (`apply`): the runtime validates every one. */
534
+ export type SceneOp = {
535
+ op: "spawn";
536
+ entity: Partial<SceneEntity> & {
537
+ id: string;
538
+ };
539
+ } | {
540
+ op: "despawn";
541
+ id: string;
542
+ cascade?: boolean;
543
+ } | {
544
+ op: "patch";
545
+ id: string;
546
+ set: Partial<SceneEntity>;
547
+ } | {
548
+ op: "reparent";
549
+ id: string;
550
+ parent: string | null;
551
+ before?: string;
552
+ after?: string;
553
+ preserve_world?: boolean;
554
+ } | {
555
+ op: "rename";
556
+ id: string;
557
+ name: string;
558
+ };
559
+ /**
560
+ * What `spawn` takes besides the kind: the name, the parent (default the world root), the components.
561
+ * The id is minted here (a ULID) unless `id` is given: a known id kept, as a restore of despawned
562
+ * entities needs (the runtime refuses one already in use, `conflict`).
563
+ */
564
+ export type SceneSpawnOptions = {
565
+ name: string;
566
+ parent?: string | null;
567
+ id?: string;
568
+ } & Partial<Omit<SceneEntity, "id" | "kind" | "name" | "parent" | "order" | "meta">>;
569
+ /** Where `reparent` puts the entity among its new siblings (default: after the last). */
570
+ export type ScenePlace = {
571
+ before: string;
572
+ } | {
573
+ after: string;
574
+ };
575
+ /** One authoring batch being built (`txn`): sent as one atomic `apply`. */
576
+ export interface SceneTxn {
577
+ /** Spawns an entity of `kind`; returns its id. */
578
+ spawn(kind: string, opts: SceneSpawnOptions): string;
579
+ /** Sets components of `id` (an object merges into an object one level deep). */
580
+ patch(id: string, set: Partial<SceneEntity>): void;
581
+ rename(id: string, name: string): void;
582
+ reparent(id: string, parent: string | null, place?: ScenePlace): void;
583
+ despawn(id: string, opts?: {
584
+ cascade?: boolean;
585
+ }): void;
586
+ setLocked(id: string, locked: boolean): void;
587
+ /** The ops as they will be sent. */
588
+ readonly ops: readonly SceneOp[];
589
+ }
590
+ export interface LivePose {
591
+ id: string;
592
+ t: Vec3;
593
+ q: Quat;
594
+ }
595
+ export interface LiveJoints {
596
+ id: string;
597
+ j: Record<string, number>;
598
+ }
599
+ export interface LiveState {
600
+ id: string;
601
+ s: Record<string, unknown>;
602
+ }
603
+ /** One live batch: live entities (`layer: "live"`), lane overrides, removals; `lifetimeMs` makes the override rows it touches expire. */
604
+ export interface LiveBatch {
605
+ upsert?: SceneEntity[];
606
+ poses?: LivePose[];
607
+ joints?: LiveJoints[];
608
+ state?: LiveState[];
609
+ remove?: string[];
610
+ lifetimeMs?: number;
611
+ }
612
+ /** A live row removed: the writer removed it, its lifetime ran out, or its writer left. */
613
+ export interface LiveRemoved {
614
+ id: string;
615
+ by: "explicit" | "timeout" | "writer_gone";
616
+ }
617
+ /**
618
+ * The active project's scene as this node sees it (PLAN v0.24 §5): authored ⊕ live, kept current.
619
+ * Views and feeds read the handle's copy of the scene; every write goes to the runtime, which
620
+ * decides it, and resolves once the entities show it.
621
+ */
622
+ export interface SceneHandle extends AsyncDisposable {
623
+ /** The entities now, authored with the live overrides applied; the same object while nothing changed. */
624
+ readonly entities: Readonly<Record<string, SceneEntity>>;
625
+ /**
626
+ * One entity as a view (`undefined` when it is not in the scene). `plane` reads one layer alone and
627
+ * the view changes only when that layer's value for the id does: `authored` is the entity as
628
+ * declared without the live lanes (the authored entity, or a live entity's upsert row); `live` is
629
+ * the id's live row (`SceneEntityLive`), `undefined` without one. Without `plane`: merged.
630
+ */
631
+ entity(id: string, opts?: {
632
+ plane?: "authored";
633
+ }): SceneView<SceneEntity | undefined>;
634
+ entity(id: string, opts: {
635
+ plane: "live";
636
+ }): SceneView<SceneEntityLive | undefined>;
637
+ entity(id: string, opts?: {
638
+ plane?: "authored" | "live";
639
+ }): SceneView<SceneEntity | SceneEntityLive | undefined>;
640
+ /** The hierarchy as a view. */
641
+ tree(): SceneView<SceneTree>;
642
+ /** The entities for which `where` holds (in id order), as a view. */
643
+ query(where: (e: SceneEntity) => boolean): SceneView<SceneEntity[]>;
644
+ readonly status: SceneStatus;
645
+ /** The authored changes, one batch per applied change. */
646
+ watchChanges(cb: (batch: SceneDiffOp[]) => void): () => void;
647
+ watchChanges(): AsyncIterableIterator<SceneDiffOp[]>;
648
+ /** The live rows removed, one batch per runtime tick that removed some. */
649
+ watchLiveRemovals(cb: (batch: LiveRemoved[]) => void): () => void;
650
+ watchLiveRemovals(): AsyncIterableIterator<LiveRemoved[]>;
651
+ /** The ids of this node's live writes the runtime dropped because another writer owns them, one batch per denied batch. */
652
+ watchLiveDenied(cb: (ids: string[]) => void): () => void;
653
+ watchLiveDenied(): AsyncIterableIterator<string[]>;
654
+ /** The handle's state at each change; ends after `closed`. */
655
+ watchState(cb: (state: SceneState) => void): () => void;
656
+ watchState(): AsyncIterableIterator<SceneState>;
657
+ /** One atomic batch; `base`: the version its structural ops assume. Resolves once the entities show it; a refusal is the runtime's error. Detached: `project_inactive`. */
658
+ apply(ops: SceneOp[], base?: number): Promise<void>;
659
+ /** The ops `build` makes, as one atomic batch. */
660
+ txn(build: (t: SceneTxn) => void): Promise<void>;
661
+ /** Spawns an entity; its id, once the entities show it. */
662
+ spawn(kind: string, opts: SceneSpawnOptions): Promise<string>;
663
+ patch(id: string, set: Partial<SceneEntity>): Promise<void>;
664
+ rename(id: string, name: string): Promise<void>;
665
+ reparent(id: string, parent: string | null, place?: ScenePlace): Promise<void>;
666
+ despawn(id: string, opts?: {
667
+ cascade?: boolean;
668
+ }): Promise<void>;
669
+ setLocked(id: string, locked: boolean): Promise<void>;
670
+ /** Writes one live batch on this node's live track; returns at once (like `publish`). Not attached: dropped and counted (`status.detachedDrops`). */
671
+ live(batch: LiveBatch): void;
672
+ /** The framework bridge (React): called on every change of the entities or the status. */
673
+ subscribe(listener: () => void): () => void;
674
+ /** Ends this handle: its verbs fail `not_connected`, its views and feeds end; the node's other scene handles go on. */
675
+ close(): void;
676
+ }
677
+ /**
678
+ * A declaration: what the runtime runs under an instance name (the robot's: a key of
679
+ * `tasks.declarations`; a project's: an entry of an entity's `tasks`). Whether it has a process to
680
+ * run is the catalog's: a task whose spec has nothing to run is passive (counted, never started).
681
+ */
682
+ export interface TaskDeclaration {
683
+ /** The task's address, as the task tool lists it (`<workspace>:<pkg>:<task>`); never split. */
684
+ ref: string;
685
+ /** `false`: left out of the next run. */
686
+ enabled?: boolean;
687
+ /** Passed to the task: as flags, positionals or environment variables per its spec, and whole as `SYNAPSE_CONFIG` (JSON). */
688
+ config?: Record<string, unknown>;
689
+ /** The catalog's `schemaHash` this declaration was written against; another one fails the run. */
690
+ schema?: string;
691
+ }
692
+ /** How a task's last process ended: its exit code, the signal that ended it, or neither. Times are Unix ms. */
693
+ export type TaskExit = {
694
+ code: number;
695
+ at: number;
696
+ } | {
697
+ signal: number;
698
+ at: number;
699
+ } | {
700
+ at: number;
701
+ };
702
+ /** One running task, as the runtime shows it. */
703
+ export interface TaskStatus {
704
+ scope: "robot" | "project";
705
+ /** The task's address (its declaration's `ref`). */
706
+ addr: string;
707
+ instance: string;
708
+ /** Project tasks: the entity's id. */
709
+ entity?: string;
710
+ /** `down`: its process exited and its restart is pending. */
711
+ state: "up" | "down";
712
+ /** Unix ms of the last change between `up` and `down`. */
713
+ since: number;
714
+ /** How many times its process was started again after an exit. */
715
+ restarts: number;
716
+ /** How its last process ended. */
717
+ exit?: TaskExit;
718
+ /** The file its stdout and stderr go to. */
719
+ log: string;
720
+ }
721
+ /** A scope's run and its tasks. */
722
+ export interface TasksRunStatus<T> {
723
+ /** `undefined`: the runtime has not said yet (not synced, or between sessions). */
724
+ state: "starting" | "running" | "stopping" | "stopped" | undefined;
725
+ /** Unix ms of the last change of `state`. */
726
+ since?: number;
727
+ /** Why it stopped or failed: `project_switch`, or an error code (`preflight_failed`, `launch_failed`, ...). */
728
+ reason?: string;
729
+ tasks: T;
730
+ }
731
+ /** What runs, both scopes: the robot's tasks by instance, the project's by entity id, then instance. */
732
+ export interface TasksStatus {
733
+ robot: TasksRunStatus<Readonly<Record<string, TaskStatus>>>;
734
+ project: TasksRunStatus<Readonly<Record<string, Readonly<Record<string, TaskStatus>>>>>;
735
+ }
736
+ /** One task the runtime knows (from the task tool's workspaces). Fields the task's manifest lacks are absent. */
737
+ export interface TaskCatalogEntry {
738
+ addr: string;
739
+ workspace: string;
740
+ pkg?: string;
741
+ name?: string;
742
+ kind?: string;
743
+ tags?: string[];
744
+ inputs?: JsonSchema;
745
+ schemaHash?: string;
746
+ definedBy?: string;
747
+ }
748
+ /** A value the runtime keeps current that this node only reads: it has no writes. */
749
+ export interface ReadonlyStoreHandle<V> {
750
+ /** The value now; the same object while nothing changed, and an unchanged part keeps its object when another part changes. */
751
+ readonly value: V;
752
+ /** `true` while the value is current. */
753
+ readonly synced: boolean;
754
+ /** Resolves once the value is current (the first time). */
755
+ ready(): Promise<void>;
756
+ /** The whole value after each change. It is state, not history: a reader slower than the changes sees the latest value. */
757
+ watch(): AsyncIterable<V>;
758
+ /** The framework bridge (React): called on every change of the value or of `synced`. */
759
+ subscribe(listener: () => void): () => void;
760
+ }
761
+ /** The robot's task declarations, `instance` → declaration (operators write them). The node owns this handle: it has no `close` or `destroy`. */
762
+ export interface TaskDeclarations extends Omit<StoreHandle, "name" | "value" | "get" | "set" | "watch" | "destroy" | "close" | typeof Symbol.asyncDispose> {
763
+ readonly value: Readonly<Record<string, TaskDeclaration>>;
764
+ get(instance: string): TaskDeclaration | undefined;
765
+ /** `set(instance, declaration)`, or `set({ ... })` for several as one atomic write; waits for the value to be current first, resolves once `value` shows the write. */
766
+ set(instance: string, declaration: TaskDeclaration): Promise<void>;
767
+ set(declarations: Record<string, TaskDeclaration>): Promise<void>;
768
+ /** The whole value after each change; with `instance`, that declaration each time it changes (`undefined` after a delete). */
769
+ watch(instance?: string): AsyncIterable<unknown>;
770
+ /** Resolves once the value is current (the first time). */
771
+ ready(): Promise<void>;
772
+ }
773
+ /**
774
+ * `node.tasks`: the robot's task declarations, what runs (`status`), what the runtime can run
775
+ * (`catalog`), and the verbs. Running and stopping need the operator role. A run's reply is the
776
+ * runtime's answer; what runs afterwards is `status`.
777
+ */
778
+ export interface Tasks {
779
+ readonly declarations: TaskDeclarations;
780
+ readonly status: ReadonlyStoreHandle<TasksStatus>;
781
+ /** By address. */
782
+ readonly catalog: ReadonlyStoreHandle<Readonly<Record<string, TaskCatalogEntry>>>;
783
+ /**
784
+ * Starts the robot's tasks (the enabled declarations). Every declaration is checked first and nothing starts unless all
785
+ * pass: a failed check rejects `preflight_failed`, a process that could not start `launch_failed` (what had started is
786
+ * stopped); both carry the report ({@link isTasksRefusal}). `already_running` while they run. Robot tasks keep running
787
+ * across project switches.
788
+ */
789
+ runRobotTasks(): Promise<TasksRun>;
790
+ /** Stops every robot task (also when none ran). Project tasks are not touched. */
791
+ stopRobotTasks(): Promise<void>;
792
+ /** Starts the active project's tasks (the enabled `tasks` of its scene's entities), as {@link runRobotTasks}; `project_inactive` with no active project. A project switch stops them. */
793
+ runProjectTasks(): Promise<TasksRun>;
794
+ /** Stops every project task (also when none ran). Robot tasks are not touched. */
795
+ stopProjectTasks(): Promise<void>;
796
+ }
797
+ export interface Catalog {
798
+ readonly topics: Readonly<Record<string, {
799
+ desc?: TrackDesc;
800
+ publishers: Record<string, {
801
+ name: string;
802
+ bind: string;
803
+ }>;
804
+ }>>;
805
+ readonly services: Readonly<Record<string, {
806
+ name: string;
807
+ prefix: string;
808
+ card: Card;
809
+ builtin: boolean;
810
+ }>>;
811
+ /** The listed card of `service`, if any. */
812
+ describe(service: string): Card | undefined;
813
+ }
814
+ export interface Metrics {
815
+ sessions: number;
816
+ reconnects: number;
817
+ callsSent: number;
818
+ callsAnswered: number;
819
+ }
820
+ export type { JsonSchema };