@geohar/un-bien 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +726 -0
- package/dist/actions/handlers.d.ts +77 -0
- package/dist/actions/handlers.js +26 -0
- package/dist/actions/handlers.js.map +1 -0
- package/dist/actions/registry.d.ts +39 -0
- package/dist/actions/registry.js +62 -0
- package/dist/actions/registry.js.map +1 -0
- package/dist/bin/supervisord.d.ts +2 -0
- package/dist/bin/supervisord.js +86 -0
- package/dist/bin/supervisord.js.map +1 -0
- package/dist/config.d.ts +84 -0
- package/dist/config.js +115 -0
- package/dist/config.js.map +1 -0
- package/dist/daemon/client.d.ts +20 -0
- package/dist/daemon/client.js +131 -0
- package/dist/daemon/client.js.map +1 -0
- package/dist/daemon/control_protocol.d.ts +176 -0
- package/dist/daemon/control_protocol.js +63 -0
- package/dist/daemon/control_protocol.js.map +1 -0
- package/dist/daemon/cron_log.d.ts +45 -0
- package/dist/daemon/cron_log.js +71 -0
- package/dist/daemon/cron_log.js.map +1 -0
- package/dist/daemon/cron_registry.d.ts +80 -0
- package/dist/daemon/cron_registry.js +198 -0
- package/dist/daemon/cron_registry.js.map +1 -0
- package/dist/daemon/id.d.ts +24 -0
- package/dist/daemon/id.js +36 -0
- package/dist/daemon/id.js.map +1 -0
- package/dist/daemon/install.d.ts +170 -0
- package/dist/daemon/install.js +603 -0
- package/dist/daemon/install.js.map +1 -0
- package/dist/daemon/registry.d.ts +63 -0
- package/dist/daemon/registry.js +153 -0
- package/dist/daemon/registry.js.map +1 -0
- package/dist/daemon/rpc_child.d.ts +194 -0
- package/dist/daemon/rpc_child.js +419 -0
- package/dist/daemon/rpc_child.js.map +1 -0
- package/dist/daemon/supervisor.d.ts +93 -0
- package/dist/daemon/supervisor.js +666 -0
- package/dist/daemon/supervisor.js.map +1 -0
- package/dist/extension_ui_bridge.d.ts +26 -0
- package/dist/extension_ui_bridge.js +341 -0
- package/dist/extension_ui_bridge.js.map +1 -0
- package/dist/index.d.ts +197 -0
- package/dist/index.js +5750 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/mesh_result.d.ts +10 -0
- package/dist/mcp/mesh_result.js +42 -0
- package/dist/mcp/mesh_result.js.map +1 -0
- package/dist/mcp/mesh_server.d.ts +16 -0
- package/dist/mcp/mesh_server.js +373 -0
- package/dist/mcp/mesh_server.js.map +1 -0
- package/dist/mesh/canonical.d.ts +30 -0
- package/dist/mesh/canonical.js +63 -0
- package/dist/mesh/canonical.js.map +1 -0
- package/dist/mesh/client.d.ts +18 -0
- package/dist/mesh/client.js +127 -0
- package/dist/mesh/client.js.map +1 -0
- package/dist/mesh/encoding.d.ts +38 -0
- package/dist/mesh/encoding.js +217 -0
- package/dist/mesh/encoding.js.map +1 -0
- package/dist/mesh/self_revoke.d.ts +73 -0
- package/dist/mesh/self_revoke.js +407 -0
- package/dist/mesh/self_revoke.js.map +1 -0
- package/dist/mesh/siblings.d.ts +49 -0
- package/dist/mesh/siblings.js +159 -0
- package/dist/mesh/siblings.js.map +1 -0
- package/dist/mesh/types.d.ts +34 -0
- package/dist/mesh/types.js +11 -0
- package/dist/mesh/types.js.map +1 -0
- package/dist/mesh/verify.d.ts +17 -0
- package/dist/mesh/verify.js +86 -0
- package/dist/mesh/verify.js.map +1 -0
- package/dist/pairing/crypto.d.ts +8 -0
- package/dist/pairing/crypto.js +22 -0
- package/dist/pairing/crypto.js.map +1 -0
- package/dist/pairing/qr.d.ts +59 -0
- package/dist/pairing/qr.js +127 -0
- package/dist/pairing/qr.js.map +1 -0
- package/dist/pairing/storage.d.ts +120 -0
- package/dist/pairing/storage.js +614 -0
- package/dist/pairing/storage.js.map +1 -0
- package/dist/panel_bridge.d.ts +15 -0
- package/dist/panel_bridge.js +409 -0
- package/dist/panel_bridge.js.map +1 -0
- package/dist/paths.d.ts +39 -0
- package/dist/paths.js +51 -0
- package/dist/paths.js.map +1 -0
- package/dist/protocol/codec.d.ts +7 -0
- package/dist/protocol/codec.js +48 -0
- package/dist/protocol/codec.js.map +1 -0
- package/dist/protocol/types.d.ts +404 -0
- package/dist/protocol/types.js +2 -0
- package/dist/protocol/types.js.map +1 -0
- package/dist/rooms.d.ts +42 -0
- package/dist/rooms.js +78 -0
- package/dist/rooms.js.map +1 -0
- package/dist/session/bridge.d.ts +37 -0
- package/dist/session/bridge.js +134 -0
- package/dist/session/bridge.js.map +1 -0
- package/dist/session/broker.d.ts +189 -0
- package/dist/session/broker.js +527 -0
- package/dist/session/broker.js.map +1 -0
- package/dist/session/broker_remote.d.ts +74 -0
- package/dist/session/broker_remote.js +581 -0
- package/dist/session/broker_remote.js.map +1 -0
- package/dist/session/cwd_lock.d.ts +33 -0
- package/dist/session/cwd_lock.js +127 -0
- package/dist/session/cwd_lock.js.map +1 -0
- package/dist/session/debug_log.d.ts +3 -0
- package/dist/session/debug_log.js +32 -0
- package/dist/session/debug_log.js.map +1 -0
- package/dist/session/envelope.d.ts +33 -0
- package/dist/session/envelope.js +117 -0
- package/dist/session/envelope.js.map +1 -0
- package/dist/session/global_config.d.ts +33 -0
- package/dist/session/global_config.js +73 -0
- package/dist/session/global_config.js.map +1 -0
- package/dist/session/ipc.d.ts +27 -0
- package/dist/session/ipc.js +22 -0
- package/dist/session/ipc.js.map +1 -0
- package/dist/session/leader_election.d.ts +32 -0
- package/dist/session/leader_election.js +108 -0
- package/dist/session/leader_election.js.map +1 -0
- package/dist/session/local_config.d.ts +67 -0
- package/dist/session/local_config.js +193 -0
- package/dist/session/local_config.js.map +1 -0
- package/dist/session/mesh_node.d.ts +116 -0
- package/dist/session/mesh_node.js +596 -0
- package/dist/session/mesh_node.js.map +1 -0
- package/dist/session/peer.d.ts +139 -0
- package/dist/session/peer.js +474 -0
- package/dist/session/peer.js.map +1 -0
- package/dist/session/peer_inventory.d.ts +13 -0
- package/dist/session/peer_inventory.js +48 -0
- package/dist/session/peer_inventory.js.map +1 -0
- package/dist/session/peer_limits.d.ts +9 -0
- package/dist/session/peer_limits.js +24 -0
- package/dist/session/peer_limits.js.map +1 -0
- package/dist/session/rpc_envelope.d.ts +108 -0
- package/dist/session/rpc_envelope.js +221 -0
- package/dist/session/rpc_envelope.js.map +1 -0
- package/dist/session/rpc_inbound.d.ts +55 -0
- package/dist/session/rpc_inbound.js +136 -0
- package/dist/session/rpc_inbound.js.map +1 -0
- package/dist/session/setup_wizard.d.ts +39 -0
- package/dist/session/setup_wizard.js +63 -0
- package/dist/session/setup_wizard.js.map +1 -0
- package/dist/session/tools.d.ts +24 -0
- package/dist/session/tools.js +272 -0
- package/dist/session/tools.js.map +1 -0
- package/dist/session/wizard.d.ts +13 -0
- package/dist/session/wizard.js +20 -0
- package/dist/session/wizard.js.map +1 -0
- package/dist/transport/peer_channel.d.ts +52 -0
- package/dist/transport/peer_channel.js +145 -0
- package/dist/transport/peer_channel.js.map +1 -0
- package/dist/transport/pi_forward_client.d.ts +29 -0
- package/dist/transport/pi_forward_client.js +68 -0
- package/dist/transport/pi_forward_client.js.map +1 -0
- package/dist/transport/relay_client.d.ts +96 -0
- package/dist/transport/relay_client.js +210 -0
- package/dist/transport/relay_client.js.map +1 -0
- package/dist/ui/footer.d.ts +32 -0
- package/dist/ui/footer.js +34 -0
- package/dist/ui/footer.js.map +1 -0
- package/docs/daemon.md +289 -0
- package/package.json +92 -0
- package/service-templates/launchd.plist.template +35 -0
- package/service-templates/systemd.service.template +19 -0
- package/service-templates/task-launcher.vbs.template +10 -0
- package/service-templates/task-scheduler.xml.template +38 -0
- package/skills/agent-network/SKILL.md +310 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { LocalConfig } from "./local_config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pi SDK UI surface needed by the wizard. Subset of `ExtensionUIContext` —
|
|
4
|
+
* declared inline so tests can mock cleanly without dragging the full
|
|
5
|
+
* ExtensionContext shape.
|
|
6
|
+
*/
|
|
7
|
+
export interface WizardUI {
|
|
8
|
+
/** Free-text prompt. Returns the entered string, or undefined if cancelled. */
|
|
9
|
+
input?: (title: string, options?: {
|
|
10
|
+
defaultValue?: string;
|
|
11
|
+
}) => Promise<string | undefined>;
|
|
12
|
+
/** Picker. Returns the picked option, or undefined if cancelled. */
|
|
13
|
+
select: (title: string, options: string[]) => Promise<string | undefined>;
|
|
14
|
+
/** Non-blocking notification. Used for inline validation feedback. */
|
|
15
|
+
notify?: (msg: string, kind: "info" | "warning" | "error") => void;
|
|
16
|
+
}
|
|
17
|
+
export interface WizardDefaults {
|
|
18
|
+
agent_name: string;
|
|
19
|
+
use_relay: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Runs the 2-question setup wizard. Returns the chosen config on confirm, or
|
|
23
|
+
* null when the user cancels any prompt.
|
|
24
|
+
*
|
|
25
|
+
* Prompts:
|
|
26
|
+
* 1. Agent name (default: parent/folder of cwd)
|
|
27
|
+
* 2. Use the relay on this terminal? (yes/no) — gates connection to the
|
|
28
|
+
* remote mesh (mobile devices + other PCs over the relay). "No" means
|
|
29
|
+
* local-only: this Pi joins the UDS mesh but doesn't open WSS.
|
|
30
|
+
* Final: review + confirm "Save and activate?" yes/no
|
|
31
|
+
*
|
|
32
|
+
* Daemon mode (run agents 24/7 via systemd/launchd) is intentionally NOT in
|
|
33
|
+
* the wizard — it's an explicit, separate opt-in via `/unbien install`.
|
|
34
|
+
*
|
|
35
|
+
* The local UDS mesh is always single per machine ("local" session) — no
|
|
36
|
+
* session question. All Pis on the same machine see each other through
|
|
37
|
+
* the same broker.
|
|
38
|
+
*/
|
|
39
|
+
export declare function runSetupWizard(ui: WizardUI, defaults: WizardDefaults): Promise<LocalConfig | null>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const YES = "Yes";
|
|
2
|
+
const NO = "No";
|
|
3
|
+
const CANCEL_TOKEN = "__cancel__";
|
|
4
|
+
/**
|
|
5
|
+
* Runs the 2-question setup wizard. Returns the chosen config on confirm, or
|
|
6
|
+
* null when the user cancels any prompt.
|
|
7
|
+
*
|
|
8
|
+
* Prompts:
|
|
9
|
+
* 1. Agent name (default: parent/folder of cwd)
|
|
10
|
+
* 2. Use the relay on this terminal? (yes/no) — gates connection to the
|
|
11
|
+
* remote mesh (mobile devices + other PCs over the relay). "No" means
|
|
12
|
+
* local-only: this Pi joins the UDS mesh but doesn't open WSS.
|
|
13
|
+
* Final: review + confirm "Save and activate?" yes/no
|
|
14
|
+
*
|
|
15
|
+
* Daemon mode (run agents 24/7 via systemd/launchd) is intentionally NOT in
|
|
16
|
+
* the wizard — it's an explicit, separate opt-in via `/unbien install`.
|
|
17
|
+
*
|
|
18
|
+
* The local UDS mesh is always single per machine ("local" session) — no
|
|
19
|
+
* session question. All Pis on the same machine see each other through
|
|
20
|
+
* the same broker.
|
|
21
|
+
*/
|
|
22
|
+
export async function runSetupWizard(ui, defaults) {
|
|
23
|
+
const agent_name = await _askText(ui, "Agent name:", defaults.agent_name);
|
|
24
|
+
if (agent_name === null)
|
|
25
|
+
return null;
|
|
26
|
+
ui.notify?.("The relay forwards encrypted messages to the Remote Pi mobile app and other PCs in your mesh. Skip this if you only want a local-only mesh on this machine.", "info");
|
|
27
|
+
const useRelayChoice = await ui.select("Use the relay on this terminal to connect to the remote mesh (mobile + PCs)?", defaults.use_relay ? [YES, NO] : [NO, YES]);
|
|
28
|
+
if (!useRelayChoice)
|
|
29
|
+
return null;
|
|
30
|
+
const auto_start_relay = useRelayChoice === YES;
|
|
31
|
+
// Review + confirm
|
|
32
|
+
const summary = [
|
|
33
|
+
` Agent name: ${agent_name}`,
|
|
34
|
+
` Use relay: ${auto_start_relay ? YES : NO}`,
|
|
35
|
+
].join("\n");
|
|
36
|
+
ui.notify?.(`Summary:\n${summary}`, "info");
|
|
37
|
+
const confirm = await ui.select("Save and activate?", [YES, NO]);
|
|
38
|
+
if (confirm !== YES)
|
|
39
|
+
return null;
|
|
40
|
+
return { agent_name, auto_start_relay };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Asks the user for free text. The Pi SDK's `ui.input` does not pre-fill the
|
|
44
|
+
* field with `defaultValue` (the SDK ignores that option), so we surface the
|
|
45
|
+
* default in the prompt label and treat an empty submission as "accept the
|
|
46
|
+
* default" — the standard CLI convention. Falls back to `select` when the
|
|
47
|
+
* SDK doesn't expose `input` at all.
|
|
48
|
+
*/
|
|
49
|
+
async function _askText(ui, title, defaultValue) {
|
|
50
|
+
const titleWithHint = `${title} (default: ${defaultValue})`;
|
|
51
|
+
const raw = ui.input
|
|
52
|
+
? await ui.input(titleWithHint, { defaultValue })
|
|
53
|
+
: await ui.select(titleWithHint, [defaultValue, CANCEL_TOKEN]);
|
|
54
|
+
if (raw === undefined)
|
|
55
|
+
return null;
|
|
56
|
+
if (raw === CANCEL_TOKEN)
|
|
57
|
+
return null;
|
|
58
|
+
const trimmed = raw.trim();
|
|
59
|
+
// Empty submission = accept the default. No re-prompt, no warning — the
|
|
60
|
+
// user explicitly asked for the default by hitting enter.
|
|
61
|
+
return trimmed.length > 0 ? trimmed : defaultValue;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=setup_wizard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup_wizard.js","sourceRoot":"","sources":["../../src/session/setup_wizard.ts"],"names":[],"mappings":"AAwBA,MAAM,GAAG,GAAG,KAAK,CAAC;AAClB,MAAM,EAAE,GAAG,IAAI,CAAC;AAChB,MAAM,YAAY,GAAG,YAAY,CAAC;AAElC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,EAAY,EACZ,QAAwB;IAExB,MAAM,UAAU,GAAG,MAAM,QAAQ,CAAC,EAAE,EAAE,aAAa,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC1E,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAErC,EAAE,CAAC,MAAM,EAAE,CACT,6JAA6J,EAC7J,MAAM,CACP,CAAC;IACF,MAAM,cAAc,GAAG,MAAM,EAAE,CAAC,MAAM,CACpC,8EAA8E,EAC9E,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,CAC3C,CAAC;IACF,IAAI,CAAC,cAAc;QAAE,OAAO,IAAI,CAAC;IACjC,MAAM,gBAAgB,GAAG,cAAc,KAAK,GAAG,CAAC;IAEhD,mBAAmB;IACnB,MAAM,OAAO,GAAG;QACd,oBAAoB,UAAU,EAAE;QAChC,oBAAoB,gBAAgB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;KAClD,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACb,EAAE,CAAC,MAAM,EAAE,CAAC,aAAa,OAAO,EAAE,EAAE,MAAM,CAAC,CAAC;IAE5C,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,oBAAoB,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;IACjE,IAAI,OAAO,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAEjC,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,CAAC;AAC1C,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,QAAQ,CACrB,EAAY,EACZ,KAAa,EACb,YAAoB;IAEpB,MAAM,aAAa,GAAG,GAAG,KAAK,cAAc,YAAY,GAAG,CAAC;IAC5D,MAAM,GAAG,GAAG,EAAE,CAAC,KAAK;QAClB,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,aAAa,EAAE,EAAE,YAAY,EAAE,CAAC;QACjD,CAAC,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC;IACjE,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,GAAG,KAAK,YAAY;QAAE,OAAO,IAAI,CAAC;IACtC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,wEAAwE;IACxE,0DAA0D;IAC1D,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC;AACrD,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { SessionPeer } from "./peer.js";
|
|
3
|
+
/**
|
|
4
|
+
* Registers the native tools the Pi LLM uses to talk to other agents in the
|
|
5
|
+
* same UDS session (plano 19 transport + plan/25 Wave 0 ACK protocol):
|
|
6
|
+
*
|
|
7
|
+
* - `agent_send` — unified delivery with broker-level ACK. Returns
|
|
8
|
+
* a status so the LLM can decide whether to retry.
|
|
9
|
+
* For unicast targets the broker auto-replies with
|
|
10
|
+
* `received | busy | denied`. For broadcast/multicast
|
|
11
|
+
* the tool is fire-and-forget (status `sent`).
|
|
12
|
+
* - `agent_request` — **deprecated**. Still works (send + block on reply
|
|
13
|
+
* via `re` correlation) but the LLM should migrate
|
|
14
|
+
* to the event-driven send+inbox pattern. Each call
|
|
15
|
+
* emits a one-shot warning to stderr.
|
|
16
|
+
*
|
|
17
|
+
* Reply pattern (new world): when you receive a message you want to answer,
|
|
18
|
+
* send back another envelope with `re=<original-id>`. The original sender
|
|
19
|
+
* sees that reply in its inbox during a future turn.
|
|
20
|
+
*
|
|
21
|
+
* `getSessionPeer` is a getter (not a captured value) so changes to the
|
|
22
|
+
* underlying `_sessionPeer` module variable are observed live.
|
|
23
|
+
*/
|
|
24
|
+
export declare function registerAgentTools(pi: ExtensionAPI, getSessionPeer: () => SessionPeer | null): void;
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
const NOT_IN_SESSION = "Not in a session. Run /unbien join first";
|
|
3
|
+
const ACK_TIMEOUT_MS = 5_000;
|
|
4
|
+
const LEGACY_REQUEST_TIMEOUT_MS = 30_000;
|
|
5
|
+
const LIST_PEERS_TIMEOUT_MS = 2_000;
|
|
6
|
+
/**
|
|
7
|
+
* Registers the native tools the Pi LLM uses to talk to other agents in the
|
|
8
|
+
* same UDS session (plano 19 transport + plan/25 Wave 0 ACK protocol):
|
|
9
|
+
*
|
|
10
|
+
* - `agent_send` — unified delivery with broker-level ACK. Returns
|
|
11
|
+
* a status so the LLM can decide whether to retry.
|
|
12
|
+
* For unicast targets the broker auto-replies with
|
|
13
|
+
* `received | busy | denied`. For broadcast/multicast
|
|
14
|
+
* the tool is fire-and-forget (status `sent`).
|
|
15
|
+
* - `agent_request` — **deprecated**. Still works (send + block on reply
|
|
16
|
+
* via `re` correlation) but the LLM should migrate
|
|
17
|
+
* to the event-driven send+inbox pattern. Each call
|
|
18
|
+
* emits a one-shot warning to stderr.
|
|
19
|
+
*
|
|
20
|
+
* Reply pattern (new world): when you receive a message you want to answer,
|
|
21
|
+
* send back another envelope with `re=<original-id>`. The original sender
|
|
22
|
+
* sees that reply in its inbox during a future turn.
|
|
23
|
+
*
|
|
24
|
+
* `getSessionPeer` is a getter (not a captured value) so changes to the
|
|
25
|
+
* underlying `_sessionPeer` module variable are observed live.
|
|
26
|
+
*/
|
|
27
|
+
export function registerAgentTools(pi, getSessionPeer) {
|
|
28
|
+
const SendParams = Type.Object({
|
|
29
|
+
to: Type.String({
|
|
30
|
+
description: "Recipient agent name (e.g. 'backend'), 'broadcast', or array of names. " +
|
|
31
|
+
"Broadcast/multicast are fire-and-forget; unicast returns an ACK status.",
|
|
32
|
+
}),
|
|
33
|
+
body: Type.Unknown({
|
|
34
|
+
description: "Free-form JSON payload. String or object — your choice.",
|
|
35
|
+
}),
|
|
36
|
+
re: Type.Optional(Type.String({
|
|
37
|
+
description: "Set this to the `id` of an incoming message when you are REPLYING to it. " +
|
|
38
|
+
"The peer correlates your answer with their original send via this field.",
|
|
39
|
+
})),
|
|
40
|
+
});
|
|
41
|
+
const RequestParams = Type.Object({
|
|
42
|
+
to: Type.String({
|
|
43
|
+
description: "Recipient agent name. Must be a single peer (not broadcast).",
|
|
44
|
+
}),
|
|
45
|
+
body: Type.Unknown({ description: "Free-form JSON payload to send." }),
|
|
46
|
+
timeout_ms: Type.Optional(Type.Number({
|
|
47
|
+
description: "Optional override of the default 30s reply timeout. Per-request.",
|
|
48
|
+
})),
|
|
49
|
+
});
|
|
50
|
+
pi.registerTool({
|
|
51
|
+
name: "agent_send",
|
|
52
|
+
label: "Agent Send",
|
|
53
|
+
description: "Send a message to another Pi agent in the current local session and " +
|
|
54
|
+
"wait for the broker's delivery ACK. Delivery is reliable: a peer that " +
|
|
55
|
+
"is mid-turn still receives the message (its harness enqueues it for " +
|
|
56
|
+
"the next turn), so you never need to retry. Returns one of: `received` " +
|
|
57
|
+
"(delivered; peer will process it in an upcoming turn), `denied` (peer " +
|
|
58
|
+
"refused), `timeout` (no ACK in 5s — peer offline / transport error), " +
|
|
59
|
+
"`sent` (broadcast/multicast — no ACK semantics). Use `re` to mark this " +
|
|
60
|
+
"message as a reply to an incoming envelope's `id` (correlation only).",
|
|
61
|
+
promptSnippet: "agent_send({to, body, re?}): unicast → returns {status: received|denied|timeout} (delivery is reliable; no retry-on-busy). Broadcast/multicast → fire-and-forget ({status:'sent'}).",
|
|
62
|
+
parameters: SendParams,
|
|
63
|
+
execute: async (_toolCallId, params) => {
|
|
64
|
+
const peer = getSessionPeer();
|
|
65
|
+
if (!peer) {
|
|
66
|
+
const details = {
|
|
67
|
+
status: "refused",
|
|
68
|
+
ok: false,
|
|
69
|
+
error: NOT_IN_SESSION,
|
|
70
|
+
};
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: "text", text: NOT_IN_SESSION }],
|
|
73
|
+
details,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const { to, body, re } = params;
|
|
77
|
+
if (to === peer.address() || to === peer.name()) {
|
|
78
|
+
const msg = `Refused: cannot agent_send to yourself ("${to}"). Just do the work directly.`;
|
|
79
|
+
const details = {
|
|
80
|
+
status: "refused",
|
|
81
|
+
ok: false,
|
|
82
|
+
error: msg,
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
content: [{ type: "text", text: msg }],
|
|
86
|
+
details,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const isUnicast = to !== "broadcast";
|
|
90
|
+
// Broadcast: fire-and-forget. Broker doesn't ACK multi-target sends.
|
|
91
|
+
if (!isUnicast) {
|
|
92
|
+
try {
|
|
93
|
+
await peer.send(to, body, re ?? null);
|
|
94
|
+
const details = { status: "sent", ok: true };
|
|
95
|
+
return {
|
|
96
|
+
content: [{ type: "text", text: `Broadcast sent.` }],
|
|
97
|
+
details,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
102
|
+
const details = {
|
|
103
|
+
status: "timeout",
|
|
104
|
+
ok: false,
|
|
105
|
+
error: msg,
|
|
106
|
+
};
|
|
107
|
+
return {
|
|
108
|
+
content: [{ type: "text", text: `Broadcast failed: ${msg}` }],
|
|
109
|
+
details,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Unicast: wait for broker ACK.
|
|
114
|
+
try {
|
|
115
|
+
const ack = await peer.sendWithAck(to, body, re ?? null, ACK_TIMEOUT_MS);
|
|
116
|
+
const ok = ack.status === "received";
|
|
117
|
+
const details = {
|
|
118
|
+
status: ack.status,
|
|
119
|
+
ok,
|
|
120
|
+
...(ack.target === undefined ? {} : { target: ack.target }),
|
|
121
|
+
...(ack.error === undefined ? {} : { error: ack.error }),
|
|
122
|
+
...(ack.reason === undefined ? {} : { reason: ack.reason }),
|
|
123
|
+
};
|
|
124
|
+
const text = _formatAck(to, ack, re);
|
|
125
|
+
return { content: [{ type: "text", text }], details };
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
129
|
+
const details = {
|
|
130
|
+
status: "timeout",
|
|
131
|
+
ok: false,
|
|
132
|
+
error: msg,
|
|
133
|
+
};
|
|
134
|
+
return {
|
|
135
|
+
content: [{ type: "text", text: `Failed to send: ${msg}` }],
|
|
136
|
+
details,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
const ListPeersParams = Type.Object({});
|
|
142
|
+
pi.registerTool({
|
|
143
|
+
name: "list_peers",
|
|
144
|
+
label: "List Peers",
|
|
145
|
+
description: "Returns the current peer inventory in this session as ADDRESSES of the " +
|
|
146
|
+
"form `<cwd>@<name>` (cross-PC peers prefixed `<pc>:`). An address is an " +
|
|
147
|
+
"opaque routing key — pass it to `agent_send`/`agent_request` VERBATIM, " +
|
|
148
|
+
"never build one by hand. Use BEFORE sending whenever you're unsure who's " +
|
|
149
|
+
"available, or after a `peer_joined` / `peer_left` notification to refresh " +
|
|
150
|
+
"your mental model. Resolves in milliseconds — a metadata query to the " +
|
|
151
|
+
"broker, not a turn of another agent.",
|
|
152
|
+
promptSnippet: "list_peers(): returns {peers: string[]} of addresses `<cwd>@<name>` (`<pc>:` prefix cross-PC). Echo an address verbatim to agent_send; never compose one. Cheap; call freely.",
|
|
153
|
+
parameters: ListPeersParams,
|
|
154
|
+
execute: async (_toolCallId) => {
|
|
155
|
+
const peer = getSessionPeer();
|
|
156
|
+
if (!peer) {
|
|
157
|
+
return {
|
|
158
|
+
content: [{ type: "text", text: NOT_IN_SESSION }],
|
|
159
|
+
details: { peers: [] },
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
// Internal use of the request/reply primitive is fine — broker
|
|
164
|
+
// replies are synthesised in-process (`_handleBrokerMessage`)
|
|
165
|
+
// without going through `_route`, so they bypass the ACK path.
|
|
166
|
+
const reply = await peer.request("broker", { type: "list_peers" }, LIST_PEERS_TIMEOUT_MS);
|
|
167
|
+
const body = reply.body;
|
|
168
|
+
const peers = Array.isArray(body?.peers)
|
|
169
|
+
? body.peers.filter((p) => typeof p === "string")
|
|
170
|
+
: [];
|
|
171
|
+
// Drop self from the list — the caller is the only one who can't
|
|
172
|
+
// address itself anyway, so listing it is noise. Peers are ADDRESSES
|
|
173
|
+
// (plan/38), so filter by address.
|
|
174
|
+
const selfAddress = peer.address();
|
|
175
|
+
const filtered = peers.filter((p) => p !== selfAddress);
|
|
176
|
+
const text = filtered.length === 0 ? "(no peers)" : filtered.join("\n");
|
|
177
|
+
return {
|
|
178
|
+
content: [{ type: "text", text }],
|
|
179
|
+
details: { peers: filtered },
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
184
|
+
return {
|
|
185
|
+
content: [{ type: "text", text: `list_peers failed: ${msg}` }],
|
|
186
|
+
details: { peers: [] },
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
pi.registerTool({
|
|
192
|
+
name: "agent_request",
|
|
193
|
+
label: "Agent Request (deprecated)",
|
|
194
|
+
description: "DEPRECATED — prefer `agent_send` + observing your inbox for the " +
|
|
195
|
+
"reply (correlated by `re=<your-send-id>`). This tool still works: " +
|
|
196
|
+
"it sends a message and synchronously blocks until the peer replies " +
|
|
197
|
+
"or the timeout fires. Default 30s. Will be removed in a future " +
|
|
198
|
+
"release; migrate to the event-driven pattern in the agent-network skill.",
|
|
199
|
+
promptSnippet: "agent_request({to, body, timeout_ms?}): DEPRECATED synchronous request/reply (blocks current turn). Prefer agent_send + inbox observation.",
|
|
200
|
+
parameters: RequestParams,
|
|
201
|
+
execute: async (_toolCallId, params) => {
|
|
202
|
+
const peer = getSessionPeer();
|
|
203
|
+
if (!peer) {
|
|
204
|
+
return {
|
|
205
|
+
content: [{ type: "text", text: NOT_IN_SESSION }],
|
|
206
|
+
details: { error: NOT_IN_SESSION },
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const { to, body, timeout_ms } = params;
|
|
210
|
+
if (to === peer.address() || to === peer.name()) {
|
|
211
|
+
const msg = `Refused: cannot agent_request to yourself ("${to}"). Just do the work directly.`;
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: "text", text: msg }],
|
|
214
|
+
details: { error: msg },
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const timeout = typeof timeout_ms === "number" && timeout_ms > 0
|
|
218
|
+
? timeout_ms
|
|
219
|
+
: LEGACY_REQUEST_TIMEOUT_MS;
|
|
220
|
+
try {
|
|
221
|
+
const reply = await peer.request(to, body, timeout);
|
|
222
|
+
const text = typeof reply.body === "string"
|
|
223
|
+
? reply.body
|
|
224
|
+
: JSON.stringify(reply.body);
|
|
225
|
+
return {
|
|
226
|
+
content: [{ type: "text", text }],
|
|
227
|
+
details: reply.body,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
232
|
+
return {
|
|
233
|
+
content: [{ type: "text", text: `Request failed: ${msg}` }],
|
|
234
|
+
details: { error: msg },
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function _formatAck(to, ack, re) {
|
|
241
|
+
const reSuffix = re ? ` (re=${re})` : "";
|
|
242
|
+
const errorSuffix = ack.error ? ` (${ack.error})` : "";
|
|
243
|
+
switch (ack.status) {
|
|
244
|
+
case "received":
|
|
245
|
+
return `Delivered to "${to}"${reSuffix} — peer will process it in an upcoming turn.`;
|
|
246
|
+
case "busy":
|
|
247
|
+
// plan/34 removed busy-drop: the current broker never returns `busy`. If
|
|
248
|
+
// we still see it, an OUT-OF-DATE broker leader dropped the message — so
|
|
249
|
+
// be honest that it was NOT delivered, and point at the fix.
|
|
250
|
+
return (`NOT delivered — "${to}"${reSuffix} came back BUSY, which only ` +
|
|
251
|
+
`happens when an out-of-date broker leader dropped the message. ` +
|
|
252
|
+
`Restart the agent leading the local broker (oldest Pi/unbien process) ` +
|
|
253
|
+
`to pick up the new build, then resend.`);
|
|
254
|
+
case "denied":
|
|
255
|
+
if (ack.reason === "not_authorized") {
|
|
256
|
+
return (`Relay did not authorize delivery to "${to}"${reSuffix}${errorSuffix}. ` +
|
|
257
|
+
`Do not blindly retry; verify authorization first.`);
|
|
258
|
+
}
|
|
259
|
+
if (ack.reason === "bad_envelope") {
|
|
260
|
+
return (`Relay rejected the envelope for "${to}"${reSuffix}${errorSuffix}. ` +
|
|
261
|
+
`Do not blindly retry; correct the envelope first.`);
|
|
262
|
+
}
|
|
263
|
+
return `"${to}" denied the message${reSuffix}. Do not retry; report to user.`;
|
|
264
|
+
case "timeout":
|
|
265
|
+
if (ack.reason === "offline") {
|
|
266
|
+
return (`Immediate Relay offline transport error for "${to}"${reSuffix}${errorSuffix}; ` +
|
|
267
|
+
`reported as timeout without waiting for the ACK deadline.`);
|
|
268
|
+
}
|
|
269
|
+
return `No ACK from "${to}" within ${ACK_TIMEOUT_MS}ms${reSuffix} — transport error. Investigate or retry later.`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../../src/session/tools.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal wizard for `/unbien join` with no argument.
|
|
3
|
+
*
|
|
4
|
+
* The Pi SDK `ExtensionUIContext.select` accepts a title and string options
|
|
5
|
+
* and returns the chosen option. Wizard offers existing sessions + an "explicit
|
|
6
|
+
* create" sentinel. If user picks the sentinel, prompts for a fresh name via
|
|
7
|
+
* `select`-with-option-other (Pi's "Other" escape hatch). Returns the picked
|
|
8
|
+
* session name, or null if user cancelled.
|
|
9
|
+
*/
|
|
10
|
+
export interface WizardUI {
|
|
11
|
+
select(title: string, options: string[]): Promise<string | undefined>;
|
|
12
|
+
}
|
|
13
|
+
export declare function joinWizard(ui: WizardUI, defaultName: string): Promise<string | null>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { listSessions, sessionHasSock } from "./global_config.js";
|
|
2
|
+
const CREATE_SENTINEL = "━━━ Create new session ━━━";
|
|
3
|
+
export async function joinWizard(ui, defaultName) {
|
|
4
|
+
const sessions = listSessions();
|
|
5
|
+
const liveSessions = sessions.filter(sessionHasSock);
|
|
6
|
+
const options = [...liveSessions, CREATE_SENTINEL];
|
|
7
|
+
const picked = await ui.select(liveSessions.length
|
|
8
|
+
? "Choose a session to join, or create a new one"
|
|
9
|
+
: "No active sessions. Create one?", options);
|
|
10
|
+
if (!picked)
|
|
11
|
+
return null;
|
|
12
|
+
if (picked === CREATE_SENTINEL) {
|
|
13
|
+
// Caller is expected to follow up with a name prompt via ctx.ui.select
|
|
14
|
+
// (the Pi SDK input dialog). For non-interactive contexts we return the
|
|
15
|
+
// default name.
|
|
16
|
+
return defaultName;
|
|
17
|
+
}
|
|
18
|
+
return picked;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=wizard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wizard.js","sourceRoot":"","sources":["../../src/session/wizard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAelE,MAAM,eAAe,GAAG,4BAA4B,CAAC;AAErD,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,EAAY,EACZ,WAAmB;IAEnB,MAAM,QAAQ,GAAG,YAAY,EAAE,CAAC;IAChC,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;IACrD,MAAM,OAAO,GAAG,CAAC,GAAG,YAAY,EAAE,eAAe,CAAC,CAAC;IACnD,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAC5B,YAAY,CAAC,MAAM;QACjB,CAAC,CAAC,+CAA+C;QACjD,CAAC,CAAC,iCAAiC,EACrC,OAAO,CACR,CAAC;IACF,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,IAAI,MAAM,KAAK,eAAe,EAAE,CAAC;QAC/B,uEAAuE;QACvE,wEAAwE;QACxE,gBAAgB;QAChB,OAAO,WAAW,CAAC;IACrB,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ClientMessage, ServerMessage } from "../protocol/types.js";
|
|
2
|
+
import { type EnvelopeMessage } from "../session/rpc_envelope.js";
|
|
3
|
+
import type { RelayClient } from "./relay_client.js";
|
|
4
|
+
/** Sink for ServerMessage outbound to the remote app. */
|
|
5
|
+
export interface PeerChannel {
|
|
6
|
+
send(msg: ServerMessage): void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Plaintext PeerChannel backed by a RelayClient WebSocket.
|
|
10
|
+
*
|
|
11
|
+
* Usage (after pair_request handshake completes):
|
|
12
|
+
* const channel = new PlainPeerChannel(relay, appPeerId, myRoomId, onMsg)
|
|
13
|
+
* channel.send(serverMessage) // base64-encodes JSON, routes via relay
|
|
14
|
+
* // incoming relay messages destined for appPeerId are auto-decoded
|
|
15
|
+
* // and delivered via onMessage callback
|
|
16
|
+
*
|
|
17
|
+
* `myRoomId` is the *local* Pi's room id — sent on every outbound envelope
|
|
18
|
+
* so the app can correlate which Pi sent it (multi-pi support, plano 17).
|
|
19
|
+
*/
|
|
20
|
+
export declare class PlainPeerChannel implements PeerChannel {
|
|
21
|
+
private readonly relay;
|
|
22
|
+
private readonly remotePeerId;
|
|
23
|
+
private readonly onMessage;
|
|
24
|
+
/** Route an inbound mesh-envelope ({rpc|evt}, docs/rpc-envelope.md) to the
|
|
25
|
+
* new-protocol dispatcher. Absent → the channel ignores envelope frames. */
|
|
26
|
+
private readonly onRpc?;
|
|
27
|
+
private readonly _unsubscribe;
|
|
28
|
+
constructor(relay: RelayClient, remotePeerId: string,
|
|
29
|
+
/**
|
|
30
|
+
* This Pi's room id. Currently NOT injected in the outer envelope
|
|
31
|
+
* (defensive — relay/app not yet ready). Kept in the constructor for
|
|
32
|
+
* forward-compat so callers don't need to change again when we re-enable.
|
|
33
|
+
*/
|
|
34
|
+
myRoomId: string | undefined, onMessage: (msg: ClientMessage) => void,
|
|
35
|
+
/** Called when this specific peer connection is considered lost. */
|
|
36
|
+
_onDisconnect?: () => void,
|
|
37
|
+
/** Route an inbound mesh-envelope ({rpc|evt}, docs/rpc-envelope.md) to the
|
|
38
|
+
* new-protocol dispatcher. Absent → the channel ignores envelope frames. */
|
|
39
|
+
onRpc?: ((env: EnvelopeMessage, sender: PlainPeerChannel) => void) | undefined);
|
|
40
|
+
send(msg: ServerMessage): void;
|
|
41
|
+
/**
|
|
42
|
+
* Send an rpc-envelope message (`{ rpc | evt }`, docs/rpc-envelope.md) to the
|
|
43
|
+
* app. Same base64-into-`ct` outer wire as `send` — the relay stays opaque —
|
|
44
|
+
* but the inner payload is an `EnvelopeMessage`, not a `ServerMessage`.
|
|
45
|
+
* Best-effort, mirroring `send`: a relay mid-reconnect must not throw an
|
|
46
|
+
* uncaught exception out of the SDK event callback that drives it.
|
|
47
|
+
*/
|
|
48
|
+
sendEnvelope(env: EnvelopeMessage): void;
|
|
49
|
+
/** Detaches from relay (does not close the relay itself). */
|
|
50
|
+
detach(): void;
|
|
51
|
+
private _onLine;
|
|
52
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { UB_KIND, RPC_KIND, EVT_KIND, isEnvelopeFrame, } from "../session/rpc_envelope.js";
|
|
2
|
+
import { envLog } from "../session/debug_log.js";
|
|
3
|
+
/**
|
|
4
|
+
* Plaintext PeerChannel backed by a RelayClient WebSocket.
|
|
5
|
+
*
|
|
6
|
+
* Usage (after pair_request handshake completes):
|
|
7
|
+
* const channel = new PlainPeerChannel(relay, appPeerId, myRoomId, onMsg)
|
|
8
|
+
* channel.send(serverMessage) // base64-encodes JSON, routes via relay
|
|
9
|
+
* // incoming relay messages destined for appPeerId are auto-decoded
|
|
10
|
+
* // and delivered via onMessage callback
|
|
11
|
+
*
|
|
12
|
+
* `myRoomId` is the *local* Pi's room id — sent on every outbound envelope
|
|
13
|
+
* so the app can correlate which Pi sent it (multi-pi support, plano 17).
|
|
14
|
+
*/
|
|
15
|
+
export class PlainPeerChannel {
|
|
16
|
+
relay;
|
|
17
|
+
remotePeerId;
|
|
18
|
+
onMessage;
|
|
19
|
+
onRpc;
|
|
20
|
+
_unsubscribe;
|
|
21
|
+
constructor(relay, remotePeerId,
|
|
22
|
+
/**
|
|
23
|
+
* This Pi's room id. Currently NOT injected in the outer envelope
|
|
24
|
+
* (defensive — relay/app not yet ready). Kept in the constructor for
|
|
25
|
+
* forward-compat so callers don't need to change again when we re-enable.
|
|
26
|
+
*/
|
|
27
|
+
myRoomId, onMessage,
|
|
28
|
+
/** Called when this specific peer connection is considered lost. */
|
|
29
|
+
_onDisconnect,
|
|
30
|
+
/** Route an inbound mesh-envelope ({rpc|evt}, docs/rpc-envelope.md) to the
|
|
31
|
+
* new-protocol dispatcher. Absent → the channel ignores envelope frames. */
|
|
32
|
+
onRpc) {
|
|
33
|
+
this.relay = relay;
|
|
34
|
+
this.remotePeerId = remotePeerId;
|
|
35
|
+
this.onMessage = onMessage;
|
|
36
|
+
this.onRpc = onRpc;
|
|
37
|
+
const listener = (line) => this._onLine(line);
|
|
38
|
+
relay.on("message", listener);
|
|
39
|
+
this._unsubscribe = () => relay.off("message", listener);
|
|
40
|
+
void _onDisconnect;
|
|
41
|
+
void myRoomId; // intentionally unused — see send() comment
|
|
42
|
+
}
|
|
43
|
+
// ── PeerChannel interface ──────────────────────────────────────────────────
|
|
44
|
+
send(msg) {
|
|
45
|
+
const ct = Buffer.from(JSON.stringify(msg)).toString("base64");
|
|
46
|
+
// NOTE: `room` removed from the outer envelope until relay (W1.A) + app
|
|
47
|
+
// (W1.C) accept the field. Multi-Pi multiplexing already works via
|
|
48
|
+
// `room_id`/`room_meta` in the WS-level `hello` — outer routing stays by
|
|
49
|
+
// `peer` alone. Re-add the field once downstream is ready.
|
|
50
|
+
const outer = { peer: this.remotePeerId, ct };
|
|
51
|
+
// Best-effort delivery. The relay WS can be mid-reconnect (idle/NAT drop, or
|
|
52
|
+
// a session_new/session-replacement teardown) when we push a server→app frame
|
|
53
|
+
// — notably the action_ok/action_error ack a handler emits right after
|
|
54
|
+
// newSession. `relay.send` throws "relay: not connected" in that window; since
|
|
55
|
+
// this runs inside an async SDK event callback, letting it propagate becomes an
|
|
56
|
+
// uncaughtException that kills the whole pi process. The relay auto-reconnects
|
|
57
|
+
// and the app re-syncs via session_sync, so a dropped frame is recoverable — a
|
|
58
|
+
// crash is not. Mirrors RelayClient.sendControl's no-op-when-closed policy.
|
|
59
|
+
try {
|
|
60
|
+
this.relay.send(JSON.stringify(outer));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
/* relay down — drop this frame; reconnect + session_sync will recover */
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Send an rpc-envelope message (`{ rpc | evt }`, docs/rpc-envelope.md) to the
|
|
68
|
+
* app. Same base64-into-`ct` outer wire as `send` — the relay stays opaque —
|
|
69
|
+
* but the inner payload is an `EnvelopeMessage`, not a `ServerMessage`.
|
|
70
|
+
* Best-effort, mirroring `send`: a relay mid-reconnect must not throw an
|
|
71
|
+
* uncaught exception out of the SDK event callback that drives it.
|
|
72
|
+
*/
|
|
73
|
+
sendEnvelope(env) {
|
|
74
|
+
// Stamp the wrapper kind + timestamp at the single outbound choke.
|
|
75
|
+
const wire = {
|
|
76
|
+
...env,
|
|
77
|
+
// Namespace stamp: each plane stamps its REAL type — ub-plane -> "ub",
|
|
78
|
+
// evt -> "evt", rpc (the spine / default) -> "rpc". Legacy "env" is no
|
|
79
|
+
// longer stamped (still accepted on read one transition).
|
|
80
|
+
type: env.type ??
|
|
81
|
+
(env.ub !== undefined
|
|
82
|
+
? UB_KIND
|
|
83
|
+
: env.evt !== undefined
|
|
84
|
+
? EVT_KIND
|
|
85
|
+
: RPC_KIND),
|
|
86
|
+
ts: env.ts ?? Date.now(),
|
|
87
|
+
};
|
|
88
|
+
const ct = Buffer.from(JSON.stringify(wire)).toString("base64");
|
|
89
|
+
const outer = { peer: this.remotePeerId, ct };
|
|
90
|
+
try {
|
|
91
|
+
this.relay.send(JSON.stringify(outer));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
/* relay down — drop this frame; reconnect + session_sync will recover */
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** Detaches from relay (does not close the relay itself). */
|
|
98
|
+
detach() {
|
|
99
|
+
this._unsubscribe();
|
|
100
|
+
}
|
|
101
|
+
// ── Incoming line from relay ────────────────────────────────────────────────
|
|
102
|
+
_onLine(line) {
|
|
103
|
+
let outer;
|
|
104
|
+
try {
|
|
105
|
+
outer = JSON.parse(line);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return; // malformed line
|
|
109
|
+
}
|
|
110
|
+
if (outer.peer !== this.remotePeerId)
|
|
111
|
+
return;
|
|
112
|
+
if (!outer.ct)
|
|
113
|
+
return;
|
|
114
|
+
let plaintext;
|
|
115
|
+
try {
|
|
116
|
+
plaintext = Buffer.from(outer.ct, "base64").toString("utf8");
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
let msg;
|
|
122
|
+
try {
|
|
123
|
+
msg = JSON.parse(plaintext);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (!msg || typeof msg !== "object")
|
|
129
|
+
return;
|
|
130
|
+
const obj = msg;
|
|
131
|
+
envLog(`inbound<-${this.remotePeerId.slice(0, 8)}: type=${String(obj.type)} rpc=${obj.rpc !== undefined} evt=${obj.evt !== undefined}`);
|
|
132
|
+
// Mesh-envelope inbound (any plane, real-typed or legacy "env" or bare
|
|
133
|
+
// field-presence) routes to the envelope dispatcher, NOT the stock
|
|
134
|
+
// ClientMessage switch. Shared predicate with index.ts's reconnect guard so
|
|
135
|
+
// they can't drift; the dispatcher branches by plane (rpc vs ub) itself.
|
|
136
|
+
if (isEnvelopeFrame(obj)) {
|
|
137
|
+
this.onRpc?.(obj, this);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (typeof obj.type !== "string")
|
|
141
|
+
return;
|
|
142
|
+
this.onMessage(msg);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=peer_channel.js.map
|