super-dsh 0.1.5 → 0.1.9

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 (79) hide show
  1. package/client/index.js +1010 -345
  2. package/cordis.patch.yml +22 -5
  3. package/package.json +8 -3
  4. package/superd-hub/dist/a2a-delivery.d.ts +189 -0
  5. package/superd-hub/dist/a2a-delivery.js +240 -0
  6. package/superd-hub/dist/a2a-host.d.ts +76 -0
  7. package/superd-hub/dist/a2a-host.js +145 -0
  8. package/superd-hub/dist/a2a-ingest.d.ts +31 -0
  9. package/superd-hub/dist/a2a-ingest.js +636 -0
  10. package/superd-hub/dist/a2a-log.d.ts +17 -0
  11. package/superd-hub/dist/a2a-log.js +42 -0
  12. package/superd-hub/dist/a2a-row.d.ts +65 -0
  13. package/superd-hub/dist/a2a-row.js +162 -0
  14. package/superd-hub/dist/a2a-service.d.ts +357 -0
  15. package/superd-hub/dist/a2a-service.js +980 -0
  16. package/superd-hub/dist/a2a-wire.d.ts +220 -0
  17. package/superd-hub/dist/a2a-wire.js +305 -0
  18. package/superd-hub/dist/agent-ids.d.ts +10 -0
  19. package/superd-hub/dist/agent-ids.js +68 -0
  20. package/superd-hub/dist/agent-roster.d.ts +32 -17
  21. package/superd-hub/dist/agent-roster.js +54 -15
  22. package/superd-hub/dist/agent-status.d.ts +119 -0
  23. package/superd-hub/dist/agent-status.js +386 -0
  24. package/superd-hub/dist/client/mesh-api.d.ts +134 -0
  25. package/superd-hub/dist/client/mesh-api.js +219 -0
  26. package/superd-hub/dist/client/mesh-groups.d.ts +63 -0
  27. package/superd-hub/dist/client/mesh-groups.js +66 -0
  28. package/superd-hub/dist/client/roster.d.ts +48 -0
  29. package/superd-hub/dist/client/roster.js +96 -0
  30. package/superd-hub/dist/exec-pty-channel.d.ts +58 -0
  31. package/superd-hub/dist/exec-pty-channel.js +162 -0
  32. package/superd-hub/dist/exec-pty-framing.d.ts +32 -0
  33. package/superd-hub/dist/exec-pty-framing.js +81 -0
  34. package/superd-hub/dist/exec-pty-rpc.d.ts +21 -0
  35. package/superd-hub/dist/exec-pty-rpc.js +128 -0
  36. package/superd-hub/dist/exec-pty-session.d.ts +76 -0
  37. package/superd-hub/dist/exec-pty-session.js +234 -0
  38. package/superd-hub/dist/exec.d.ts +56 -0
  39. package/superd-hub/dist/exec.js +225 -0
  40. package/superd-hub/dist/fusion-transport.d.ts +23 -21
  41. package/superd-hub/dist/fusion-transport.js +12 -40
  42. package/superd-hub/dist/gateway.d.ts +39 -2
  43. package/superd-hub/dist/gateway.js +62 -3
  44. package/superd-hub/dist/hub-tool.d.ts +174 -0
  45. package/superd-hub/dist/hub-tool.js +464 -0
  46. package/superd-hub/dist/index.d.ts +18 -4
  47. package/superd-hub/dist/index.js +19 -3
  48. package/superd-hub/dist/machines.d.ts +45 -0
  49. package/superd-hub/dist/machines.js +125 -0
  50. package/superd-hub/dist/mesh-federate.d.ts +138 -0
  51. package/superd-hub/dist/mesh-federate.js +223 -0
  52. package/superd-hub/dist/mesh-gateway.d.ts +67 -0
  53. package/superd-hub/dist/mesh-gateway.js +408 -0
  54. package/superd-hub/dist/mesh-link.d.ts +274 -0
  55. package/superd-hub/dist/mesh-link.js +441 -0
  56. package/superd-hub/dist/mesh-rpc.d.ts +123 -0
  57. package/superd-hub/dist/mesh-rpc.js +250 -0
  58. package/superd-hub/dist/mux-client.d.ts +71 -0
  59. package/superd-hub/dist/mux-client.js +281 -0
  60. package/superd-hub/dist/pack-version.d.ts +2 -0
  61. package/superd-hub/dist/pack-version.js +30 -0
  62. package/superd-hub/dist/roster.d.ts +10 -0
  63. package/superd-hub/dist/roster.js +9 -1
  64. package/superd-hub/dist/rpc.d.ts +18 -2
  65. package/superd-hub/dist/rpc.js +34 -4
  66. package/superd-hub/dist/spawn-world.js +3 -1
  67. package/superd-hub/dist/switch-core.d.ts +98 -21
  68. package/superd-hub/dist/switch-core.js +123 -28
  69. package/superd-hub/dist/system-head.d.ts +22 -12
  70. package/superd-hub/dist/system-head.js +100 -84
  71. package/superd-hub/dist/toast-core.d.ts +43 -0
  72. package/superd-hub/dist/toast-core.js +63 -0
  73. package/superd-hub/dist/wire-endpoints.d.ts +264 -0
  74. package/superd-hub/dist/wire-endpoints.js +180 -0
  75. package/superd-hub/dist/world-join.js +8 -3
  76. package/superd-hub/dist/world-mount.d.ts +27 -0
  77. package/superd-hub/dist/world-mount.js +67 -0
  78. package/superd-hub/lib/client/index.js +1010 -345
  79. package/superd-hub/package.json +11 -2
