agent-comms 8.15.9 → 8.15.10
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/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/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,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
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real cc-peer/MeshStore construction for the default coordinator-run front (agent-comms#157) -- the counterpart to bridges/cc-peer/run.ts's own real CcPeer.create()/createBridgeMesh construction for the one-shot `bridge cc-peer` command. Wires front-controller.ts's CcPeerFront (attach/detach diffing across ticks) to front-relay.ts's buildFrontedSessionRecord/detachFrontedSession (one session's own relay) against one shared, front-wide CcPeer instance and a fresh per-session MeshStore built from loadIdentityForFront's lock-free identity.
|
|
3
|
+
*
|
|
4
|
+
* Untested directly, exactly like run.ts's own real construction (see cc-peer-bridge.test.ts's header comment): front.ts, front-controller.ts, and front-relay.ts already carry the front's entire decision/diffing/relay logic under direct DI-based unit tests, so this file's only remaining job -- calling the real cc-peer/core APIs in the right order -- is exercised by actually running a bridge as this machine's coordinator.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { parseDisplayName } from "../../core/display-name.js";
|
|
8
|
+
import { CcPeer, CC_PEER_VERSION } from "cc-peer";
|
|
9
|
+
import { AliasPool } from "cc-peer/alias-pool";
|
|
10
|
+
import type { AliasMessage } from "cc-peer/alias-pool";
|
|
11
|
+
import {
|
|
12
|
+
createBridgeMeshFromIdentity,
|
|
13
|
+
ensureRegistered,
|
|
14
|
+
ensureProjectRoom,
|
|
15
|
+
} from "../../core/index.js";
|
|
16
|
+
import {
|
|
17
|
+
loadIdentityForFront,
|
|
18
|
+
probeSlotOwner,
|
|
19
|
+
} from "../../core/identity-store.js";
|
|
20
|
+
import { CcPeerFront } from "./front-controller.js";
|
|
21
|
+
import { computeFrontSlot } from "./front.js";
|
|
22
|
+
import type { CcPeerRosterEntryLike } from "./front.js";
|
|
23
|
+
import type { CcPeerInboundMessage } from "./bridge.js";
|
|
24
|
+
import {
|
|
25
|
+
buildFrontedSessionRecord,
|
|
26
|
+
detachFrontedSession,
|
|
27
|
+
type FrontedRelayRecord,
|
|
28
|
+
type FrontRelayPeer,
|
|
29
|
+
} from "./front-relay.js";
|
|
30
|
+
import { ReplyAliasDirectory } from "./reply-aliases.js";
|
|
31
|
+
import { SharedPeer } from "./shared-peer.js";
|
|
32
|
+
|
|
33
|
+
/** cc-peer's own registered display name for the front's shared peer -- distinct from any individual fronted session's own agent-comms display name (front-relay.ts's ensureRegistered call uses the session's own cc-peer name/pid for that). */
|
|
34
|
+
const FRONT_PEER_NAME = "agent-comms-front";
|
|
35
|
+
|
|
36
|
+
/** The slice of a cc-peer peer the front uses: reading the local roster, receiving messages, and sending to a fronted session. The real CcPeer satisfies it. */
|
|
37
|
+
export interface FrontCcPeer extends FrontRelayPeer {
|
|
38
|
+
roster: () => Promise<readonly CcPeerRosterEntryLike[]>;
|
|
39
|
+
on: (
|
|
40
|
+
event: "message",
|
|
41
|
+
listener: (message: Readonly<CcPeerInboundMessage>) => void,
|
|
42
|
+
) => unknown;
|
|
43
|
+
stop: () => Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CreateDefaultCcPeerFrontOptions {
|
|
47
|
+
coordinatorPort?: number | undefined;
|
|
48
|
+
hubUrl?: string | undefined;
|
|
49
|
+
pollIntervalMs?: number | undefined;
|
|
50
|
+
onError?: ((error: Error) => void) | undefined;
|
|
51
|
+
/** The cc-peer peer this process already owns, when it has one. cc-peer allows one peer per process, so a process that runs its own (the one-shot `bridge cc-peer` command) must lend it here rather than let the front try to create a second. The front only borrows it: it never stops it, and registers its inbound listener on it once. Without this the front creates and owns its own peer. */
|
|
52
|
+
peer?: FrontCcPeer | undefined;
|
|
53
|
+
/** The name the shared peer is registered under, which fronted sessions are told to message to answer a join request. Defaults to the front's own peer name; a host process that lends its own peer names it here. */
|
|
54
|
+
peerName?: string | undefined;
|
|
55
|
+
/** Sessions this process already relays by other means, which the front must therefore leave alone. */
|
|
56
|
+
excludeSession?:
|
|
57
|
+
((entry: Readonly<CcPeerRosterEntryLike>) => boolean) | undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The session's own cc-peer display name when it is usable as an agent name (parseDisplayName, which registration enforces), otherwise "claude-code-<pid>": ensureRegistered requires a defaultName, and a session that never picked a name, or picked one registration would refuse, is still worth fronting under something stable and identifiable. */
|
|
61
|
+
function defaultSessionName(entry: Readonly<CcPeerRosterEntryLike>): string {
|
|
62
|
+
const name =
|
|
63
|
+
entry.name === undefined ? undefined : parseDisplayName(entry.name);
|
|
64
|
+
return name ?? `claude-code-${String(entry.pid)}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Builds the default cc-peer front. start()/stop() are safe to call from bridge-mesh.ts's onCoordinatorRoleChanged hook regardless of whether cc-peer itself is usable on this machine: the shared CcPeer instance is created lazily, on the first poll tick that actually needs it (listRoster), not at start() itself, and any construction or attach failure is reported via onError rather than thrown -- "absent sockets" (no local Claude Code sessions at all) is then a clean no-op front rather than a crash of the coordinator that owns it.
|
|
69
|
+
*/
|
|
70
|
+
export function createDefaultCcPeerFront(
|
|
71
|
+
options: Readonly<CreateDefaultCcPeerFrontOptions> = {},
|
|
72
|
+
): Pick<CcPeerFront<FrontedRelayRecord>, "start" | "stop"> {
|
|
73
|
+
const borrowedPeer = options.peer;
|
|
74
|
+
const peerName = options.peerName ?? FRONT_PEER_NAME;
|
|
75
|
+
let aliasPool: AliasPool | undefined;
|
|
76
|
+
const aliasDirectory = new ReplyAliasDirectory();
|
|
77
|
+
|
|
78
|
+
const front = new CcPeerFront<FrontedRelayRecord>({
|
|
79
|
+
listRoster: async () => {
|
|
80
|
+
const peer = await ensureSharedPeer();
|
|
81
|
+
return peer.roster();
|
|
82
|
+
},
|
|
83
|
+
probeSlotOwner,
|
|
84
|
+
attach: async (entry) => attachSession(entry, await ensureSharedPeer()),
|
|
85
|
+
detach: detachFrontedSession,
|
|
86
|
+
aliasDirectory,
|
|
87
|
+
excludeSession: options.excludeSession,
|
|
88
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
89
|
+
onError: options.onError,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const sharedPeer = new SharedPeer<FrontCcPeer>(async () => {
|
|
93
|
+
if (borrowedPeer !== undefined) return borrowedPeer;
|
|
94
|
+
const peer = await CcPeer.create({ name: FRONT_PEER_NAME });
|
|
95
|
+
peer.on("message", (message: Readonly<CcPeerInboundMessage>) => {
|
|
96
|
+
front.handleInboundMessage(message);
|
|
97
|
+
});
|
|
98
|
+
return peer;
|
|
99
|
+
});
|
|
100
|
+
// A borrowed peer outlives the front's own start/stop cycles, so its listener is attached once here rather than on every (re)creation, which would relay each inbound message once per restart.
|
|
101
|
+
borrowedPeer?.on("message", (message: Readonly<CcPeerInboundMessage>) => {
|
|
102
|
+
front.handleInboundMessage(message);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
start: () => {
|
|
107
|
+
front.start();
|
|
108
|
+
},
|
|
109
|
+
stop: async () => {
|
|
110
|
+
await front.stop();
|
|
111
|
+
const peer = await sharedPeer.release();
|
|
112
|
+
if (borrowedPeer === undefined) await peer?.stop();
|
|
113
|
+
await aliasPool?.stopAll();
|
|
114
|
+
aliasPool = undefined;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** Materialises the front's own shared AliasPool on first use (mirroring ensureSharedPeer's own lazy-construction convention) and wires its "message" event, every reply arriving on any fronted session's own correspondent aliases, straight into the controller's handleAliasMessage, which resolves the sending session and the alias's own correspondent before routing it on. The pool is also what delivers a correspondent's own messages into a fronted session, from that correspondent's alias, so the session's reply comes back on this same "message" event. */
|
|
119
|
+
function ensureAliasPool(): AliasPool {
|
|
120
|
+
if (aliasPool) return aliasPool;
|
|
121
|
+
const pool = AliasPool.create();
|
|
122
|
+
pool.on("message", (message: Readonly<AliasMessage>) => {
|
|
123
|
+
front.handleAliasMessage(message);
|
|
124
|
+
});
|
|
125
|
+
aliasPool = pool;
|
|
126
|
+
return pool;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function ensureSharedPeer(): Promise<FrontCcPeer> {
|
|
130
|
+
return sharedPeer.get();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function attachSession(
|
|
134
|
+
entry: Readonly<CcPeerRosterEntryLike>,
|
|
135
|
+
peer: Readonly<FrontCcPeer>,
|
|
136
|
+
): Promise<FrontedRelayRecord> {
|
|
137
|
+
const slot = computeFrontSlot(entry.cwd);
|
|
138
|
+
const identity = loadIdentityForFront(slot);
|
|
139
|
+
const { store, tool } = await createBridgeMeshFromIdentity(identity, slot, {
|
|
140
|
+
coordinatorPort: options.coordinatorPort,
|
|
141
|
+
hubUrl: options.hubUrl,
|
|
142
|
+
});
|
|
143
|
+
store.getCcPeerVersion = () => CC_PEER_VERSION;
|
|
144
|
+
|
|
145
|
+
const reg = await ensureRegistered({
|
|
146
|
+
store,
|
|
147
|
+
harness: "claude-code",
|
|
148
|
+
cwd: entry.cwd,
|
|
149
|
+
defaultName: defaultSessionName(entry),
|
|
150
|
+
visibility: "visible",
|
|
151
|
+
});
|
|
152
|
+
const roomId = await ensureProjectRoom(store, reg.agentId, entry.cwd);
|
|
153
|
+
|
|
154
|
+
return buildFrontedSessionRecord({
|
|
155
|
+
entry,
|
|
156
|
+
peerName,
|
|
157
|
+
agentId: reg.agentId,
|
|
158
|
+
roomId,
|
|
159
|
+
store,
|
|
160
|
+
tool,
|
|
161
|
+
peer,
|
|
162
|
+
aliasPool: ensureAliasPool(),
|
|
163
|
+
aliasDirectory,
|
|
164
|
+
onError: options.onError,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure decision logic for the default cc-peer front (agent-comms#157): a machine's coordinator bridge relays every local Claude Code session that doesn't already front itself, using the same (harness, cwd) identity slot that session's own agent-comms bridge would use, so addressing carries over unchanged the moment that session's own bridge appears.
|
|
3
|
+
*
|
|
4
|
+
* Kept free of any real cc-peer/MeshStore construction so it's testable without a real local Claude Code session, a real filesystem, or real sockets -- see front-runtime.ts for the stateful controller and the real wiring built on top of these functions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { IdentitySlot } from "../../core/identity-store.js";
|
|
8
|
+
|
|
9
|
+
/** The slice of cc-peer's own RegistryEntry this module needs. Narrowed rather than importing cc-peer's type directly so this file has no compile-time dependency on the cc-peer package (only front-runtime.ts, which does the real construction, needs that). */
|
|
10
|
+
export interface CcPeerRosterEntryLike {
|
|
11
|
+
pid: number;
|
|
12
|
+
cwd: string;
|
|
13
|
+
name?: string | undefined;
|
|
14
|
+
/** cc-peer's own registry entries for library-backed peers (this front itself, the one-shot `bridge cc-peer` command, an alias-pool worker) always carry the literal string "cc-peer" here -- see buildRegistryEntry in cc-peer's own source. A real interactive Claude Code session, which registers itself natively rather than through the cc-peer library, reports its own Claude Code version instead. */
|
|
15
|
+
version: string;
|
|
16
|
+
messagingSocketPath: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** True for a roster entry that is itself a cc-peer-library-backed peer (this front's own shared identity, the one-shot bridge command, an alias-pool worker) rather than a real interactive Claude Code session -- see CcPeerRosterEntryLike.version's own doc comment for the mechanism. The front must never try to front one of these: it would compute a spurious claude-code identity slot for a process that isn't a Claude Code session at all, and relay cc-peer traffic into it that nothing there ever reads. */
|
|
20
|
+
export function isCcPeerLibraryPeer(
|
|
21
|
+
entry: Readonly<CcPeerRosterEntryLike>,
|
|
22
|
+
): boolean {
|
|
23
|
+
return entry.version === "cc-peer";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The identity slot a session's own agent-comms bridge would hold if it started right now -- the same (harness, cwd) pair every real claude-code bridge entry point (bridges/claude-code/channel.ts) already constructs. */
|
|
27
|
+
export function computeFrontSlot(cwd: string): IdentitySlot {
|
|
28
|
+
return { harness: "claude-code", cwd };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Filters a cc-peer roster down to the real Claude Code sessions this front should attach to right now: excludes other cc-peer-library peers, and excludes any session whose own slot is already held by a live PID (that session already fronts itself, via its own agent-comms bridge). probeSlotOwner is injected rather than imported directly so this stays a pure function over its inputs -- the real probe (identity-store.ts's probeSlotOwner) reads the filesystem, which has no place in a decision function tested purely over data.
|
|
33
|
+
*/
|
|
34
|
+
export function selectSessionsToFront(
|
|
35
|
+
roster: readonly CcPeerRosterEntryLike[],
|
|
36
|
+
probeSlotOwner: (slot: Readonly<IdentitySlot>) => number | undefined,
|
|
37
|
+
): CcPeerRosterEntryLike[] {
|
|
38
|
+
return roster.filter((entry) => {
|
|
39
|
+
if (isCcPeerLibraryPeer(entry)) return false;
|
|
40
|
+
const slot = computeFrontSlot(entry.cwd);
|
|
41
|
+
return probeSlotOwner(slot) === undefined;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Finds which currently-fronted session an inbound cc-peer message came from, by matching the envelope's own `from` field (always the literal string "uds:" followed by the sender's own listening socket path, per cc-peer's own send() -- see CcPeer.send in cc-peer's source) against each fronted session's messagingSocketPath. Generic over the fronted-session record type so front-runtime.ts's real records (which carry a live MeshStore/CommsTool alongside the roster entry) can be matched directly without this module needing to know their shape.
|
|
47
|
+
*/
|
|
48
|
+
export function matchInboundMessageSession<
|
|
49
|
+
T extends { readonly messagingSocketPath: string },
|
|
50
|
+
>(
|
|
51
|
+
fronted: Readonly<Iterable<T>>,
|
|
52
|
+
message: Readonly<{ from?: string }>,
|
|
53
|
+
): T | undefined {
|
|
54
|
+
if (message.from === undefined) return undefined;
|
|
55
|
+
for (const record of fronted) {
|
|
56
|
+
if (message.from === `uds:${record.messagingSocketPath}`) return record;
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|