agent-comms 8.15.9 → 8.15.11
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/dist/bridges/user/web/server.d.ts.map +1 -1
- package/dist/bridges/user/web/server.js +44 -24
- package/dist/bridges/user/web/server.js.map +1 -1
- package/dist/bridges/user/web/test/web-assets.unit.test.d.ts +5 -0
- package/dist/bridges/user/web/test/web-assets.unit.test.d.ts.map +1 -0
- package/dist/bridges/user/web/test/web-assets.unit.test.js +90 -0
- package/dist/bridges/user/web/test/web-assets.unit.test.js.map +1 -0
- package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.d.ts +2 -0
- package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.d.ts.map +1 -0
- package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.js +75 -0
- package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.js.map +1 -0
- package/dist/bridges/user/web/web-assets.d.ts +28 -0
- package/dist/bridges/user/web/web-assets.d.ts.map +1 -0
- package/dist/bridges/user/web/web-assets.js +67 -0
- package/dist/bridges/user/web/web-assets.js.map +1 -0
- package/dist/test/pi-manifest-packed.integration.test.d.ts +2 -0
- package/dist/test/pi-manifest-packed.integration.test.d.ts.map +1 -0
- package/dist/test/pi-manifest-packed.integration.test.js +68 -0
- package/dist/test/pi-manifest-packed.integration.test.js.map +1 -0
- package/package.json +8 -2
- package/src/account-cli.ts +195 -0
- package/src/account-join-cli.ts +191 -0
- package/src/bridges/cc-peer/approval-commands.ts +82 -0
- package/src/bridges/cc-peer/bridge.ts +138 -0
- package/src/bridges/cc-peer/default-front.ts +58 -0
- package/src/bridges/cc-peer/front-controller.ts +160 -0
- package/src/bridges/cc-peer/front-relay.ts +191 -0
- package/src/bridges/cc-peer/front-runtime.ts +167 -0
- package/src/bridges/cc-peer/front.ts +59 -0
- package/src/bridges/cc-peer/reply-aliases.ts +90 -0
- package/src/bridges/cc-peer/run.ts +100 -0
- package/src/bridges/cc-peer/shared-peer.ts +28 -0
- package/src/bridges/claude-code/channel.ts +318 -0
- package/src/bridges/codex/tool.ts +132 -0
- package/src/bridges/mcp/server.ts +137 -0
- package/src/bridges/opencode/plugin.ts +130 -0
- package/src/bridges/pi/extension.ts +330 -0
- package/src/bridges/registry.ts +17 -0
- package/src/bridges/user/cli.ts +87 -0
- package/src/bridges/user/controller.ts +345 -0
- package/src/bridges/user/tui.ts +300 -0
- package/src/bridges/user/web/actions.ts +173 -0
- package/src/bridges/user/web/contract.ts +222 -0
- package/src/bridges/user/web/dist/bundle.js +49 -0
- package/src/bridges/user/web/dist/icons/icon-192x192.svg +9 -0
- package/src/bridges/user/web/dist/icons/icon-512x512.svg +9 -0
- package/src/bridges/user/web/dist/icons/icon-96x96.svg +9 -0
- package/src/bridges/user/web/dist/index.html +21 -0
- package/src/bridges/user/web/dist/manifest.json +26 -0
- package/src/bridges/user/web/dist/mesh-worker.js +64 -0
- package/src/bridges/user/web/dist/sw.js +1 -0
- package/src/bridges/user/web/event-publisher.ts +114 -0
- package/src/bridges/user/web/port-discovery.ts +49 -0
- package/src/bridges/user/web/reads.ts +35 -0
- package/src/bridges/user/web/router.ts +158 -0
- package/src/bridges/user/web/server.ts +782 -0
- package/src/bridges/user/web/vite.config.ts +71 -0
- package/src/bridges/user/web/web-access.ts +404 -0
- package/src/bridges/user/web/web-assets.ts +90 -0
- package/src/bridges/user/web/web-beacon.ts +69 -0
- package/src/bridges/user/web/web-console-static.ts +90 -0
- package/src/cli-main.ts +635 -0
- package/src/cli.ts +13 -0
- package/src/core/account-bundle.ts +264 -0
- package/src/core/account-join.ts +289 -0
- package/src/core/account-ledger-crypto.ts +154 -0
- package/src/core/account-ledger-store.ts +66 -0
- package/src/core/account-ledger.ts +385 -0
- package/src/core/agent-details.ts +216 -0
- package/src/core/agent-list-format.ts +97 -0
- package/src/core/agent-registry.ts +282 -0
- package/src/core/agent-table.ts +58 -0
- package/src/core/atomic-file.ts +46 -0
- package/src/core/bind-retry.ts +38 -0
- package/src/core/bridge-mesh.ts +176 -0
- package/src/core/bridge.ts +718 -0
- package/src/core/capability-ask.ts +162 -0
- package/src/core/capability-bubble-up.ts +237 -0
- package/src/core/comms-store.ts +130 -0
- package/src/core/connection-approval.ts +134 -0
- package/src/core/connection-code-tool.ts +92 -0
- package/src/core/connection-code.ts +296 -0
- package/src/core/coordinator-role.ts +224 -0
- package/src/core/data-directory.ts +32 -0
- package/src/core/data-frame-routing.ts +309 -0
- package/src/core/default-command.ts +12 -0
- package/src/core/delegation-policy.ts +41 -0
- package/src/core/delivery-engine.ts +649 -0
- package/src/core/device-membership-verification.ts +125 -0
- package/src/core/device-membership.ts +130 -0
- package/src/core/directory-admission.ts +155 -0
- package/src/core/discovery-mdns.ts +247 -0
- package/src/core/discovery-tailscale.ts +224 -0
- package/src/core/discovery.ts +303 -0
- package/src/core/display-name.ts +73 -0
- package/src/core/dm-grant-actions.ts +72 -0
- package/src/core/dm-send-delegation.ts +73 -0
- package/src/core/dm-token-verification.ts +76 -0
- package/src/core/election-sessions.ts +135 -0
- package/src/core/error-reporting.ts +85 -0
- package/src/core/first-contact.ts +242 -0
- package/src/core/gateway-trust-actions.ts +184 -0
- package/src/core/gateway-trust.ts +305 -0
- package/src/core/gossip-directory.ts +276 -0
- package/src/core/gossip-extensions.ts +123 -0
- package/src/core/group-proofs.ts +205 -0
- package/src/core/handshake.ts +354 -0
- package/src/core/hub-forwarding.ts +24 -0
- package/src/core/hub-link.ts +176 -0
- package/src/core/hub-session.ts +374 -0
- package/src/core/identity-store.ts +715 -0
- package/src/core/identity.ts +489 -0
- package/src/core/index.ts +65 -0
- package/src/core/issuer-grouping.ts +165 -0
- package/src/core/issuer-identity-file.ts +187 -0
- package/src/core/lan-web-actions.ts +34 -0
- package/src/core/lan-web-uis.ts +55 -0
- package/src/core/ledger-lock.ts +81 -0
- package/src/core/listener-registry.ts +133 -0
- package/src/core/loopback-address.ts +22 -0
- package/src/core/machine-identity.ts +77 -0
- package/src/core/machine-list-groups.ts +53 -0
- package/src/core/mcp-registry-retry.ts +69 -0
- package/src/core/mcp-registry-version.ts +18 -0
- package/src/core/membership-proof.ts +139 -0
- package/src/core/membership-proofs.ts +149 -0
- package/src/core/mesh-graph-trace-tool.ts +81 -0
- package/src/core/mesh-graph.ts +89 -0
- package/src/core/mesh-network-actions.ts +183 -0
- package/src/core/mesh-store-options.ts +23 -0
- package/src/core/mesh-store-shared.ts +134 -0
- package/src/core/mesh-store.ts +1114 -0
- package/src/core/name-claim.ts +145 -0
- package/src/core/naming-actions.ts +90 -0
- package/src/core/naming.ts +138 -0
- package/src/core/nanoid.ts +20 -0
- package/src/core/network-interfaces.ts +24 -0
- package/src/core/package-version.ts +34 -0
- package/src/core/peer-lifecycle.ts +377 -0
- package/src/core/peer-link-error.ts +41 -0
- package/src/core/peer-versions.ts +98 -0
- package/src/core/pending-connection.ts +53 -0
- package/src/core/petnames.ts +121 -0
- package/src/core/pgp-keyserver.ts +32 -0
- package/src/core/pid-lock-file.ts +122 -0
- package/src/core/push-manager.ts +76 -0
- package/src/core/random-id.ts +9 -0
- package/src/core/receiver-set.ts +42 -0
- package/src/core/relay-role.ts +182 -0
- package/src/core/relay-selection.ts +120 -0
- package/src/core/relay-server.ts +198 -0
- package/src/core/relay-uplink.ts +78 -0
- package/src/core/request-timeouts.ts +37 -0
- package/src/core/room-discovery.ts +48 -0
- package/src/core/room-lifecycle.ts +838 -0
- package/src/core/room-lookup.ts +41 -0
- package/src/core/room-messaging.ts +247 -0
- package/src/core/room-notice-log.ts +61 -0
- package/src/core/room-path.ts +84 -0
- package/src/core/room-protocol.ts +893 -0
- package/src/core/room-router.ts +180 -0
- package/src/core/room-token-verification.ts +85 -0
- package/src/core/room-wire-extensions.ts +60 -0
- package/src/core/self-name-claim.ts +67 -0
- package/src/core/send-outcome.ts +69 -0
- package/src/core/shutdown-signals.ts +57 -0
- package/src/core/stale-agent-checker.ts +108 -0
- package/src/core/store.ts +704 -0
- package/src/core/token-text.ts +40 -0
- package/src/core/tool.ts +853 -0
- package/src/core/transport-events.ts +73 -0
- package/src/core/transport-queries.ts +54 -0
- package/src/core/transport.ts +367 -0
- package/src/core/types.ts +550 -0
- package/src/core/user-identity.ts +290 -0
- package/src/core/vapid.ts +81 -0
- package/src/core/version-check.ts +147 -0
- package/src/core/version-report-actions.ts +79 -0
- package/src/core/web-beacon.ts +31 -0
- package/src/core/web-push.ts +322 -0
- package/src/core/wire-mesh-core-version.ts +25 -0
- package/src/core/wire-mesh-identity.ts +36 -0
- package/src/core/wire-mesh-transport-options.ts +82 -0
- package/src/core/wire-mesh-transport.ts +1007 -0
- package/src/core/wire-protocol.ts +153 -0
- package/src/core/ws-dial.ts +191 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cc-peer bridge — relays between one local Claude Code peer (reached via cc-peer's own native cross-session protocol) and this side's agent-comms mesh, giving that local session cross-machine reach through the mesh's own transport (agent-comms#87).
|
|
3
|
+
*
|
|
4
|
+
* One bridge process relays for exactly one target local Claude Code peer, matching the "one bridge process is one agent is one device" model every other bridge in this codebase already follows -- not a many-to-one fan-out. Inbound cc-peer messages from that target are posted into this agent's own project room (the same auto-created room ensureRegistered/ensureProjectRoom already gives every bridge); mesh deliveries for this agent are relayed back to that same target via CcPeer.send().
|
|
5
|
+
*
|
|
6
|
+
* The wiring itself (wireCcPeerBridge) is dependency-injected and deliberately separate from the real CcPeer/MeshStore construction in run.ts, so it can be tested against a fake CcPeer without a real local Claude Code session.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { CommsTool } from "../../core/tool.js";
|
|
10
|
+
import { buildAction } from "../../core/bridge.js";
|
|
11
|
+
import {
|
|
12
|
+
answerJoinRequest,
|
|
13
|
+
formatCcPeerDelivery,
|
|
14
|
+
parseApprovalCommand,
|
|
15
|
+
} from "./approval-commands.js";
|
|
16
|
+
import type { DeliveryEvent } from "../../core/types.js";
|
|
17
|
+
|
|
18
|
+
/** How cc-peer addresses a target peer -- mirrors cc-peer's own PeerRef type without importing it, so this file has no direct dependency on the cc-peer package (only run.ts, which does the real construction, needs that). */
|
|
19
|
+
export type CcPeerRef =
|
|
20
|
+
{ pid: number } | { name: string } | { address: string };
|
|
21
|
+
|
|
22
|
+
/** The one inbound-message shape this bridge reads from CcPeer's own "message" event -- narrowed to the fields it actually uses. */
|
|
23
|
+
export interface CcPeerInboundMessage {
|
|
24
|
+
from?: string;
|
|
25
|
+
fromName?: string;
|
|
26
|
+
body: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The narrow slice of CcPeer's own real API this bridge needs -- satisfied by the real class in run.ts, and by a fake in tests. */
|
|
30
|
+
export interface CcPeerLike {
|
|
31
|
+
on: (
|
|
32
|
+
event: "message",
|
|
33
|
+
listener: (m: Readonly<CcPeerInboundMessage>) => void,
|
|
34
|
+
) => void;
|
|
35
|
+
send: (
|
|
36
|
+
target: Readonly<CcPeerRef>,
|
|
37
|
+
body: string,
|
|
38
|
+
) => Promise<{ msgId: string }>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The narrow slice of MeshStore this bridge needs -- onDelivery is MeshStore-only (not part of the generic CommsStore interface every bridge otherwise depends on), since only a mesh-backed store can push. */
|
|
42
|
+
export interface CcPeerBridgeStore {
|
|
43
|
+
onDelivery:
|
|
44
|
+
| ((agentId: string, event: DeliveryEvent) => void | Promise<void>)
|
|
45
|
+
| undefined;
|
|
46
|
+
/** Where a failure relaying an inbound message is reported, since the relay is fire-and-forget from the cc-peer event's own point of view. */
|
|
47
|
+
onError: ((error: Error) => void) | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The slice of a cc-peer roster entry needed to tell whether it is a given target. */
|
|
51
|
+
export interface CcPeerTargetEntry {
|
|
52
|
+
pid: number;
|
|
53
|
+
name?: string | undefined;
|
|
54
|
+
messagingSocketPath: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Whether a roster entry is the session a target ref addresses. An address target names the session's own `uds:<socket path>` address. */
|
|
58
|
+
export function targetMatchesEntry(
|
|
59
|
+
target: Readonly<CcPeerRef>,
|
|
60
|
+
entry: Readonly<CcPeerTargetEntry>,
|
|
61
|
+
): boolean {
|
|
62
|
+
if ("pid" in target) return entry.pid === target.pid;
|
|
63
|
+
if ("name" in target) return entry.name === target.name;
|
|
64
|
+
return target.address === `uds:${entry.messagingSocketPath}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Builds the predicate that says whether an inbound message was sent by the target session. The sender is identified by its `uds:<socket path>` address, resolved against a fresh roster read for every message so a target addressed by name is still recognised after that session restarts under a new pid. */
|
|
68
|
+
export function createTargetSenderMatcher(
|
|
69
|
+
target: Readonly<CcPeerRef>,
|
|
70
|
+
listRoster: () => Promise<readonly CcPeerTargetEntry[]>,
|
|
71
|
+
): (message: Readonly<CcPeerInboundMessage>) => Promise<boolean> {
|
|
72
|
+
return async (message) => {
|
|
73
|
+
if (message.from === undefined) return false;
|
|
74
|
+
if ("address" in target) return message.from === target.address;
|
|
75
|
+
const roster = await listRoster();
|
|
76
|
+
const sender = roster.find(
|
|
77
|
+
(entry) => `uds:${entry.messagingSocketPath}` === message.from,
|
|
78
|
+
);
|
|
79
|
+
return sender !== undefined && targetMatchesEntry(target, sender);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface CcPeerBridgeDeps {
|
|
84
|
+
store: CcPeerBridgeStore;
|
|
85
|
+
tool: Pick<CommsTool, "handle">;
|
|
86
|
+
peer: CcPeerLike;
|
|
87
|
+
agentId: string;
|
|
88
|
+
roomId: string;
|
|
89
|
+
target: Readonly<CcPeerRef>;
|
|
90
|
+
cwd: string;
|
|
91
|
+
/** The name this bridge's peer is registered under, which the target is told to message to answer a join request. */
|
|
92
|
+
peerName: string;
|
|
93
|
+
/** Whether an inbound message was sent by the target session. Only those are posted into the project room: the peer may be shared with the default front, whose fronted sessions message it as well, and those belong to the front's own relay. */
|
|
94
|
+
isFromTarget: (message: Readonly<CcPeerInboundMessage>) => Promise<boolean>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Wires the two directions of the relay. Never awaited by the caller -- both directions are genuinely fire-and-forget from this function's own point of view (a send failure surfaces through cc-peer's own receipt events / agent-comms' own delivery-status events, not a thrown error here). */
|
|
98
|
+
export function wireCcPeerBridge(deps: Readonly<CcPeerBridgeDeps>): void {
|
|
99
|
+
deps.peer.on("message", (m) => {
|
|
100
|
+
void (async () => {
|
|
101
|
+
if (!(await deps.isFromTarget(m))) return;
|
|
102
|
+
const ctx = {
|
|
103
|
+
agentId: deps.agentId,
|
|
104
|
+
harness: "cc-peer",
|
|
105
|
+
cwd: deps.cwd,
|
|
106
|
+
pid: process.pid,
|
|
107
|
+
};
|
|
108
|
+
// A well-formed accept or reject is the target's decision on a waiting join request, not something to post into the project room.
|
|
109
|
+
const decision = parseApprovalCommand(m.body);
|
|
110
|
+
if (decision !== undefined) {
|
|
111
|
+
const text = await answerJoinRequest(
|
|
112
|
+
{ tool: deps.tool, ctx },
|
|
113
|
+
decision,
|
|
114
|
+
);
|
|
115
|
+
await deps.peer.send(deps.target, text);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const sender = m.fromName ?? m.from ?? "unknown";
|
|
119
|
+
const action = buildAction({
|
|
120
|
+
action: "send",
|
|
121
|
+
room: deps.roomId,
|
|
122
|
+
content: `${sender}: ${m.body}`,
|
|
123
|
+
});
|
|
124
|
+
await deps.tool.handle(ctx, action);
|
|
125
|
+
})().catch((error: unknown) => {
|
|
126
|
+
deps.store.onError?.(
|
|
127
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
deps.store.onDelivery = (_targetId, event) => {
|
|
133
|
+
void deps.peer.send(
|
|
134
|
+
deps.target,
|
|
135
|
+
formatCcPeerDelivery(event, deps.peerName),
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wires the default cc-peer front (agent-comms#157) onto a real bridge's own store, right after construction -- the piece that actually makes "a machine's coordinator bridge fronts local Claude Code sessions by default" true, rather than a capability every bridge builds but nothing ever switches on. Every real bridge entry point (pi, claude-code, mcp, codex, opencode, user, cc-peer's own one-shot command) calls this once, immediately after createBridgeMesh/createBridgeMeshSync.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately the only file outside bridges/cc-peer/ that ever needs to know cc-peer exists: core/mesh-store.ts's onCoordinatorRoleChanged hook is generic (a bare boolean callback), and core/bridge-mesh.ts's factories return a plain MeshStore with no cc-peer awareness at all -- keeping this Node/filesystem/cc-peer-specific capability out of the transport-agnostic core, per this repo's own portable-runtime-boundary convention. A bridge that never becomes this machine's coordinator never starts a front at all; one that does gets it started and stopped automatically as that role comes and goes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { MeshStore } from "../../core/mesh-store.js";
|
|
8
|
+
import {
|
|
9
|
+
createDefaultCcPeerFront,
|
|
10
|
+
type CreateDefaultCcPeerFrontOptions,
|
|
11
|
+
} from "./front-runtime.js";
|
|
12
|
+
import type { CcPeerFront } from "./front-controller.js";
|
|
13
|
+
import type { FrontedSessionRecord } from "./front-controller.js";
|
|
14
|
+
|
|
15
|
+
export interface WireDefaultCcPeerFrontOptions {
|
|
16
|
+
coordinatorPort?: number | undefined;
|
|
17
|
+
hubUrl?: string | undefined;
|
|
18
|
+
/** The cc-peer peer this process already owns, lent to the front instead of it creating a second one -- see CreateDefaultCcPeerFrontOptions.peer. */
|
|
19
|
+
peer?: CreateDefaultCcPeerFrontOptions["peer"];
|
|
20
|
+
/** The name the lent peer is registered under -- see CreateDefaultCcPeerFrontOptions.peerName. */
|
|
21
|
+
peerName?: CreateDefaultCcPeerFrontOptions["peerName"];
|
|
22
|
+
/** Sessions this process already relays by other means -- see CreateDefaultCcPeerFrontOptions.excludeSession. */
|
|
23
|
+
excludeSession?: CreateDefaultCcPeerFrontOptions["excludeSession"];
|
|
24
|
+
/** Builds the front itself -- defaults to the real createDefaultCcPeerFront. Overridable so this function's own start/stop wiring against store.onCoordinatorRoleChanged is testable without a real CcPeer/local Claude Code session. */
|
|
25
|
+
createFront?:
|
|
26
|
+
| ((
|
|
27
|
+
options: Readonly<CreateDefaultCcPeerFrontOptions>,
|
|
28
|
+
) => Pick<CcPeerFront<FrontedSessionRecord>, "start" | "stop">)
|
|
29
|
+
| undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Builds the store.onCoordinatorRoleChanged callback for a real bridge's own store. Returns the callback rather than assigning it directly, so the caller does the actual store.onCoordinatorRoleChanged = ... assignment itself -- this keeps the store parameter here read-only, matching every other pure-construction function in this file's neighbourhood, instead of this function reaching into the store to mutate it.
|
|
34
|
+
*/
|
|
35
|
+
export function wireDefaultCcPeerFront(
|
|
36
|
+
store: Readonly<Pick<MeshStore, "onError">>,
|
|
37
|
+
options: Readonly<WireDefaultCcPeerFrontOptions> = {},
|
|
38
|
+
): (isCoordinator: boolean) => Promise<void> {
|
|
39
|
+
const createFront = options.createFront ?? createDefaultCcPeerFront;
|
|
40
|
+
const front = createFront({
|
|
41
|
+
coordinatorPort: options.coordinatorPort,
|
|
42
|
+
hubUrl: options.hubUrl,
|
|
43
|
+
peer: options.peer,
|
|
44
|
+
peerName: options.peerName,
|
|
45
|
+
excludeSession: options.excludeSession,
|
|
46
|
+
onError: (error) => {
|
|
47
|
+
store.onError?.(error);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return async (isCoordinator: boolean) => {
|
|
52
|
+
if (isCoordinator) {
|
|
53
|
+
front.start();
|
|
54
|
+
} else {
|
|
55
|
+
await front.stop();
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CcPeerFront — coordinator-only periodic controller for the default cc-peer front (agent-comms#157), attaching and detaching fronted sessions on each poll tick. Mirrors StaleAgentChecker's own "coordinator-only periodic probe" shape (owns its own interval timer exclusively, start/stop, no immediate first tick) since both are the same kind of thing: a background job that only ever runs on this machine's current mesh coordinator.
|
|
3
|
+
*
|
|
4
|
+
* Kept free of any real cc-peer/MeshStore construction, exactly like front.ts's pure decision logic this class is built on -- every real I/O (listRoster, probeSlotOwner, attach, detach) is injected, so the attach/detach/yield diffing across ticks is testable with fake timers and no real local Claude Code session. front-runtime.ts supplies the real dependencies.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
selectSessionsToFront,
|
|
9
|
+
matchInboundMessageSession,
|
|
10
|
+
type CcPeerRosterEntryLike,
|
|
11
|
+
} from "./front.js";
|
|
12
|
+
import type { IdentitySlot } from "../../core/identity-store.js";
|
|
13
|
+
import type { ReplyContext } from "./reply-aliases.js";
|
|
14
|
+
|
|
15
|
+
/** How often the front re-enumerates the local cc-peer roster and re-probes every currently-fronted session's own slot. */
|
|
16
|
+
const DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
17
|
+
|
|
18
|
+
/** The minimum shape a fronted-session record must carry so CcPeerFront can track and route to it, regardless of whatever real MeshStore/CommsTool state a concrete implementation (front-runtime.ts) attaches alongside these fields. */
|
|
19
|
+
export interface FrontedSessionRecord {
|
|
20
|
+
readonly pid: number;
|
|
21
|
+
readonly cwd: string;
|
|
22
|
+
readonly messagingSocketPath: string;
|
|
23
|
+
/** Delivers an inbound cc-peer message addressed to this fronted session into the mesh (the session-to-mesh direction) -- wired by attach() itself, exactly as wireCcPeerBridge's own peer.on("message") handler does for the one-shot bridge command. */
|
|
24
|
+
readonly handleInbound: (
|
|
25
|
+
message: Readonly<{ from?: string; fromName?: string; body: string }>,
|
|
26
|
+
) => void;
|
|
27
|
+
/** Routes a reply arriving on one of this session's own reply aliases back into the mesh, sent as this session's own device-id (agent-comms#158) — the session-to-mesh direction for lazy per-correspondent aliases. A dm context sends a mesh DM addressed to the correspondent that alias stands for; a room context instead posts back into the room the aliased message was itself sent in, since a reply to a room message must return to that room rather than becoming a private DM to its sender (agent-comms#289). Only called once handleAliasMessage has already resolved the alias to a known correspondent and context; a stale alias goes to notifyStaleAlias instead. */
|
|
28
|
+
readonly handleAliasReply: (
|
|
29
|
+
correspondentId: string,
|
|
30
|
+
context: Readonly<ReplyContext>,
|
|
31
|
+
message: Readonly<{ from?: string; fromName?: string; body: string }>,
|
|
32
|
+
) => void;
|
|
33
|
+
/** Delivers a clear error back into this session when a reply arrives on an alias the directory no longer maps to any correspondent -- e.g. after a front restart, since reply aliases are ephemeral by design -- rather than silently dropping the reply. */
|
|
34
|
+
readonly notifyStaleAlias: (aliasName: string) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The narrow slice of ReplyAliasDirectory (reply-aliases.ts) CcPeerFront needs to resolve an inbound alias message's own correspondent -- narrowed rather than importing the class directly so front-controller.ts stays free of any construction concern, matching probeSlotOwner's own injected-function convention. */
|
|
38
|
+
export interface CcPeerFrontAliasDirectory {
|
|
39
|
+
correspondentFor: (aliasName: string) => string | undefined;
|
|
40
|
+
/** The reply context (dm vs. a named room) a previously-minted alias name currently carries — reply-aliases.ts's ReplyAliasDirectory.contextFor in production. Always defined whenever correspondentFor is, since both are set together by the directory's own ensure(); treated as a stale alias when either is missing. */
|
|
41
|
+
contextFor: (aliasName: string) => ReplyContext | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CcPeerFrontDeps<TRecord extends FrontedSessionRecord> {
|
|
45
|
+
/** Enumerates the current local cc-peer roster. Rejects propagate to onError rather than throwing out of the poll timer. */
|
|
46
|
+
listRoster: () => Promise<readonly CcPeerRosterEntryLike[]>;
|
|
47
|
+
/** Read-only probe of a slot's current lock holder -- identity-store.ts's probeSlotOwner in production. */
|
|
48
|
+
probeSlotOwner: (slot: Readonly<IdentitySlot>) => number | undefined;
|
|
49
|
+
/** Builds a fronted-session record for a newly-selected roster entry (mints/loads its identity, wires the mesh store and the inbound relay). Rejects propagate to onError; the entry is retried on the next tick. */
|
|
50
|
+
attach: (entry: Readonly<CcPeerRosterEntryLike>) => Promise<TRecord>;
|
|
51
|
+
/** Tears a fronted-session record down (marks its agent offline, shuts its mesh store down). Rejects propagate to onError. */
|
|
52
|
+
detach: (record: TRecord) => Promise<void>;
|
|
53
|
+
/** Resolves an inbound alias message's own alias name back to the correspondent it stands for -- reply-aliases.ts's ReplyAliasDirectory in production. */
|
|
54
|
+
aliasDirectory: CcPeerFrontAliasDirectory;
|
|
55
|
+
/** Sessions this process already relays by other means and must not also front -- the one-shot `bridge cc-peer` command's own target, which shares this process's single cc-peer peer with the front. A session that becomes excluded while fronted is detached on the next tick, like one that yields to a live bridge. */
|
|
56
|
+
excludeSession?:
|
|
57
|
+
((entry: Readonly<CcPeerRosterEntryLike>) => boolean) | undefined;
|
|
58
|
+
pollIntervalMs?: number | undefined;
|
|
59
|
+
onError?: ((error: Error) => void) | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Normalises a caught value for the front's own error channel, which reports Errors. Exported so front-relay.ts reports a failed relay onto that same channel in the same shape, rather than keeping its own copy of this. */
|
|
63
|
+
export function toError(value: unknown): Error {
|
|
64
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class CcPeerFront<TRecord extends FrontedSessionRecord> {
|
|
68
|
+
private readonly fronted = new Map<number, TRecord>();
|
|
69
|
+
private timer: ReturnType<typeof setInterval> | undefined;
|
|
70
|
+
|
|
71
|
+
constructor(private readonly deps: Readonly<CcPeerFrontDeps<TRecord>>) {}
|
|
72
|
+
|
|
73
|
+
/** Starts the periodic poll (coordinator-only). No-op if already running. Matches StaleAgentChecker's own convention of no immediate first tick -- the first attach happens on the first elapsed interval, not synchronously on start(). */
|
|
74
|
+
start(): void {
|
|
75
|
+
if (this.timer) return;
|
|
76
|
+
this.timer = setInterval(() => {
|
|
77
|
+
void this.tick();
|
|
78
|
+
}, this.deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Stops the periodic poll and detaches every currently-fronted session -- this side is no longer the coordinator, so it has no business still relaying on any session's behalf. */
|
|
82
|
+
async stop(): Promise<void> {
|
|
83
|
+
if (this.timer) {
|
|
84
|
+
clearInterval(this.timer);
|
|
85
|
+
this.timer = undefined;
|
|
86
|
+
}
|
|
87
|
+
const records = [...this.fronted.values()];
|
|
88
|
+
this.fronted.clear();
|
|
89
|
+
for (const record of records) {
|
|
90
|
+
await this.detachOne(record);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Routes an inbound cc-peer message to whichever fronted session it came from, if any -- the shared front-wide CcPeer instance's own "message" listener calls this directly (front-runtime.ts), since only this class knows the current fronted set. A message matching no fronted session (already detached, or genuinely foreign) is silently dropped -- there's nowhere for it to go. */
|
|
95
|
+
handleInboundMessage(
|
|
96
|
+
message: Readonly<{ from?: string; fromName?: string; body: string }>,
|
|
97
|
+
): void {
|
|
98
|
+
const record = matchInboundMessageSession(this.fronted.values(), message);
|
|
99
|
+
record?.handleInbound(message);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Routes a reply arriving on a shared reply alias to whichever fronted session actually sent it — matched the same way as handleInboundMessage, by the envelope's own socket-path convention. A message matching no fronted session is silently dropped, same as handleInboundMessage: there is nowhere for it to go, and we cannot safely address an error back to a session we do not control. Once the sending session is identified, the alias directory resolves which correspondent this reply is for and the reply context (dm vs. room) it should return to; an alias the directory no longer knows about, or whose context is missing, is reported back into the session as a stale-alias error rather than silently dropped, per agent-comms#158. */
|
|
103
|
+
handleAliasMessage(
|
|
104
|
+
message: Readonly<{ alias: string; from?: string; body: string }>,
|
|
105
|
+
): void {
|
|
106
|
+
const record = matchInboundMessageSession(this.fronted.values(), message);
|
|
107
|
+
if (!record) return;
|
|
108
|
+
const correspondentId = this.deps.aliasDirectory.correspondentFor(
|
|
109
|
+
message.alias,
|
|
110
|
+
);
|
|
111
|
+
const context = this.deps.aliasDirectory.contextFor(message.alias);
|
|
112
|
+
if (correspondentId === undefined || context === undefined) {
|
|
113
|
+
record.notifyStaleAlias(message.alias);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
record.handleAliasReply(correspondentId, context, message);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private async tick(): Promise<void> {
|
|
120
|
+
let roster: readonly CcPeerRosterEntryLike[];
|
|
121
|
+
try {
|
|
122
|
+
roster = await this.deps.listRoster();
|
|
123
|
+
} catch (err) {
|
|
124
|
+
this.deps.onError?.(toError(err));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const selected = selectSessionsToFront(
|
|
129
|
+
roster,
|
|
130
|
+
this.deps.probeSlotOwner,
|
|
131
|
+
).filter((entry) => this.deps.excludeSession?.(entry) !== true);
|
|
132
|
+
const selectedPids = new Set(selected.map((entry) => entry.pid));
|
|
133
|
+
const rosterPids = new Set(roster.map((entry) => entry.pid));
|
|
134
|
+
|
|
135
|
+
// Detach anything that either exited (gone from the roster entirely) or yielded (its own slot is now held by a live bridge, so it's no longer in the selected set even though the session process itself is still running).
|
|
136
|
+
for (const [pid, record] of [...this.fronted]) {
|
|
137
|
+
if (rosterPids.has(pid) && selectedPids.has(pid)) continue;
|
|
138
|
+
this.fronted.delete(pid);
|
|
139
|
+
await this.detachOne(record);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const entry of selected) {
|
|
143
|
+
if (this.fronted.has(entry.pid)) continue;
|
|
144
|
+
try {
|
|
145
|
+
const record = await this.deps.attach(entry);
|
|
146
|
+
this.fronted.set(entry.pid, record);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
this.deps.onError?.(toError(err));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async detachOne(record: TRecord): Promise<void> {
|
|
154
|
+
try {
|
|
155
|
+
await this.deps.detach(record);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
this.deps.onError?.(toError(err));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, DI-testable wiring for one fronted session's own cc-peer-to-mesh relay (agent-comms#157) -- the "attach"/"detach" half of the default front, building a FrontedSessionRecord (front-controller.ts) from an already-constructed mesh and the front's shared cc-peer peer. Mirrors bridges/cc-peer/bridge.ts's own wireCcPeerBridge split: kept free of any real CcPeer.create()/createBridgeMeshFromIdentity construction so it's testable against fakes, with front-runtime.ts supplying the real ones.
|
|
3
|
+
*
|
|
4
|
+
* Reuses CcPeerRef/CcPeerInboundMessage from bridge.ts rather than redeclaring them -- the wire shape a shared front-wide peer speaks is identical to the one-shot bridge command's own peer, just addressed by pid instead of a fixed target.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { buildAction } from "../../core/bridge.js";
|
|
8
|
+
import {
|
|
9
|
+
answerJoinRequest,
|
|
10
|
+
formatCcPeerDelivery,
|
|
11
|
+
parseApprovalCommand,
|
|
12
|
+
} from "./approval-commands.js";
|
|
13
|
+
import type { CommsTool } from "../../core/tool.js";
|
|
14
|
+
import type { CommsAction, DeliveryEvent } from "../../core/types.js";
|
|
15
|
+
import { toError, type FrontedSessionRecord } from "./front-controller.js";
|
|
16
|
+
import type { CcPeerRosterEntryLike } from "./front.js";
|
|
17
|
+
import type { CcPeerInboundMessage, CcPeerRef } from "./bridge.js";
|
|
18
|
+
import { replyTargetForEvent, type ReplyContext } from "./reply-aliases.js";
|
|
19
|
+
|
|
20
|
+
/** The narrow slice of MeshStore a fronted session's own record needs -- onDelivery to wire the mesh-to-session direction, setAgentOffline/shutdown for detachFrontedSession's own teardown. */
|
|
21
|
+
export interface FrontRelayStore {
|
|
22
|
+
onDelivery:
|
|
23
|
+
| ((agentId: string, event: DeliveryEvent) => void | Promise<void>)
|
|
24
|
+
| undefined;
|
|
25
|
+
setAgentOffline: (id: string) => Promise<void>;
|
|
26
|
+
shutdown: () => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The narrow slice of the front's shared CcPeer instance this relay needs -- just send(), since the front-wide "message" listener (front-runtime.ts) is what routes an inbound message to this record's own handleInbound in the first place, not this module. */
|
|
30
|
+
export interface FrontRelayPeer {
|
|
31
|
+
send: (
|
|
32
|
+
target: Readonly<CcPeerRef>,
|
|
33
|
+
body: string,
|
|
34
|
+
) => Promise<{ msgId: string }>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The narrow slice of AliasPool (cc-peer's own cc-peer/alias-pool subpath) this relay needs: sending from the real OS-backed reply alias a correspondent's name was already minted for by FrontRelayAliasDirectory, which materialises that alias on the way. Narrowed so this module has no compile-time dependency on the cc-peer package itself, leaving front-runtime.ts to supply the real AliasPool. */
|
|
38
|
+
export interface FrontRelayAliasPool {
|
|
39
|
+
send: (
|
|
40
|
+
name: string,
|
|
41
|
+
target: Readonly<CcPeerRef>,
|
|
42
|
+
body: string,
|
|
43
|
+
) => Promise<{ msgId: string }>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The narrow slice of ReplyAliasDirectory (reply-aliases.ts) this relay needs to mint/recall the alias name for one correspondent, and record its current reply context, on the mesh-to-session direction. */
|
|
47
|
+
export interface FrontRelayAliasDirectory {
|
|
48
|
+
ensure: (correspondentId: string, context: ReplyContext) => string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface FrontedRelayRecord extends FrontedSessionRecord {
|
|
52
|
+
agentId: string;
|
|
53
|
+
roomId: string;
|
|
54
|
+
store: FrontRelayStore;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface BuildFrontedSessionRecordDeps {
|
|
58
|
+
entry: Readonly<CcPeerRosterEntryLike>;
|
|
59
|
+
/** The name the shared peer is registered under, which a session is told to message to answer a join request. */
|
|
60
|
+
peerName: string;
|
|
61
|
+
agentId: string;
|
|
62
|
+
roomId: string;
|
|
63
|
+
store: FrontRelayStore;
|
|
64
|
+
tool: Pick<CommsTool, "handle">;
|
|
65
|
+
peer: FrontRelayPeer;
|
|
66
|
+
aliasPool: FrontRelayAliasPool;
|
|
67
|
+
aliasDirectory: FrontRelayAliasDirectory;
|
|
68
|
+
/** Where a relay failure this session cannot act on is reported: the front's own error channel (CcPeerFront's onError, ultimately the host bridge's store.onError), so a message that could not be delivered repliably is visible to the operator and not only to the session. */
|
|
69
|
+
onError?: ((error: Error) => void) | undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Wires both relay directions for one fronted session and returns the record CcPeerFront tracks it under. Mesh-to-session: store.onDelivery sends the formatted event to this session's own pid. An event with a single originating correspondent (a dm or room_message, per replyTargetForEvent) is sent from that correspondent's own reply alias, so the sender the session sees, and therefore replies to, is the correspondent rather than the front (agent-comms#284); every other event, having no one repliable behind it, is sent from the front's shared peer. Session-to-mesh: the returned handleInbound (called by the front's shared "message" listener once it's matched this record by socket path) posts the message into this session's own project room, exactly as wireCcPeerBridge's own peer.on("message") handler does for the one-shot bridge command; handleAliasReply (called once CcPeerFront has resolved an inbound alias message to its correspondent and reply context) sends a mesh DM to that correspondent when the context is a dm, or posts back into the originating room when the context is a room — a reply to a room message no longer always becomes a private DM to its sender (agent-comms#289); notifyStaleAlias delivers a clear error back into the session for a reply on an alias the directory no longer recognises. All three mesh actions report a refusal back into the session rather than discarding it, so a reply the mesh would not carry is visible where it was written.
|
|
74
|
+
*/
|
|
75
|
+
export function buildFrontedSessionRecord(
|
|
76
|
+
deps: Readonly<BuildFrontedSessionRecordDeps>,
|
|
77
|
+
): FrontedRelayRecord {
|
|
78
|
+
const {
|
|
79
|
+
entry,
|
|
80
|
+
peerName,
|
|
81
|
+
agentId,
|
|
82
|
+
roomId,
|
|
83
|
+
store,
|
|
84
|
+
tool,
|
|
85
|
+
peer,
|
|
86
|
+
aliasPool,
|
|
87
|
+
aliasDirectory,
|
|
88
|
+
onError,
|
|
89
|
+
} = deps;
|
|
90
|
+
|
|
91
|
+
const ctx = {
|
|
92
|
+
agentId,
|
|
93
|
+
harness: "claude-code",
|
|
94
|
+
cwd: entry.cwd,
|
|
95
|
+
pid: process.pid,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const sendToSession = async (body: string): Promise<{ msgId: string }> =>
|
|
99
|
+
peer.send({ pid: entry.pid }, body);
|
|
100
|
+
|
|
101
|
+
/** Runs a mesh action as this session's own agent and relays a refusal back into the session. A relayed message the mesh refused is otherwise invisible from the session's side, so a reply that never left looks exactly like one that was delivered. */
|
|
102
|
+
const runForSession = async (
|
|
103
|
+
action: Readonly<CommsAction>,
|
|
104
|
+
what: string,
|
|
105
|
+
): Promise<void> => {
|
|
106
|
+
const outcome = await tool.handle(ctx, action);
|
|
107
|
+
if (!outcome.isError) return;
|
|
108
|
+
await sendToSession(`${what}: ${outcome.content}`);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
store.onDelivery = async (_targetId, event) => {
|
|
112
|
+
const body = formatCcPeerDelivery(event, peerName);
|
|
113
|
+
const target = replyTargetForEvent(event);
|
|
114
|
+
if (target === undefined) {
|
|
115
|
+
await sendToSession(body);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const aliasName = aliasDirectory.ensure(
|
|
119
|
+
target.correspondentId,
|
|
120
|
+
target.context,
|
|
121
|
+
);
|
|
122
|
+
try {
|
|
123
|
+
// Sent from the correspondent's own alias, not from the front's shared peer, so the session's native reply-to-sender lands on that alias and handleAliasReply routes it back to this correspondent's dm channel, or back into the room, per the alias's own recorded context. A hint naming the alias is deliberately not appended: the reply target is now the sender the session already replies to, and a model is free to ignore a hint (agent-comms#284).
|
|
124
|
+
await aliasPool.send(aliasName, { pid: entry.pid }, body);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// Re-sending from the front's shared peer with a plain body would put the session back in exactly the failure this alias delivery exists to remove: its reply would reach the front, be posted into the project room, and never arrive. Deliver the content, say plainly that a reply cannot get back, and report the failure on the front's own error channel.
|
|
127
|
+
onError?.(toError(error));
|
|
128
|
+
await sendToSession(
|
|
129
|
+
`${body}\n(Cannot reply: ${target.correspondentId}'s reply peer could not be started, so a reply to this message goes to this session's project room instead.)`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
pid: entry.pid,
|
|
136
|
+
cwd: entry.cwd,
|
|
137
|
+
messagingSocketPath: entry.messagingSocketPath,
|
|
138
|
+
agentId,
|
|
139
|
+
roomId,
|
|
140
|
+
store,
|
|
141
|
+
handleInbound: (message: Readonly<CcPeerInboundMessage>) => {
|
|
142
|
+
// A well-formed accept or reject is the session's decision on a waiting join request, not something to post into the project room.
|
|
143
|
+
const decision = parseApprovalCommand(message.body);
|
|
144
|
+
if (decision !== undefined) {
|
|
145
|
+
void answerJoinRequest({ tool, ctx }, decision).then(sendToSession);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const sender = message.fromName ?? message.from ?? "unknown";
|
|
149
|
+
const action = buildAction({
|
|
150
|
+
action: "send",
|
|
151
|
+
room: roomId,
|
|
152
|
+
content: `${sender}: ${message.body}`,
|
|
153
|
+
});
|
|
154
|
+
void runForSession(action, `Not posted to ${roomId}`);
|
|
155
|
+
},
|
|
156
|
+
handleAliasReply: (
|
|
157
|
+
correspondentId: string,
|
|
158
|
+
context: Readonly<ReplyContext>,
|
|
159
|
+
message: Readonly<{ body: string }>,
|
|
160
|
+
) => {
|
|
161
|
+
if (context.kind === "room") {
|
|
162
|
+
const action = buildAction({
|
|
163
|
+
action: "send",
|
|
164
|
+
room: context.room,
|
|
165
|
+
content: message.body,
|
|
166
|
+
});
|
|
167
|
+
void runForSession(action, `Reply to ${context.room} not delivered`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const action = buildAction({
|
|
171
|
+
action: "dm",
|
|
172
|
+
target: correspondentId,
|
|
173
|
+
content: message.body,
|
|
174
|
+
});
|
|
175
|
+
void runForSession(action, `Reply to ${correspondentId} not delivered`);
|
|
176
|
+
},
|
|
177
|
+
notifyStaleAlias: (aliasName: string) => {
|
|
178
|
+
void sendToSession(
|
|
179
|
+
`Reply not delivered: peer "${aliasName}" is no longer a known correspondent (reply aliases don't survive a front restart). Wait for a new message from them and reply to that instead.`,
|
|
180
|
+
);
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Tears a fronted session's record down: marks its agent offline (a courtesy to peers watching its presence), then shuts its mesh store down. There is no identity lock to release -- attach() never took one (loadIdentityForFront's whole point), so a real bridge for this slot can already have claimed it by the time this runs. */
|
|
186
|
+
export async function detachFrontedSession(
|
|
187
|
+
record: Readonly<Pick<FrontedRelayRecord, "agentId" | "store">>,
|
|
188
|
+
): Promise<void> {
|
|
189
|
+
await record.store.setAgentOffline(record.agentId);
|
|
190
|
+
await record.store.shutdown();
|
|
191
|
+
}
|