@@ -23,16 +23,32 @@ export interface RuntimeTargetSource {
23
23
  }
24
24
  /** The live registry view: native plus every registered foreign context. */
25
25
  export declare const registryTargets: RuntimeTargetSource;
26
+ /** Injection seam for the roster/hub identity: the hub home backing the a6
27
+ * id store and the machines registry. Defaults to `resolveDshHome()`
28
+ * (registerSelectorRpc passes no deps — the same home the rest of this
29
+ * process uses); unit tests inject a tmpdir so nothing ever touches an
30
+ * ambient/prod DSH_HOME. */
31
+ export interface AgentRuntimeDeps {
32
+ home?: string;
33
+ }
26
34
  /**
27
35
  * The control-plane handler: world inventory only.
36
+ *
37
+ * ADDITIVE contract (mesh P1): the GET body gained `hub: {machineId, label,
38
+ * version}` and every `agents` row gained `id` (a6); `available` and all
39
+ * pre-existing agents fields are unchanged — old clients ignore the new
40
+ * fields, new clients read them.
28
41
  * @param request - the incoming `/api/agent-runtime` request.
29
42
  * @param source - live runtime registry view.
43
+ * @param deps - optional home override; defaults to `resolveDshHome()`.
30
44
  * @returns the JSON response.
31
45
  */
32
- export declare function handleAgentRuntime(request: Request, source: RuntimeTargetSource): Promise<Response>;
46
+ export declare function handleAgentRuntime(request: Request, source: RuntimeTargetSource, deps?: AgentRuntimeDeps): Promise<Response>;
33
47
  /**
34
48
  * Register the control-plane RPC face on the shared `/api` connection channel
35
49
  * (behind the standard browser-trust + auth fence). No-op when the
36
- * `connection` service is absent (e.g. fake-ctx unit tests).
50
+ * `connection` service is absent (e.g. fake-ctx unit tests). The route answers
51
+ * with the DEFAULT home (`resolveDshHome()` at request time); callers needing
52
+ * a pinned home invoke handleAgentRuntime directly with deps.
37
53
  */
