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.
- package/client/index.js +1010 -345
- package/cordis.patch.yml +22 -5
- package/package.json +8 -3
- package/superd-hub/dist/a2a-delivery.d.ts +189 -0
- package/superd-hub/dist/a2a-delivery.js +240 -0
- package/superd-hub/dist/a2a-host.d.ts +76 -0
- package/superd-hub/dist/a2a-host.js +145 -0
- package/superd-hub/dist/a2a-ingest.d.ts +31 -0
- package/superd-hub/dist/a2a-ingest.js +636 -0
- package/superd-hub/dist/a2a-log.d.ts +17 -0
- package/superd-hub/dist/a2a-log.js +42 -0
- package/superd-hub/dist/a2a-row.d.ts +65 -0
- package/superd-hub/dist/a2a-row.js +162 -0
- package/superd-hub/dist/a2a-service.d.ts +357 -0
- package/superd-hub/dist/a2a-service.js +980 -0
- package/superd-hub/dist/a2a-wire.d.ts +220 -0
- package/superd-hub/dist/a2a-wire.js +305 -0
- package/superd-hub/dist/agent-ids.d.ts +10 -0
- package/superd-hub/dist/agent-ids.js +68 -0
- package/superd-hub/dist/agent-roster.d.ts +32 -17
- package/superd-hub/dist/agent-roster.js +54 -15
- package/superd-hub/dist/agent-status.d.ts +119 -0
- package/superd-hub/dist/agent-status.js +386 -0
- package/superd-hub/dist/client/mesh-api.d.ts +134 -0
- package/superd-hub/dist/client/mesh-api.js +219 -0
- package/superd-hub/dist/client/mesh-groups.d.ts +63 -0
- package/superd-hub/dist/client/mesh-groups.js +66 -0
- package/superd-hub/dist/client/roster.d.ts +48 -0
- package/superd-hub/dist/client/roster.js +96 -0
- package/superd-hub/dist/exec-pty-channel.d.ts +58 -0
- package/superd-hub/dist/exec-pty-channel.js +162 -0
- package/superd-hub/dist/exec-pty-framing.d.ts +32 -0
- package/superd-hub/dist/exec-pty-framing.js +81 -0
- package/superd-hub/dist/exec-pty-rpc.d.ts +21 -0
- package/superd-hub/dist/exec-pty-rpc.js +128 -0
- package/superd-hub/dist/exec-pty-session.d.ts +76 -0
- package/superd-hub/dist/exec-pty-session.js +234 -0
- package/superd-hub/dist/exec.d.ts +56 -0
- package/superd-hub/dist/exec.js +225 -0
- package/superd-hub/dist/fusion-transport.d.ts +23 -21
- package/superd-hub/dist/fusion-transport.js +12 -40
- package/superd-hub/dist/gateway.d.ts +39 -2
- package/superd-hub/dist/gateway.js +62 -3
- package/superd-hub/dist/hub-tool.d.ts +174 -0
- package/superd-hub/dist/hub-tool.js +464 -0
- package/superd-hub/dist/index.d.ts +18 -4
- package/superd-hub/dist/index.js +19 -3
- package/superd-hub/dist/machines.d.ts +45 -0
- package/superd-hub/dist/machines.js +125 -0
- package/superd-hub/dist/mesh-federate.d.ts +138 -0
- package/superd-hub/dist/mesh-federate.js +223 -0
- package/superd-hub/dist/mesh-gateway.d.ts +67 -0
- package/superd-hub/dist/mesh-gateway.js +408 -0
- package/superd-hub/dist/mesh-link.d.ts +274 -0
- package/superd-hub/dist/mesh-link.js +441 -0
- package/superd-hub/dist/mesh-rpc.d.ts +123 -0
- package/superd-hub/dist/mesh-rpc.js +250 -0
- package/superd-hub/dist/mux-client.d.ts +71 -0
- package/superd-hub/dist/mux-client.js +281 -0
- package/superd-hub/dist/pack-version.d.ts +2 -0
- package/superd-hub/dist/pack-version.js +30 -0
- package/superd-hub/dist/roster.d.ts +10 -0
- package/superd-hub/dist/roster.js +9 -1
- package/superd-hub/dist/rpc.d.ts +18 -2
- package/superd-hub/dist/rpc.js +34 -4
- package/superd-hub/dist/spawn-world.js +3 -1
- package/superd-hub/dist/switch-core.d.ts +98 -21
- package/superd-hub/dist/switch-core.js +123 -28
- package/superd-hub/dist/system-head.d.ts +22 -12
- package/superd-hub/dist/system-head.js +100 -84
- package/superd-hub/dist/toast-core.d.ts +43 -0
- package/superd-hub/dist/toast-core.js +63 -0
- package/superd-hub/dist/wire-endpoints.d.ts +264 -0
- package/superd-hub/dist/wire-endpoints.js +180 -0
- package/superd-hub/dist/world-join.js +8 -3
- package/superd-hub/dist/world-mount.d.ts +27 -0
- package/superd-hub/dist/world-mount.js +67 -0
- package/superd-hub/lib/client/index.js +1010 -345
- package/superd-hub/package.json +11 -2
package/superd-hub/dist/rpc.d.ts
CHANGED
|
@@ -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;
|
package/superd-hub/dist/rpc.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
461
|
-
|
|
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
|
|
14
|
-
*
|
|
15
|
-
* and
|
|
16
|
-
*
|
|
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:
|
|
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
|
|
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>;
|