super-dsh 0.1.4 → 0.1.5

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.
Files changed (55) hide show
  1. package/README.md +6 -7
  2. package/agent-agy/dist/world-plugin.js +18 -33
  3. package/agent-codex/dist/world-plugin.js +18 -33
  4. package/agent-hermes/dist/world-plugin.js +18 -34
  5. package/agent-omp/dist/world-plugin.js +16 -32
  6. package/agent-pi/dist/world-plugin.js +18 -33
  7. package/client/index.js +2086 -0
  8. package/cordis.patch.yml +20 -7
  9. package/package.json +13 -3
  10. package/superd-hub/dist/agent-roster.d.ts +3 -1
  11. package/superd-hub/dist/agent-roster.js +4 -2
  12. package/superd-hub/dist/carrier.d.ts +41 -28
  13. package/superd-hub/dist/carrier.js +148 -202
  14. package/superd-hub/dist/fusion-transport.d.ts +148 -0
  15. package/superd-hub/dist/fusion-transport.js +397 -0
  16. package/superd-hub/dist/gateway.d.ts +7 -2
  17. package/superd-hub/dist/gateway.js +28 -2
  18. package/superd-hub/dist/hub-route.d.ts +74 -0
  19. package/superd-hub/dist/hub-route.js +188 -0
  20. package/superd-hub/dist/index.d.ts +10 -4
  21. package/superd-hub/dist/index.js +10 -4
  22. package/superd-hub/dist/roster.d.ts +2 -2
  23. package/superd-hub/dist/spawn-world.js +5 -3
  24. package/superd-hub/dist/switch-core.d.ts +630 -0
  25. package/superd-hub/dist/switch-core.js +734 -0
  26. package/superd-hub/dist/web-trust.d.ts +5 -5
  27. package/superd-hub/dist/web-trust.js +5 -5
  28. package/superd-hub/dist/world-entry.d.ts +20 -13
  29. package/superd-hub/dist/world-entry.js +15 -19
  30. package/superd-hub/dist/world-host.d.ts +15 -20
  31. package/superd-hub/dist/world-host.js +25 -4
  32. package/superd-hub/dist/world-join.js +14 -29
  33. package/superd-hub/dist/world-mount.d.ts +48 -26
  34. package/superd-hub/dist/world-mount.js +59 -27
  35. package/superd-hub/dist/world-provision.js +11 -4
  36. package/superd-hub/dist/world-web-server.d.ts +57 -106
  37. package/superd-hub/dist/world-web-server.js +61 -216
  38. package/superd-hub/lib/client/index.js +2086 -0
  39. package/superd-hub/package.json +12 -0
  40. package/agent-agy/dist/agent-preset-projection.d.ts +0 -31
  41. package/agent-agy/dist/agent-preset-projection.js +0 -33
  42. package/agent-claude/dist/agent-preset-projection.js +0 -30
  43. package/agent-codex/dist/agent-preset-projection.js +0 -37
  44. package/agent-codex/dist/app-home.js +0 -65
  45. package/agent-codex/dist/spike.js +0 -46
  46. package/agent-hermes/dist/agent-preset-projection.js +0 -34
  47. package/agent-omp/dist/agent-preset-projection.js +0 -36
  48. package/agent-omp/dist/app-home.js +0 -86
  49. package/agent-pi/dist/agent-preset-projection.js +0 -29
  50. package/superd-hub/dist/client-shim.d.ts +0 -28
  51. package/superd-hub/dist/client-shim.js +0 -251
  52. package/superd-hub/dist/index-pass.d.ts +0 -22
  53. package/superd-hub/dist/index-pass.js +0 -47
  54. package/superd-hub/dist/labels.d.ts +0 -45
  55. package/superd-hub/dist/labels.js +0 -83