38
54
  export declare function registerSelectorRpc(ctx: Context, source: RuntimeTargetSource): void;
@@ -1,11 +1,20 @@
1
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
2
+ import { PACK_VERSION } from './pack-version.js';
1
3
  import { listRuntimeKeys } from './targets.js';
2
4
  import { agentLinks } from './agent-roster.js';
5
+ import { ensureSelf } from './machines.js';
3
6
  /** The shared-channel Fetch route path this RPC face owns. */
4
7
  export const AGENT_RUNTIME_RPC_PATH = '/api/agent-runtime';
5
8
  /** The live registry view: native plus every registered foreign context. */
6
9
  export const registryTargets = {
7
10
  listRuntimes: listRuntimeKeys,
8
11
  };
12
+ /** The version this hub SERVES as `hub.version` — the super-dsh PACK version
13
+ * (D6): the subpackage's own 0.1.0 never moves, so peers comparing against it
14
+ * could never see a mismatch. The pack manifest ships exactly two hops above
15
+ * the compiled output in every layout (see pack-version.ts); 'unknown' when
16
+ * unreadable — an honest non-number, not a fake one. */
17
+ const HUB_VERSION = PACK_VERSION;
9
18
  function json(status, body) {
10
19
  return new Response(JSON.stringify(body), {
11
20
  status,
@@ -14,16 +23,35 @@ function json(status, body) {
14
23
  }
15
24
  /**
16
25
  * The control-plane handler: world inventory only.
26
+ *
27
+ * ADDITIVE contract (mesh P1): the GET body gained `hub: {machineId, label,
28
+ * version}` and every `agents` row gained `id` (a6); `available` and all
29
+ * pre-existing agents fields are unchanged — old clients ignore the new
30
+ * fields, new clients read them.
17
31
  * @param request - the incoming `/api/agent-runtime` request.
18
32
  * @param source - live runtime registry view.
33
+ * @param deps - optional home override; defaults to `resolveDshHome()`.
19
34
  * @returns the JSON response.
20
35
  */
21
- export async function handleAgentRuntime(request, source) {
36
+ export async function handleAgentRuntime(request, source, deps) {
22
37
  const available = source.listRuntimes();
23
38
  if (request.method === 'GET' || request.method === 'HEAD') {
24
39
  // The selector asks this face for the roster: keys AND the mount path each
25
- // key is addressed by. Read-only — the roster carries no selection.
26
- return json(200, { available, agents: agentLinks() });
40
+ // key is addressed by. Read-only — the roster carries no selection. The
41
+ // hub block (this machine's m4 + label + hub version) rides along for the
42
+ // multi-machine UI; the a6 row ids ride on every agents row.
43
+ const home = deps?.home ?? resolveDshHome();
44
+ const self = ensureSelf(home, 'local');
45
+ // Self-description discipline (2026-09-28 user ruling): a peer's discovery
46
+ // probe (`?mesh=1`) receives THIS machine's own rows only — a hub never
47
+ // re-broadcasts other remotes' agents to its peers (hub-to-hub is P2P;
48
+ // the requester fans out one probe per paired machine and merges itself).
49
+ // The plain browser GET keeps the full roster — the local UI's machine
50
+ // grouping consumes exactly that.
51
+ const links = new URL(request.url).searchParams.get('mesh') === '1'
52
+ ? agentLinks(home).filter((l) => l.machine === 'local')
53
+ : agentLinks(home);
54
+ return json(200, { available, agents: links, hub: { machineId: self.id, label: self.label, version: HUB_VERSION } });
27
55
  }
28
56
  // A POST that used to switch a global selector has no meaning any more:
29
57
  // the addressed world is the mount path of the request, not a shared value.
@@ -35,7 +63,9 @@ export async function handleAgentRuntime(request, source) {
35
63
  /**
36
64
  * Register the control-plane RPC face on the shared `/api` connection channel
37
65
  * (behind the standard browser-trust + auth fence). No-op when the
38
- * `connection` service is absent (e.g. fake-ctx unit tests).
66
+ * `connection` service is absent (e.g. fake-ctx unit tests). The route answers
67
+ * with the DEFAULT home (`resolveDshHome()` at request time); callers needing
68
+ * a pinned home invoke handleAgentRuntime directly with deps.
39
69
  */
40
70
  export function registerSelectorRpc(ctx, source) {
41
71
  ctx.inject(['connection'], (connCtx) => {
@@ -71,7 +71,9 @@ async function publishWorldInHostResolutionTurn(packages, launcher, opts, worldP
71
71
  const declarer = join(worldProfileDir, 'package.json');
72
72
  const worldScope = join(worldProfileDir, 'node_modules', '@pgmi-builds');
73
73
  const baseNames = new Set(base.entries.map((entry) => entry.name));
74
- for (const name of readdirSync(worldScope, { withFileTypes: true })) {
74
+ // R0 scan guard: a dsh-base world has NO farm — a missing scope is not a
75
+ // publish failure, just nothing to add (existsSync before readdirSync).
76
+ for (const name of existsSync(worldScope) ? readdirSync(worldScope, { withFileTypes: true }) : []) {
75
77
  if (!name.isDirectory() && !name.isSymbolicLink())
76
78
  continue;
77
79
  const pkgName = `@pgmi-builds/${name.name}`;
@@ -457,8 +457,13 @@ export interface HubRosterRow {
457
457
  readonly label: string;
458
458
  /** The row's mount path. */
459
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;
460
+ /**
461
+ * Machine the runtime comes from (mesh P2): `'local'` for this machine's
462
+ * rows, the owning machine's m4 for federated rows. Optional — the wire
463
+ * may omit it and the grouping now lives client-side (mesh-groups.ts,
464
+ * fed the machines API's label map).
465
+ */
466
+ readonly machine?: string;
462
467
  }
463
468
  /**
464
469
  * The active agent's display name for the Hub row's right-edge selector: the
@@ -470,25 +475,6 @@ export interface HubRosterRow {
470
475
  * Host-half pure logic so `test/client-switch.test.mjs` pins it from `dist/`.
471
476
  */
472
477
  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
478
  /**
493
479
  * What the machine hands the host's gate composition (T-G): the machine-side
494
480
  * ingredients of {@link LandingGateOptions}. Structurally a full
@@ -628,3 +614,94 @@ export interface SwitchController {
628
614
  * switch's pointer-restored view).
629
615
  */
630
616
  export declare function createSwitchController(host: SwitchHost, options?: SwitchOptions): SwitchController;
617
+ /** What attemptSwitch does with a roster row: run the machine / report failure / wait for readiness / fail fast on a degraded federated row. */
618
+ export type PreFlightDecision = 'run' | 'failed' | 'wait' | 'machine-down';
619
+ /**
620
+ * The roster fields the pre-flight reads (the projection carries both since
621
+ * C1 task 3; `machine` since the mesh P2 roster dimension).
622
+ */
623
+ export interface PreFlightRow {
624
+ ready?: boolean;
625
+ failed?: boolean;
626
+ /**
627
+ * Owning machine (m4) for FEDERATED rows — `'local'` or absent means this
628
+ * machine's own world (the C1 spawn-wait semantics). The client's
629
+ * `AgentLink` satisfies this structurally; the field decides the I1(c)
630
+ * fail-fast branch.
631
+ */
632
+ machine?: string;
633
+ }
634
+ /**
635
+ * The pure pre-flight decision. `native` always runs; an absent row (roster
636
+ * fetch failed, or the key is not listed) also runs — the machine's roster
637
+ * probe is the authority on unknown targets, exactly as before C1. A
638
+ * not-ready FEDERATED row (`machine` set and not `'local'`) decides
639
+ * `'machine-down'` — an immediate actionable failure, never a wait loop
640
+ * (the hub's probe already knows the machine is unreachable or its auth
641
+ * expired); `failed` keeps precedence everywhere (a peer-reported spawn
642
+ * rejection still reports as failed).
643
+ */
644
+ export declare function decidePreFlight(label: string, row: PreFlightRow | undefined): PreFlightDecision;
645
+ /** Bound on the machine-down branch's enrichment fetch (mesh P3 Task 5a:
646
+ * the parked P2 minor — an unbounded `fetchMachines` await — folds here; a
647
+ * hung hub answer must never swallow the toast). */
648
+ export declare const MACHINE_DOWN_ENRICH_MS = 3000;
649
+ /** The machines-face slice the machine-down feedback reads (the client's
650
+ * `fetchMachines()` satisfies this structurally). */
651
+ export interface MachineDownMachinesFace {
652
+ machines: readonly {
653
+ id: string;
654
+ label: string;
655
+ status: string;
656
+ }[];
657
+ }
658
+ /** The branch's injected faces — the browser wiring supplies the real
659
+ * fetch/reprobe/toast; tests fake all of them. */
660
+ export interface MachineDownFaces {
661
+ /** Same-origin GET of the hub machine list (enrichment only). */
662
+ fetchMachines(): Promise<MachineDownMachinesFace>;
663
+ /** The I1(b) light probe: POST `{op: 'reprobe', id}` — fire-and-forget. */
664
+ reprobe(id: string): Promise<void>;
665
+ toast(message: string): void;
666
+ setTimeout(fn: () => void, ms: number): unknown;
667
+ clearTimeout(handle: unknown): void;
668
+ }
669
+ /**
670
+ * The machine-down branch's pure half (I1(c) feedback + I1(b) light probe,
671
+ * mesh P3 Task 5a): enrich the toast through the machines face — BOUNDED at
672
+ * {@link MACHINE_DOWN_ENRICH_MS} — then, after toasting, fire ONE reprobe
673
+ * for the row's machine and forget it: a stale-unreachable machine gets a
674
+ * fresh attempt per failed click, so the NEXT click can succeed without
675
+ * waiting for the hub's 5-minute sweep. Never blocks the UI path (the whole
676
+ * body is detached), never surfaces a reprobe failure (the sweep retries).
677
+ */
678
+ export declare function runMachineDownFeedback(label: string, row: PreFlightRow & {
679
+ label?: string;
680
+ }, faces: MachineDownFaces): void;
681
+ /** Default bound on one readiness wait (the page never locks behind a stuck world). */
682
+ export declare const WAIT_BOUND_MS = 120000;
683
+ /** Default poll interval for one readiness wait. */
684
+ export declare const WAIT_POLL_MS = 1000;
685
+ /** The waiter's injected faces (tests drive time and the roster by hand). */
686
+ export interface ReadinessWaiterFaces {
687
+ /** Read the target's roster row; undefined on any failure. */
688
+ fetchRow(): Promise<PreFlightRow | undefined>;
689
+ setTimeout(fn: () => void, ms: number): unknown;
690
+ clearTimeout(handle: unknown): void;
691
+ }
692
+ /** One bounded readiness wait. */
693
+ export interface ReadinessWaiter {
694
+ /** Resolves when the row flips ready ('ready'), reports failure ('failed'), the bound expires ('bound'), or the waiter is cancelled ('cancelled'). */
695
+ wait(label: string): Promise<'ready' | 'failed' | 'bound' | 'cancelled'>;
696
+ /** A newer switch attempt cancels a pending wait (it resolves 'cancelled'). */
697
+ cancel(): void;
698
+ }
699
+ /**
700
+ * Build a bounded readiness waiter over injected faces. The poll re-reads
701
+ * the roster row every {@link ReadinessWaiterOptions.pollMs}; `failed`
702
+ * short-circuits immediately; the bound always terminates the wait.
703
+ */
704
+ export declare function createReadinessWaiter(faces: ReadinessWaiterFaces, options?: {
705
+ boundMs?: number;
706
+ pollMs?: number;
707
+ }): ReadinessWaiter;
@@ -502,34 +502,6 @@ export function activeAgentName(activeAgent, roster) {
502
502
  return undefined;
503
503
  return roster.find((row) => row.key === activeAgent)?.label ?? activeAgent;
504
504
  }
505
- /** Which machine one roster row groups under: the row's own machine (parser-guaranteed). */
506
- export function machineGroup(row) {
507
- return row.machine;
508
- }
509
- /**
510
- * Group a roster by machine for the HubPanelIcon chevron menu's native
511
- * workspace-list look: one group per distinct `machineGroup` key, groups in
512
- * first-appearance order, members keeping the roster's order inside their
513
- * group. A single-group roster (today's local-only shape) renders exactly
514
- * like the old flat list minus the card border.
515
- *
516
- * Host-half pure logic so `test/client-switch.test.mjs` pins it from `dist/`.
517
- */
518
- export function machineGroups(rows) {
519
- const groups = [];
520
- const byMachine = new Map();
521
- for (const row of rows) {
522
- const machine = machineGroup(row);
523
- let group = byMachine.get(machine);
524
- if (group === undefined) {
525
- group = { machine, agents: [] };
526
- byMachine.set(machine, group);
527
- groups.push(group);
528
- }
529
- group.agents.push(row);
530
- }
531
- return groups;
532
- }
533
505
  /**
534
506
  * Build the switch machine over an injected {@link SwitchHost}.
535
507
  *
@@ -732,3 +704,126 @@ export function createSwitchController(host, options = {}) {
732
704
  },
733
705
  };
734
706
  }
707
+ /**
708
+ * The pure pre-flight decision. `native` always runs; an absent row (roster
709
+ * fetch failed, or the key is not listed) also runs — the machine's roster
710
+ * probe is the authority on unknown targets, exactly as before C1. A
711
+ * not-ready FEDERATED row (`machine` set and not `'local'`) decides
712
+ * `'machine-down'` — an immediate actionable failure, never a wait loop
713
+ * (the hub's probe already knows the machine is unreachable or its auth
714
+ * expired); `failed` keeps precedence everywhere (a peer-reported spawn
715
+ * rejection still reports as failed).
716
+ */
717
+ export function decidePreFlight(label, row) {
718
+ if (label === NATIVE_AGENT)
719
+ return 'run';
720
+ if (row === undefined)
721
+ return 'run';
722
+ if (row.failed === true)
723
+ return 'failed';
724
+ if (row.ready !== true) {
725
+ // I1(c): a federated not-ready row is DEGRADED, not spawning — fail fast.
726
+ if (row.machine !== undefined && row.machine !== 'local')
727
+ return 'machine-down';
728
+ return 'wait';
729
+ }
730
+ return 'run';
731
+ }
732
+ // ---- I1(b): the machine-down feedback (preflight light probe) ----------------
733
+ /** Bound on the machine-down branch's enrichment fetch (mesh P3 Task 5a:
734
+ * the parked P2 minor — an unbounded `fetchMachines` await — folds here; a
735
+ * hung hub answer must never swallow the toast). */
736
+ export const MACHINE_DOWN_ENRICH_MS = 3_000;
737
+ /**
738
+ * The machine-down branch's pure half (I1(c) feedback + I1(b) light probe,
739
+ * mesh P3 Task 5a): enrich the toast through the machines face — BOUNDED at
740
+ * {@link MACHINE_DOWN_ENRICH_MS} — then, after toasting, fire ONE reprobe
741
+ * for the row's machine and forget it: a stale-unreachable machine gets a
742
+ * fresh attempt per failed click, so the NEXT click can succeed without
743
+ * waiting for the hub's 5-minute sweep. Never blocks the UI path (the whole
744
+ * body is detached), never surfaces a reprobe failure (the sweep retries).
745
+ */
746
+ export function runMachineDownFeedback(label, row, faces) {
747
+ void (async () => {
748
+ let machineName = row.machine;
749
+ let status;
750
+ const enriched = await new Promise((resolve) => {
751
+ const bound = faces.setTimeout(() => resolve(undefined), MACHINE_DOWN_ENRICH_MS);
752
+ faces.fetchMachines().then((list) => { faces.clearTimeout(bound); resolve(list); }, () => { faces.clearTimeout(bound); resolve(undefined); });
753
+ });
754
+ if (enriched !== undefined) {
755
+ const found = enriched.machines.find((entry) => entry.id === row.machine);
756
+ if (found !== undefined) {
757
+ machineName = found.label;
758
+ if (found.status === 'unreachable' || found.status === 'auth-expired')
759
+ status = found.status;
760
+ }
761
+ }
762
+ const who = row.label ?? label;
763
+ const machine = machineName ?? 'its machine';
764
+ faces.toast(status === undefined
765
+ ? `Super D: ${who} is unavailable — machine ${machine} is not ready. Reprobe or re-add it in the Hub machines panel.`
766
+ : `Super D: ${who} is unavailable — machine ${machine} reports ${status}. Reprobe or re-add it in the Hub machines panel.`);
767
+ // I1(b): exactly ONE fresh attempt per failed click — keyed by the row's
768
+ // machine id (the machine-down decision guarantees it is set; a malformed
769
+ // row simply skips the light probe).
770
+ if (row.machine !== undefined)
771
+ faces.reprobe(row.machine).catch(() => { });
772
+ })();
773
+ }
774
+ /** Default bound on one readiness wait (the page never locks behind a stuck world). */
775
+ export const WAIT_BOUND_MS = 120_000;
776
+ /** Default poll interval for one readiness wait. */
777
+ export const WAIT_POLL_MS = 1_000;
778
+ /**
779
+ * Build a bounded readiness waiter over injected faces. The poll re-reads
780
+ * the roster row every {@link ReadinessWaiterOptions.pollMs}; `failed`
781
+ * short-circuits immediately; the bound always terminates the wait.
782
+ */
783
+ export function createReadinessWaiter(faces, options = {}) {
784
+ const boundMs = options.boundMs ?? WAIT_BOUND_MS;
785
+ const pollMs = options.pollMs ?? WAIT_POLL_MS;
786
+ let pending;
787
+ let pollHandle;
788
+ let boundHandle;
789
+ let settled = false;
790
+ const settle = (outcome) => {
791
+ if (settled)
792
+ return;
793
+ settled = true;
794
+ if (pollHandle !== undefined)
795
+ faces.clearTimeout(pollHandle);
796
+ if (boundHandle !== undefined)
797
+ faces.clearTimeout(boundHandle);
798
+ pending?.(outcome);
799
+ pending = undefined;
800
+ };
801
+ return {
802
+ wait(label) {
803
+ return new Promise((resolve) => {
804
+ pending = resolve;
805
+ boundHandle = faces.setTimeout(() => { settle('bound'); }, boundMs);
806
+ const tick = () => {
807
+ if (settled)
808
+ return;
809
+ void faces.fetchRow().then((row) => {
810
+ if (settled)
811
+ return;
812
+ if (row?.failed === true) {
813
+ settle('failed');
814
+ return;
815
+ }
816
+ if (row?.ready === true) {
817
+ settle('ready');
818
+ return;
819
+ }
820
+ pollHandle = faces.setTimeout(tick, pollMs);
821
+ }, () => { if (!settled)
822
+ pollHandle = faces.setTimeout(tick, pollMs); });
823
+ };
824
+ tick();
825
+ });
826
+ },
827
+ cancel() { settle('cancelled'); },
828
+ };
829
+ }
@@ -10,16 +10,10 @@
10
10
  * (before any step) — so every bridge-written log violated one rule or the
11
11
  * other and became unresumable across restarts.
12
12
  *
13
- * The native loop never hits this because it opens the step before writing
14
- * anything: `turn/start → step/start → system/message → user/message …`,
15
- * and later prompt changes REPLACE the protected head (the operation
16
- * compaction also uses). This module gives bridge adapters that posture with
17
- * a one-line wrap:
18
- *
19
- * this.session = withSystemHead(session, () => createSystemMessage(""))
20
- *
21
- * The wrap is a transparent Proxy that tracks the open turn/step (exactly as
22
- * the v4 reader does) and intervenes minimally:
13
+ * The wrap installs the co-writer as OWN PROPERTIES on the session object
14
+ * itself (an `append` own-property shadow over the prototype method, plus
15
+ * `stampSystemPrompt`) and tracks the open turn/step (exactly as the v4
16
+ * reader does), intervening minimally:
23
17
  *
24
18
  * - the FIRST `user/message` append on a still-headless, still-empty
25
19
  * surface first opens the turn's next step (when a turn is open but no
@@ -35,6 +29,18 @@
35
29
  * queued and flushed inside the next step the adapter opens;
36
30
  * - everything else delegates unchanged.
37
31
  *
32
+ * IDENTITY IS THE CONTRACT (2026-09-28, D1): the wrap must return the session
33
+ * object ITSELF, never a wrapper. Upstream identity-checks live sessions by
34
+ * object equality — `session-title`'s `rename()` refuses unless
35
+ * `ctx.sessions.get(session.id) === session` (session-title/src/index.ts),
36
+ * and adapters register the RAW prepared session with `sessions.enter()` /
37
+ * `announce()` (e.g. agent-pi/src/index.ts). An earlier revision returned a
38
+ * transparent `Proxy`; every world-context rename then failed with
39
+ * `session "<id>" is not live in this store` because the Agent handle's
40
+ * `session` (the Proxy) was never the store-registered object. Returning the
41
+ * same object keeps `agent.session === ctx.sessions.get(id)` true, so the
42
+ * whole upstream session plane (rename included) sees one identity.
43
+ *
38
44
  * A surface that already starts with a user message and has no head (legacy
39
45
  * logs) stays headless — valid — and is never stamped: appending a head
40
46
  * there is exactly the corruption this module exists to prevent.
@@ -54,7 +60,7 @@ export interface HeadAwareSessionFace {
54
60
  }
55
61
  /** Builds the placeholder head message (adapters pass their createSystemMessage). */
56
62
  export type HeadPlaceholder = () => unknown;
57
- /** One co-written session: transparent delegation + head bookkeeping. */
63
+ /** One co-written session: the session itself, carrying head bookkeeping (object identity preserved — see the module note). */
58
64
  export type WithSystemHead<S> = S & {
59
65
  /** Append-or-replace the protected head with the runtime's system prompt. */
60
66
  stampSystemPrompt(turn: number, step: number, message: unknown): void;
@@ -64,6 +70,10 @@ export type WithSystemHead<S> = S & {
64
70
  * @param session - the adapter's real Session.
65
71
  * @param placeholder - builds the placeholder head message when the first
66
72
  * user message arrives on an empty surface.
67
- * @returns the session (transparently delegated) with `stampSystemPrompt`.
73
+ * @returns the SAME session object with the co-writer installed as own
74
+ * properties (`append` shadow + `stampSystemPrompt`). Identity is the
75
+ * contract (D1): upstream liveness checks compare
76
+ * `ctx.sessions.get(id) === agent.session`, so the returned object must be
77
+ * the one registered with `sessions.enter()` — never a wrapper.
68
78
  */
69
79
  export declare function withSystemHead<S extends HeadAwareSessionFace>(session: S, placeholder: HeadPlaceholder): WithSystemHead<S>;