@@ -0,0 +1,630 @@
1
+ /**
2
+ * Switch orchestration core (native UI fusion, plan Task 4) — the pure logic
3
+ * behind the in-page agent switch: storage-namespace key computation, the
4
+ * session landing decision over the persisted `dsh.sessions.current`
5
+ * selection, the agent-selector click-interception decision (a fusion page
6
+ * never navigates on a left click — spec: 融合页 = state 切换,拦截 click,无导航;
7
+ * T-B: consumed by the Hub UI surfaces, the sole switch surface since the
8
+ * RuntimeSeat retirement), and the fallback state machine (timeout or
9
+ * roster-probe failure → revert to native + a visible toast, never a page
10
+ * lock).
11
+ *
12
+ * Host-half on purpose: plain TS with every effect injected through
13
+ * {@link SwitchHost} — no DOM, no client imports — so `test/client-switch.test.mjs`
14
+ * imports it from `dist/` like every other host module. The client half
15
+ * (`src/client/switch.ts`) builds the real host face from the client context:
16
+ * the `__SUPERD_UI_STATE__` atom (Task 3), `ctx.connection`,
17
+ * `ctx.uiWorkspace`, the Task-3 namespaced `Storage`, the control-plane
18
+ * roster fetch, and a minimal self-owned toast overlay.
19
+ *
20
+ * Orchestration sequence (spec §3.5 as amended 2026-09-26, T-A + T-F
21
+ * errata; user rulings 7/8 and the T-F ruling "按原生的语义去理解…把这个
22
+ * last visit session 也做一下"):
23
+ * 1. `setAgent(label)` — state + NS swap immediately; subsequent requests
24
+ * demux to the target world (Task 3's call-time patches read the atom).
25
+ * 2. `reconnect()` — the generation re-opens against the retargeted data
26
+ * plane; the gateway emits `connection/reset` on establishment, the
27
+ * designed "wire-derived caches must repull" signal, so sessions /
28
+ * config / theme re-derive on their own. Nothing navigates before the
29
+ * repull: the chat area is never cleared and no loading skeleton is
30
+ * inserted — old content stays in place awaiting the landing.
31
+ * 3. Session landing (T-F: NATIVE restore semantics) — upstream's
32
+ * fresh-load restore is `watchNavigation`/`restoreSelection`
33
+ * (navigation.ts:284-347): it fires only when BOTH `workspaces.list`
34
+ * and `sessions.list` reached phase `'ready'`, and then walks
35
+ * `saved.subagentAddress` → `saved.sessionId` looked up in the FRESH
36
+ * `sessions.byId` (stale-proof) → recent-workspace → the fresh-boot
37
+ * default workspace. The landing replicates exactly that: it fires once
38
+ * when BOTH wire-derived caches have installed fresh post-establishment
39
+ * baselines ({@link observeCachesRefreshed} — the T-A gate watched
40
+ * `workspaces.list` alone, so a plane whose sessions list landed later
41
+ * served stale rows to the decision and broke native's own
42
+ * last-visited restore; T-G additionally holds the gate while a
43
+ * persisted pointer has not yet joined the freshly minted INCREMENTAL
44
+ * sessions list — membership via `byId` — firing on membership or on
45
+ * the quiet-window ladder, {@link LANDING_QUIET_MS}, so partial-list
46
+ * first edges stop laddering every switch into a brand-new session),
47
+ * then reads the target NS's persisted
48
+ * `dsh.sessions.current` (the NS swap has long since happened) and
49
+ * executes {@link planRestoreLanding}'s precedence through the public
50
+ * faces. The landing is BOUNDED: if the baselines never install within
51
+ * one further fallback window after the settle, a staleness watchdog
52
+ * disarms it — an armed landing never outlives its switch by more than
53
+ * that window (the safe direction: no landing at all beats a late
54
+ * surprise navigation).
55
+ * 4. Fallback: no generation within {@link SWITCH_FALLBACK_MS}, or a failed
56
+ * roster probe → `setAgent('native')` + `reconnect()` back to the native
57
+ * plane + toast. A revert also cancels a pending landing — a
58
+ * reverted switch never lands.
59
+ *
60
+ * Upstream anchors (dsh-v0.1.7-rc.1):
61
+ * - `ctx.connection.reconnect()` — `packages/client/connection/src/client/index.ts`
62
+ * `ConnectionHandle.reconnect` (declared :148, wired :264-266); the
63
+ * controller resets retry progression and replaces the current attempt
64
+ * immediately (`connection.ts:113-121`).
65
+ * - `connection/reset` — emitted by the gateway client on every generation
66
+ * establishment (`packages/api/gateway/src/client/index.ts:169`),
67
+ * declared as the repull signal
68
+ * (`packages/client/connection/src/client/index.ts:9-17`).
69
+ * - `uiWorkspace.openSession(target)` / `startSession(workspaceId?)` /
70
+ * `openWorkspace(workspaceId)` — the navigation face
71
+ * (`packages/client/ui-workspace/src/client/navigation.ts:34-92`);
72
+ * `SessionTarget = SessionId | SubagentAddress`
73
+ * (`packages/api/session-controller/src/client/contract/sessions.ts:22`).
74
+ * - `watchNavigation` / `restoreSelection` — the fresh-load restore this
75
+ * landing replicates (`navigation.ts:284-347`): both-lists ready gate
76
+ * (:292), `subagentAddress` first (:320-323), the fresh-`byId` session
77
+ * lookup with the blank-reuse nuance (:324-335), `recentWorkspace`
78
+ * fallback (:337, the tie-break at :434-453), and
79
+ * `initializeDefaultWorkspace` on a fresh boot (:338-342, :349-361).
80
+ * - `startSession()`'s workspace resolution — `workspaceId ??
81
+ * currentWorkspaceId ?? recent` over the wire-derived `workspaces.list`
82
+ * / `sessions.list` snapshots (`navigation.ts:223-238`); `connectWorkspace`
83
+ * → `reuseOrCreateBlank` reuses the pointer's own blank whenever the
84
+ * private restore path would (:179-198).
85
+ * - The wire-derived stores: `workspaces.list` installs fresh data only
86
+ * through its follow-stream baseline (`packages/api/workspace-controller/src/client/model.ts`
87
+ * `replaceBaseline` → `installViews`, a new items array every install);
88
+ * `sessions.list` is projected with a fresh `ids` array on every manager
89
+ * change (`packages/api/session-controller/src/client/sessions/service.ts:607-677`)
90
+ * — array identity is the freshness token for both.
91
+ * - `dsh.sessions.current` — the per-tab last-selection store
92
+ * (`navigation.ts:130-131`); its restore reads `subagentAddress` first,
93
+ * then `sessionId` (`navigation.ts:319-330`) — {@link parseLandingTarget}
94
+ * keeps that precedence. The store hydrates once at page boot and
95
+ * persists on write (`packages/client/store/src/index.ts:155-174`), so
96
+ * the landing read must go through the namespaced `Storage` (Task 3
97
+ * patch 5), which prefixes `'<label>:'` at call time.
98
+ *
99
+ * @module @pgmi-builds/superd-hub/switch-core
100
+ */
101
+ /** The active-agent label meaning the native (ctx0) data plane. */
102
+ export declare const NATIVE_AGENT = "native";
103
+ /** The persisted last-selection key ui-workspace owns (survey §2.6). */
104
+ export declare const SESSIONS_CURRENT_KEY = "dsh.sessions.current";
105
+ /**
106
+ * The fallback window: how long a switch may wait for the target world's
107
+ * generation (mux re-open + repull) before reverting to native. A
108
+ * human-scale constant; Task 5's config face may expose it later.
109
+ */
110
+ export declare const SWITCH_FALLBACK_MS = 10000;
111
+ /**
112
+ * The membership-wait quiet window (T-G): after both navigation caches are
113
+ * fresh, a persisted pointer the sessions list has not shown yet holds the
114
+ * landing gate — the list is minted INCREMENTALLY post-reconnect (upstream
115
+ * `handleSessionAdded` applies one session at a time, each application
116
+ * re-projecting the store with a fresh `ids` array, service.ts:677), so the
117
+ * first fresh edge sees a partial `byId` that legitimately lacks the
118
+ * pointer's session. The gate fires on membership; while the pointer is
119
+ * still absent, this window of list SILENCE (no `ids` identity change) is
120
+ * the concession point: the gate fires anyway and the plan takes the ladder
121
+ * — an honest miss, because a stored blank pointer legitimately never
122
+ * appears (matching upstream's own boot restore of a stale/absent id). An
123
+ * identity change during the window restarts it (the list is still growing).
124
+ * The machine's post-settle staleness watchdog remains the hard bound on
125
+ * the whole wait.
126
+ */
127
+ export declare const LANDING_QUIET_MS = 600;
128
+ /**
129
+ * The storage namespace for an active agent: native prefixes nothing
130
+ * (byte-identical to stock behavior), a foreign agent owns `'<label>:'`.
131
+ * Mirrors Task 3's storage patch (`ns()` in fusion-transport).
132
+ */
133
+ export declare function storageNamespace(label: string): string;
134
+ /** The full storage key for `key` while `label` is the active agent. */
135
+ export declare function namespacedKey(label: string, key: string): string;
136
+ /**
137
+ * The persisted-selection parse feeding {@link planRestoreLanding}.
138
+ *
139
+ * A value is navigable when it parses to a ui-workspace `MainSelection`
140
+ * carrying a usable `SessionTarget`: a non-null `subagentAddress` object
141
+ * wins (upstream restore order), else a non-empty string `sessionId`.
142
+ * Everything else — no value, a read failure (surfaced here as `null`), a
143
+ * parse failure, an unusable shape — parses to `null` and the plan falls to
144
+ * the recent-workspace / default-workspace ladder.
145
+ *
146
+ * @returns the subagent address object, the session id string, or `null`.
147
+ */
148
+ export declare function parseLandingTarget(raw: string | null | undefined): string | object | null;
149
+ /** Structural slice of one `workspaces.list` row the landing plan reads. */
150
+ export interface RestoreWorkspaceView {
151
+ readonly workspaceId: string;
152
+ readonly sessionIds: readonly string[];
153
+ readonly createdAt?: string;
154
+ }
155
+ /** Structural slice of one `sessions.list` `byId` row the landing plan reads. */
156
+ export interface RestoreSessionSummary {
157
+ readonly id: string;
158
+ readonly blank?: boolean;
159
+ readonly updatedAt?: number;
160
+ }
161
+ /**
162
+ * The FRESH wire-derived navigation caches the landing decides over —
163
+ * upstream `restoreSelection` receives exactly these two snapshots once both
164
+ * phases are ready (navigation.ts:290-292, :318). The structural slices keep
165
+ * this module host-half: the client binding projects the real
166
+ * `ctx.workspaces.list` / `ctx.sessions.list` snapshots into it.
167
+ */
168
+ export interface RestoreCaches {
169
+ readonly workspaces: {
170
+ readonly items: readonly RestoreWorkspaceView[];
171
+ readonly phase: unknown;
172
+ };
173
+ readonly sessions: {
174
+ readonly ids: readonly string[];
175
+ readonly byId: Readonly<Record<string, RestoreSessionSummary>>;
176
+ readonly phase: unknown;
177
+ };
178
+ }
179
+ /** One landing action, in upstream `restoreSelection`'s order of outcomes. */
180
+ export type RestoreLandingPlan = {
181
+ kind: 'open-session';
182
+ target: string | object;
183
+ }
184
+ /**
185
+ * The new-session flow through the public `uiWorkspace.startSession` face:
186
+ * with a workspace id it connects THAT workspace (its `reuseOrCreateBlank`
187
+ * reuses the pointer's own blank exactly when upstream's private
188
+ * `reuseBlank` pre-check would pass); `undefined` inherits the
189
+ * current-then-recent resolution.
190
+ */
191
+ | {
192
+ kind: 'start-session';
193
+ workspaceId?: string;
194
+ }
195
+ /** Upstream's `initializeDefaultWorkspace`: the fresh-boot empty-plane case. */
196
+ | {
197
+ kind: 'default-workspace';
198
+ };
199
+ /**
200
+ * The landing decision with NATIVE restore semantics (T-F): replicate
201
+ * upstream `restoreSelection` (navigation.ts:318-347) against the fresh
202
+ * caches and the parsed persisted selection ({@link parseLandingTarget}'s
203
+ * result, which already applies the `subagentAddress`-first precedence).
204
+ *
205
+ * - `subagentAddress` → open it directly, no cache consultation
206
+ * (navigation.ts:320-323).
207
+ * - `sessionId` found in the FRESH `byId`: non-blank — or blank but
208
+ * workspace-less — opens the id (navigation.ts:324-329); blank with a
209
+ * workspace resolves to that workspace (navigation.ts:333-335's private
210
+ * `reuseBlank` + :337's `workspace.workspaceId` target — the public
211
+ * `startSession(workspaceId)` face covers both: its `connectWorkspace`
212
+ * → `reuseOrCreateBlank` reuses the pointer's own blank whenever the
213
+ * private pre-check would pass, because the eligibility scan is the
214
+ * same).
215
+ * - A stale id (absent from the fresh `byId`) falls through to the
216
+ * no-pointer ladder exactly as upstream's `summary === undefined` does.
217
+ * - No pointer / stale pointer: the recent workspace gets the new-session
218
+ * flow (navigation.ts:337); both lists empty is the fresh boot and
219
+ * mirrors `initializeDefaultWorkspace` (navigation.ts:338-342).
220
+ * - `caches` unavailable (the host's read failed) → `undefined`: land
221
+ * nothing — no landing beats a wrong one. Same for the degenerate
222
+ * zero-workspaces-with-sessions shape, where upstream simply leaves the
223
+ * view as it is (no target, both-empty guard false, :337-342).
224
+ *
225
+ * @param parsed - the persisted-selection parse ({@link parseLandingTarget}).
226
+ * @param caches - the fresh both-lists snapshots, or `undefined` unreadable.
227
+ * @returns the action to execute, or `undefined` to land nothing.
228
+ */
229
+ export declare function planRestoreLanding(parsed: string | object | null, caches: RestoreCaches | undefined): RestoreLandingPlan | undefined;
230
+ /** The structural face of one wire-derived list store (workspaces or sessions). */
231
+ export interface NavigationCacheFace {
232
+ /**
233
+ * Read the current snapshot: the `items` ARRAY IDENTITY is the freshness
234
+ * token. For `workspaces.list` this is the snapshot's items array; for
235
+ * `sessions.list` the client binding adapts the projection's `ids` array
236
+ * (a fresh array is minted on every projection — service.ts:607-677).
237
+ */
238
+ getSnapshot(): {
239
+ items: readonly unknown[];
240
+ phase: unknown;
241
+ };
242
+ /** Subscribe to snapshot invalidation; the returned disposer stops it. */
243
+ subscribe(listener: () => void): () => void;
244
+ /**
245
+ * T-G membership face: does the store's CURRENT snapshot contain `id` as
246
+ * a listed session? The client binding answers from the sessions
247
+ * projection's `byId` (the same face upstream's boot restore resolves the
248
+ * pointer against, navigation.ts:324) — NOT from `ids`, and deliberately
249
+ * not from a stale copy: `byId` can gain a row without a new `ids`
250
+ * identity (publishRetention re-sets `byId` alone, service.ts:565), so
251
+ * the face must read the live snapshot at call time. Optional; without
252
+ * it the gate can never observe membership and the quiet window becomes
253
+ * the pointer's only concession point (the watchdog still bounds it).
254
+ */
255
+ contains?(id: string): boolean;
256
+ }
257
+ /** The structural face of the connection generation observable. */
258
+ export interface GenerationFace {
259
+ /** The live generation, or `undefined` while no attempt has succeeded. */
260
+ getSnapshot(): unknown;
261
+ /** Subscribe to generation changes; the returned disposer stops it. */
262
+ subscribe(listener: () => void): () => void;
263
+ }
264
+ /**
265
+ * What the membership-aware gate needs beyond the two lists (T-G), all
266
+ * injected so this module stays host-half pure. Every field is optional:
267
+ * an absent/empty options object restores the T-F gate verbatim (fire on
268
+ * the first fresh edge of both lists — the no-pointer fresh-boot path).
269
+ */
270
+ export interface LandingGateOptions {
271
+ /**
272
+ * Read the landing pointer's plain session id at DECISION time — on every
273
+ * gate check, not once at arming. `undefined` means no navigable pointer
274
+ * OR a subagent-address pointer (which opens directly, upstream
275
+ * navigation.ts:320-323, and needs no cache consultation): either way the
276
+ * gate falls back to the T-F first-fresh-edge behavior. A throwing read
277
+ * is an honest `undefined` (the machine's own landing read treats a
278
+ * storage failure as `null` → the same ladder).
279
+ */
280
+ pointerSessionId?(): string | undefined;
281
+ /** The quiet window in ms; defaults to {@link LANDING_QUIET_MS}. */
282
+ quietMs?: number;
283
+ /** Timer injection for the quiet window (the SwitchHost faces in practice). */
284
+ setTimeout?(fn: () => void, ms: number): unknown;
285
+ clearTimeout?(handle: unknown): void;
286
+ }
287
+ /**
288
+ * The caches-fresh gate behind the deferred landing (T-F; T-G made it
289
+ * pointer-membership-aware): fires `listener` AT MOST once, when BOTH
290
+ * navigation caches demonstrably hold the TARGET plane's data — the
291
+ * client-face equivalent of upstream's both-lists ready gate
292
+ * (`watchNavigation`, navigation.ts:292: `workspace.phase !== 'ready'
293
+ * || sessions.phase !== 'ready'` → return).
294
+ *
295
+ * The freshness token is each snapshot's `items` ARRAY IDENTITY: upstream's
296
+ * workspace model mints a new array on every baseline install
297
+ * (`installViews` → `[...installed.values()]`, model.ts:398) and the session
298
+ * list projection mints a fresh `ids` array on every manager change
299
+ * (service.ts:607-677), while carrier churn keeps references. The gate arms
300
+ * at the first generation establishment OBSERVED BY ITS OWN subscription —
301
+ * the anchors are retaken there, and nothing before that point can pass, so
302
+ * the dying old plane's last increments racing the reconnect are absorbed;
303
+ * the first replacements after establishment are the target plane's
304
+ * baselines. The `phase === 'ready'` checks ride along as a guard against a
305
+ * page whose first baseline never arrived. Both edges are tracked
306
+ * independently, in either arrival order (the T-A gap: watching workspaces
307
+ * alone let a late sessions baseline leave the pointer lookup reading stale
308
+ * `byId` rows).
309
+ *
310
+ * T-G — the pointer holds the gate. The sessions list is minted
311
+ * INCREMENTALLY after the reconnect (one `handleSessionAdded` per session,
312
+ * each re-projecting the store with a fresh `ids` array), so the first fresh
313
+ * edge sees a partial `byId` that legitimately lacks the pointer's session;
314
+ * consuming the landing there laddered every switch into a brand-new
315
+ * session (the browser-proven defect). When a pointer exists (the injected
316
+ * {@link LandingGateOptions.pointerSessionId} face, read at decision time),
317
+ * the gate therefore does NOT fire on the first fresh edge. It fires when:
318
+ * - the sessions list CONTAINS the pointer's session — the membership
319
+ * face (`byId`, consulted on EVERY sessions notification: `byId` can
320
+ * gain a row without a new `ids` identity, service.ts:565), or
321
+ * - the sessions list has gone QUIET — no `ids` identity change for the
322
+ * configured quiet window ({@link LANDING_QUIET_MS}; an identity change
323
+ * during the window restarts it) while the pointer is still absent:
324
+ * the honest miss, firing with the ladder exactly like upstream's boot
325
+ * restore of a stored id its fresh list does not know.
326
+ * No pointer → the first fresh edge fires, byte-identical to T-F (the
327
+ * fresh-boot path). The machine's post-settle staleness watchdog bounds the
328
+ * whole membership wait: it disposes this gate, which also cancels a
329
+ * running quiet window.
330
+ *
331
+ * Establishment strictly precedes any target-plane baseline (the follow
332
+ * streams re-open on top of the established generation), so a fire implies
333
+ * both "generation established" and "repull landed" — the two preconditions
334
+ * of a correct landing resolution.
335
+ *
336
+ * Host-half pure logic: the client binding composes it with the real
337
+ * `ctx.workspaces.list` + `ctx.sessions.list` stores and the connection
338
+ * generation observable; the machine consumes it through
339
+ * {@link SwitchHost.onCachesRefreshed}.
340
+ *
341
+ * @returns the disposer stopping all three subscriptions (and the quiet window).
342
+ */
343
+ export declare function observeCachesRefreshed(workspaces: NavigationCacheFace, sessions: NavigationCacheFace, generation: GenerationFace, listener: () => void, gate?: LandingGateOptions): () => void;
344
+ /** The decision one roster-entry left click produces in the agent selector. */
345
+ export interface SeatClickDecision {
346
+ /**
347
+ * `true` = the anchor's default action must be prevented: a fusion page
348
+ * never navigates on a left click (the href survives only for middle-click
349
+ * / open-in-new-tab / copy link). `false` = the world-page arm — the URL is
350
+ * the state and the browser owns navigation, byte-identical to stock.
351
+ */
352
+ preventDefault: boolean;
353
+ /** Whether the switch orchestration was invoked for the clicked entry. */
354
+ attempted: boolean;
355
+ }
356
+ /** The inputs of one seat click: the page mode, the entry, the attempt. */
357
+ export interface SeatClickInput {
358
+ /** The page carries the baked `__SUPERD_UI_FUSION__` flag global. */
359
+ fusion: boolean;
360
+ /** The clicked roster entry is the currently active agent. */
361
+ active: boolean;
362
+ /**
363
+ * The switch attempt (`attemptSwitch`). Invoked exactly when
364
+ * `fusion && !active`; its return value is recorded but can never re-open
365
+ * navigation — an unbound orchestration swallows the click instead of
366
+ * abandoning the fused page (review round 1).
367
+ */
368
+ attempt: () => boolean;
369
+ }
370
+ /**
371
+ * The agent-selector click-interception contract as a pure table (native UI
372
+ * fusion; originally landed for the sidebar-bottom RuntimeSeat, which T-B
373
+ * retired — the Hub UI surfaces (HubPage rows + the HubPanelIcon chevron
374
+ * menu) are now the sole switch surface and consume this SAME table, so the
375
+ * contract below is inherited verbatim).
376
+ *
377
+ * - World page (`fusion: false`): no interception, no orchestration — the
378
+ * real hyperlink navigates (spec §5 双半边).
379
+ * - Fusion page, active entry: intercepted, no attempt — switching to the
380
+ * active agent is a no-op, and the click must never fall through to the
381
+ * anchor (an `href="/"` reload would discard the in-page switch state; a
382
+ * foreign `href` would abandon the fused page entirely).
383
+ * - Fusion page, inactive entry: intercepted and the attempt runs. The
384
+ * attempt's `false` (orchestration unbound) still keeps the page: a
385
+ * swallowed click beats silent navigation away from the fusion surface.
386
+ *
387
+ * Host-half pure logic so `test/client-switch.test.mjs` pins the table from
388
+ * `dist/`; the Hub surfaces map `preventDefault` onto the DOM event.
389
+ */
390
+ export declare function seatClickDecision(input: SeatClickInput): SeatClickDecision;
391
+ /** The inputs of one Hub UI roster row's active indication. */
392
+ export interface HubRowActiveInput {
393
+ /** The page carries the baked `__SUPERD_UI_FUSION__` flag global. */
394
+ fusion: boolean;
395
+ /** The row's roster key (`'native'` or a runtime key). */
396
+ agentKey: string;
397
+ /** The row's mount path. */
398
+ agentPath: string;
399
+ /** The page atom's active agent (fusion pages; absent otherwise). */
400
+ activeAgent?: string;
401
+ /** The page's normalized mount path (world pages). */
402
+ mountPath: string;
403
+ }
404
+ /**
405
+ * Which Hub UI roster row shows as the active agent (T-B: the Hub surfaces —
406
+ * HubPage rows and the HubPanelIcon chevron menu — are the sole switch
407
+ * surface after the RuntimeSeat retirement).
408
+ *
409
+ * - Fusion page: the page atom's `activeAgent` — the switch state — decides
410
+ * by roster key; the URL is irrelevant on a fused page. The `native`
411
+ * entry participates like any other key.
412
+ * - Non-fusion page (flag off; the UI's navigation arm is retired since
413
+ * T-C): the mount path is the selection — the entry whose path equals
414
+ * the page's normalized mount. The pure arm survives as contract; no
415
+ * served page can hit the foreign case anymore.
416
+ *
417
+ * Host-half pure logic so `test/client-switch.test.mjs` pins it from `dist/`.
418
+ */
419
+ export declare function hubRowActive(input: HubRowActiveInput): boolean;
420
+ /** The degradation hint a Hub UI surface shows while the flag is off (T-E). */
421
+ export declare const FUSION_OFF_HINT = "fusion off \u2014 no foreign entry";
422
+ /** What one Hub UI surface renders as its switch affordance (T-E closeout). */
423
+ export interface SwitchAffordance {
424
+ /** Whether the in-place switch affordance may render at all. */
425
+ available: boolean;
426
+ /** The degradation hint; non-empty exactly when {@link available} is false. */
427
+ hint: string;
428
+ }
429
+ /**
430
+ * The flag-off shape of the Hub UI switch surfaces (path-separation Part 2
431
+ * T-E closeout). With the per-label URL family retired (T-C) and the world
432
+ * web UIs disabled (T-D), a flag-OFF ctx0 page has NO foreign entry at all —
433
+ * and since T-C the Hub UI ships no hyperlinks either, so the smallest honest
434
+ * rendering is an inert roster with a visible degradation hint instead of a
435
+ * silently dead control:
436
+ *
437
+ * - `available: true` (fusion page): the affordance renders — the HubPage
438
+ * `switch` action and the HubPanelIcon chevron menu.
439
+ * - `available: false` (flag off): the affordance is NOT rendered; the
440
+ * roster stays informational and the surface shows the degradation hint
441
+ * ({@link FUSION_OFF_HINT}) once (HubPage: a single chip under the
442
+ * section head, not one per row). Nothing navigates — there is
443
+ * nothing to navigate to.
444
+ *
445
+ * Host-half pure logic so `test/client-switch.test.mjs` pins it from `dist/`.
446
+ */
447
+ export declare function switchAffordance(fusion: boolean): SwitchAffordance;
448
+ /**
449
+ * Structural slice of one roster row the Hub UI helpers read (the client
450
+ * roster's `AgentLink` shape mirrored structurally — this module never imports
451
+ * from the client half).
452
+ */
453
+ export interface HubRosterRow {
454
+ /** The roster key (`'native'` or a runtime key). */
455
+ readonly key: string;
456
+ /** The display label the roster projected for the key. */
457
+ readonly label: string;
458
+ /** The row's mount path. */
459
+ readonly path: string;
460
+ /** Machine the runtime comes from (the roster parser guarantees it — `'local'` when an older host shape omitted it). */
461
+ readonly machine: string;
462
+ }
463
+ /**
464
+ * The active agent's display name for the Hub row's right-edge selector: the
465
+ * roster label of the atom's `activeAgent` key — the name the selector's
466
+ * `title` carries, since the row label itself is the constant `Hub`. A key the
467
+ * (seeded) roster does not know degrades to the raw key. `undefined`/empty
468
+ * (flag off, or no atom read) means "no active agent".
469
+ *
470
+ * Host-half pure logic so `test/client-switch.test.mjs` pins it from `dist/`.
471
+ */
472
+ export declare function activeAgentName(activeAgent: string | undefined, roster: readonly HubRosterRow[]): string | undefined;
473
+ /** Which machine one roster row groups under: the row's own machine (parser-guaranteed). */
474
+ export declare function machineGroup(row: HubRosterRow): string;
475
+ /** One machine's group of roster rows, in roster order. */
476
+ export interface MachineGroup {
477
+ /** The grouping key (the members' shared `machine`). */
478
+ readonly machine: string;
479
+ /** The rows of this machine, keeping the roster's own order. */
480
+ readonly agents: readonly HubRosterRow[];
481
+ }
482
+ /**
483
+ * Group a roster by machine for the HubPanelIcon chevron menu's native
484
+ * workspace-list look: one group per distinct `machineGroup` key, groups in
485
+ * first-appearance order, members keeping the roster's order inside their
486
+ * group. A single-group roster (today's local-only shape) renders exactly
487
+ * like the old flat list minus the card border.
488
+ *
489
+ * Host-half pure logic so `test/client-switch.test.mjs` pins it from `dist/`.
490
+ */
491
+ export declare function machineGroups(rows: readonly HubRosterRow[]): readonly MachineGroup[];
492
+ /**
493
+ * What the machine hands the host's gate composition (T-G): the machine-side
494
+ * ingredients of {@link LandingGateOptions}. Structurally a full
495
+ * `LandingGateOptions` — the binding passes it straight through after adding
496
+ * its own membership face on the sessions store.
497
+ */
498
+ export interface LandingGateRequest {
499
+ /**
500
+ * The persisted pointer's plain session id, read at DECISION time through
501
+ * this host's {@link readPersistedSelection} + {@link parseLandingTarget}
502
+ * — the same storage face and precedence the landing callback itself uses
503
+ * at fire time. `undefined` for no pointer or a subagent-address pointer.
504
+ */
505
+ pointerSessionId(): string | undefined;
506
+ /** The quiet window ({@link SwitchOptions.quietWindowMs} ?? {@link LANDING_QUIET_MS}). */
507
+ quietMs: number;
508
+ /** This host's timer faces, so the quiet window is drivable in tests. */
509
+ setTimeout(fn: () => void, ms: number): unknown;
510
+ clearTimeout(handle: unknown): void;
511
+ }
512
+ /**
513
+ * The effects the switch machine needs, all injected. Every method used by
514
+ * the guarded steps ({@link setAgent}, the landing reads/writes,
515
+ * {@link reconnect}, {@link toast}) may throw; the machine contains the
516
+ * damage — a broken effect degrades its own step, never the orchestration
517
+ * and never the page. Timer and generation-subscription arming are the
518
+ * wiring's own faces (host `setTimeout`, the connection's generation
519
+ * observable) and are used directly.
520
+ */
521
+ export interface SwitchHost {
522
+ /** Swap the state atom (+ NS) to `label` — orchestration step 1. */
523
+ setAgent(label: string): void;
524
+ /**
525
+ * Read the active NS's persisted `dsh.sessions.current` raw value. The
526
+ * machine reads it at LANDING time — inside the both-lists gate callback,
527
+ * long after the NS swap, exactly where upstream `restoreSelection` reads
528
+ * its hydrated selection store relative to its ready gate. A read failure
529
+ * may be reported either by throwing or by returning `null`.
530
+ */
531
+ readPersistedSelection(): string | null;
532
+ /** Navigate the main view to a parsed landing target (open-session plan). */
533
+ openSession(target: string | object): void;
534
+ /**
535
+ * Run the new-session flow (the public `uiWorkspace.startSession` face):
536
+ * with a workspace id the flow targets that workspace (blank reuse
537
+ * included); without, the inherited current-then-recent resolution —
538
+ * which on a fresh, plane-consistent cache set IS the recent-workspace
539
+ * ladder. Executed for the start-session plan.
540
+ */
541
+ startSession(workspaceId?: string): void;
542
+ /**
543
+ * Mirror upstream `initializeDefaultWorkspace` (navigation.ts:349-361)
544
+ * through the public faces: prepare the default workspace and open it.
545
+ * Executed for the default-workspace plan. Asynchronous upstream; a
546
+ * failure is feedback, never a revert.
547
+ */
548
+ openDefaultWorkspace(): void;
549
+ /**
550
+ * Read the target plane's fresh navigation caches — the `workspaces.list`
551
+ * and `sessions.list` snapshots the landing plan decides over. The both-
552
+ * lists gate has already fired, so both phases are ready; a read failure
553
+ * may throw (a failed read lands nothing).
554
+ */
555
+ readRestoreCaches(): RestoreCaches;
556
+ /**
557
+ * Observe the target plane's wire-derived navigation caches being replaced
558
+ * after the switch began — BOTH stores' post-`connection/reset` repull
559
+ * baselines landed (workspaces and sessions, either order), AND — T-G —
560
+ * when the landing pointer exists, the sessions list contains its session
561
+ * or has gone quiet for {@link LandingGateRequest.quietMs}. The all-clear
562
+ * for the landing: only then do the snapshots hold the target agent's data
563
+ * and upstream's resolution faces read the target plane. The listener must
564
+ * fire at most once per subscription; the returned disposer stops it (and
565
+ * any running quiet window).
566
+ *
567
+ * The `gate` request carries what the MACHINE owns — the pointer face
568
+ * (over this host's {@link readPersistedSelection}), the quiet window from
569
+ * {@link SwitchOptions.quietWindowMs}, and this host's own timers — so the
570
+ * binding can compose the membership-aware {@link observeCachesRefreshed}
571
+ * by adding only its membership face (`sessions.list` `byId`).
572
+ */
573
+ onCachesRefreshed(listener: () => void, gate: LandingGateRequest): () => void;
574
+ /** Replace the connection attempt immediately (step 3, and the revert). */
575
+ reconnect(): void;
576
+ /**
577
+ * Observe generation establishment (post-`connection/reset`). The listener
578
+ * fires whenever a generation exists again; the returned disposer stops it.
579
+ */
580
+ onGenerationEstablished(listener: () => void): () => void;
581
+ /**
582
+ * The roster probe: resolve `true` while `label` is still a live roster
583
+ * entry on the control plane. Reject or resolve `false` = probe failure.
584
+ */
585
+ probeTarget(label: string): Promise<boolean>;
586
+ /** One visible toast/message; the revert's user feedback. */
587
+ toast(message: string): void;
588
+ /** Timers, injected so tests drive time by hand. */
589
+ setTimeout(fn: () => void, ms: number): unknown;
590
+ clearTimeout(handle: unknown): void;
591
+ }
592
+ /** Options; every field optional. */
593
+ export interface SwitchOptions {
594
+ /** Fallback window in ms; defaults to {@link SWITCH_FALLBACK_MS}. */
595
+ fallbackMs?: number;
596
+ /**
597
+ * T-G: the landing gate's membership-wait quiet window in ms; defaults to
598
+ * {@link LANDING_QUIET_MS}. Ridden through {@link LandingGateRequest} to
599
+ * the gate the binding composes.
600
+ */
601
+ quietWindowMs?: number;
602
+ }
603
+ /** The produced orchestration interface (brief: `switchAgent(label): Promise<void>`). */
604
+ export interface SwitchController {
605
+ /**
606
+ * Run the full switch lifecycle for `label`: state swap, landing,
607
+ * reconnect, then settle on generation establishment or revert on the
608
+ * fallback. Resolves when the attempt ends — a revert is a handled
609
+ * outcome, not a rejection. A new run supersedes any prior attempt —
610
+ * pending or already settled — silently (cancelled, no revert) and kills
611
+ * its still-armed deferred landing through the controller-scope handle,
612
+ * so a settled switch can never land over a newer one.
613
+ */
614
+ run(label: string): Promise<void>;
615
+ }
616
+ /**
617
+ * Build the switch machine over an injected {@link SwitchHost}.
618
+ *
619
+ * Only one attempt is ever armed at a time: starting a new switch finishes
620
+ * the previous one silently (timer cleared, subscription released, promise
621
+ * resolved) and cancels its pending landing, so a stale attempt can never
622
+ * revert or land over a newer switch. The landing cancel is held at
623
+ * CONTROLLER scope, not reached through the current-attempt pointer: the
624
+ * deferred landing deliberately survives the settle — which removes the
625
+ * attempt from `current` — so a supersede must still kill the armed landing
626
+ * of a PRIOR, ALREADY-SETTLED attempt (final-review round 2: without the
627
+ * controller-scope handle, a settled switch's landing fired over the newer
628
+ * switch's pointer-restored view).
629
+ */
630
+ export declare function createSwitchController(host: SwitchHost, options?: SwitchOptions): SwitchController;