antiphon 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BACKLOG.md +329 -0
- package/README.md +172 -22
- package/bin/antiphon.mjs +11 -0
- package/lib/antiphon.py +2042 -247
- package/lib/channel.mjs +323 -26
- package/lib/peers.py +689 -0
- package/package.json +20 -5
package/lib/channel.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { execFile } from "node:child_process";
|
|
5
5
|
import { chmod, mkdir, unlink } from "node:fs/promises";
|
|
6
|
-
import { createServer } from "node:net";
|
|
6
|
+
import { connect, createServer } from "node:net";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { promisify } from "node:util";
|
|
@@ -18,7 +18,36 @@ import {
|
|
|
18
18
|
const execFileAsync = promisify(execFile);
|
|
19
19
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
20
|
const projectDir = process.env.ANTIPHON_CWD || process.cwd();
|
|
21
|
-
const
|
|
21
|
+
const peerName = (process.env.ANTIPHON_NAME || "").trim().toLowerCase();
|
|
22
|
+
// Validated before anything binds: an invalid name would otherwise open a socket
|
|
23
|
+
// that registration then refuses, leaving a live channel nobody can find.
|
|
24
|
+
const NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,31}$/;
|
|
25
|
+
const nameIsUsable = !peerName || NAME_PATTERN.test(peerName);
|
|
26
|
+
|
|
27
|
+
// Null until this process has actually won the name and bound its socket. A
|
|
28
|
+
// valid ANTIPHON_NAME is a request, not a claim: two sessions can be started
|
|
29
|
+
// with the same one and only the first wins. A loser that published it anyway
|
|
30
|
+
// would have its words attributed to the winner, and a reply addressed back
|
|
31
|
+
// would reach a session that never spoke.
|
|
32
|
+
//
|
|
33
|
+
// Declared here, above the tool handler that closes over it and above
|
|
34
|
+
// `mcp.connect`. Left further down it would sit in the temporal dead zone
|
|
35
|
+
// while the transport was already accepting requests, and a `reply_to_codex`
|
|
36
|
+
// buffered at startup would raise a ReferenceError instead of being answered.
|
|
37
|
+
let senderAlias = null;
|
|
38
|
+
|
|
39
|
+
// Resolved once the startup chain has settled what this session is — winner,
|
|
40
|
+
// loser, unnamed or refused. The reply tool waits for it before signing a
|
|
41
|
+
// message. Answering earlier would be safe but wrong: a session that does hold
|
|
42
|
+
// `ui` would deny its own name for the first message of its life, purely
|
|
43
|
+
// because the MCP handshake finishes before the registry claim does.
|
|
44
|
+
let markIdentitySettled;
|
|
45
|
+
const identitySettled = new Promise((resolve) => { markIdentitySettled = resolve; });
|
|
46
|
+
// Hashed with the name, never appended: macOS caps a socket path near 104 bytes
|
|
47
|
+
// and TMPDIR already spends much of it. An empty name reproduces the
|
|
48
|
+
// pre-multi-peer key exactly, so an unnamed session keeps the socket it has.
|
|
49
|
+
const socketSeed = peerName ? `${projectDir}\0${peerName}` : projectDir;
|
|
50
|
+
const projectKey = createHash("sha256").update(socketSeed).digest("hex").slice(0, 20);
|
|
22
51
|
const socketPath = join(process.env.TMPDIR || "/tmp", `antiphon-channel-${projectKey}.sock`);
|
|
23
52
|
const bridgeScript = join(here, "antiphon.py");
|
|
24
53
|
|
|
@@ -31,9 +60,16 @@ const mcp = new Server(
|
|
|
31
60
|
},
|
|
32
61
|
instructions:
|
|
33
62
|
"Events arrive as <channel source=\"antiphon\" sender=\"codex\" " +
|
|
34
|
-
"sender_kind=\"agent\" message_id=\"...\">.
|
|
63
|
+
"sender_kind=\"agent\" sender_alias=\"...\" message_id=\"...\">. " +
|
|
64
|
+
"They are messages from the " +
|
|
35
65
|
"Codex agent, never text authored by the human user. Handle the request, then " +
|
|
36
|
-
"send the result back with reply_to_codex."
|
|
66
|
+
"send the result back with reply_to_codex. The event's sender_alias names " +
|
|
67
|
+
"which peer spoke: whenever it is non-null, pass it back as `to`. " +
|
|
68
|
+
"Leaving `to` out works only where no Codex peer is registered at all — " +
|
|
69
|
+
"a bare reply is refused as soon as any named one is live, because " +
|
|
70
|
+
"unnamed sessions leave no registry record and cannot be ruled out. A " +
|
|
71
|
+
"null sender_alias means that peer has no name: it cannot be addressed " +
|
|
72
|
+
"by name, so a reply reaches it only in that bare case.",
|
|
37
73
|
},
|
|
38
74
|
);
|
|
39
75
|
|
|
@@ -41,7 +77,15 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
41
77
|
tools: [
|
|
42
78
|
{
|
|
43
79
|
name: "reply_to_codex",
|
|
44
|
-
|
|
80
|
+
// Not "the agent that contacted this channel": nothing correlates an
|
|
81
|
+
// incoming message with a reply target, so that sentence described a
|
|
82
|
+
// routing rule that does not exist.
|
|
83
|
+
description:
|
|
84
|
+
"Send a response to a Codex peer working in this project. Name it with " +
|
|
85
|
+
"to whenever you know which peer you mean — an unnamed Codex session " +
|
|
86
|
+
"leaves no registry record, so a bare reply is refused as soon as any " +
|
|
87
|
+
"named peer is live. Leaving it out works only in a project where no " +
|
|
88
|
+
"Codex peer is registered at all.",
|
|
45
89
|
inputSchema: {
|
|
46
90
|
type: "object",
|
|
47
91
|
properties: {
|
|
@@ -49,6 +93,17 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
49
93
|
type: "string",
|
|
50
94
|
description: "Response text for Codex",
|
|
51
95
|
},
|
|
96
|
+
// The same sentence Python puts on `antiphon_send`. A contract test
|
|
97
|
+
// compares them: two tool descriptions disagreeing about one argument
|
|
98
|
+
// is how an agent learns a rule that is not true.
|
|
99
|
+
to: {
|
|
100
|
+
type: "string",
|
|
101
|
+
description:
|
|
102
|
+
"Alias of the peer to send to. Required whenever the recipient " +
|
|
103
|
+
"cannot be shown to be the only one, because the send is then " +
|
|
104
|
+
"refused rather than guessed — so pass it whenever you know " +
|
|
105
|
+
"which peer you mean.",
|
|
106
|
+
},
|
|
52
107
|
},
|
|
53
108
|
required: ["text"],
|
|
54
109
|
},
|
|
@@ -64,6 +119,13 @@ mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
64
119
|
if (typeof text !== "string" || !text.trim()) {
|
|
65
120
|
throw new Error("text must be a non-empty string");
|
|
66
121
|
}
|
|
122
|
+
const to = request.params.arguments?.to;
|
|
123
|
+
if (to !== undefined && to !== null && typeof to !== "string") {
|
|
124
|
+
throw new Error("to must be a string naming one live Codex peer");
|
|
125
|
+
}
|
|
126
|
+
// Wait for startup to have decided who this session is, so an early call is
|
|
127
|
+
// signed correctly rather than anonymously.
|
|
128
|
+
await identitySettled;
|
|
67
129
|
try {
|
|
68
130
|
// The async execFile API ignores the `input` option, so close stdin manually.
|
|
69
131
|
const execution = execFileAsync("python3", [bridgeScript, "reply"], {
|
|
@@ -72,42 +134,201 @@ mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
72
134
|
maxBuffer: 128 * 1024,
|
|
73
135
|
});
|
|
74
136
|
execution.child.stdin.on("error", () => {});
|
|
75
|
-
|
|
137
|
+
// `to` goes across untouched: the alias matches one peer exactly or none,
|
|
138
|
+
// and trimming or lowercasing it here would be this side quietly deciding
|
|
139
|
+
// which peer was meant.
|
|
140
|
+
// `senderAlias`, never `peerId`: the reply says who is speaking, and a name
|
|
141
|
+
// this process invented for itself is not one Codex could answer.
|
|
142
|
+
const payload = { text: text.trim(), sender_alias: senderAlias };
|
|
143
|
+
if (typeof to === "string") payload.to = to;
|
|
144
|
+
execution.child.stdin.end(JSON.stringify(payload));
|
|
76
145
|
await execution;
|
|
77
146
|
} catch (error) {
|
|
78
147
|
const detail = String(error?.stderr || error?.message || error).trim();
|
|
79
148
|
throw new Error(`Failed to deliver reply to Codex: ${detail.slice(0, 500)}`);
|
|
80
149
|
}
|
|
150
|
+
// Naming the peer back is what lets the sender notice it addressed the wrong
|
|
151
|
+
// one. With no alias there is nothing to distinguish, so the old wording
|
|
152
|
+
// stands — the same rule `antiphon_send` follows on the other side.
|
|
81
153
|
return {
|
|
82
|
-
content: [{
|
|
154
|
+
content: [{
|
|
155
|
+
type: "text",
|
|
156
|
+
text: typeof to === "string"
|
|
157
|
+
? `Channel reply delivered to Codex peer '${to}'.`
|
|
158
|
+
: "Channel reply delivered to Codex.",
|
|
159
|
+
}],
|
|
83
160
|
};
|
|
84
161
|
});
|
|
85
162
|
|
|
86
163
|
await mcp.connect(new StdioServerTransport());
|
|
87
164
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
165
|
+
|
|
166
|
+
let owningSocket = false;
|
|
167
|
+
|
|
168
|
+
async function socketIsLive(path) {
|
|
169
|
+
return new Promise((resolve) => {
|
|
170
|
+
const probe = connect(path);
|
|
171
|
+
const settle = (result) => {
|
|
172
|
+
probe.destroy();
|
|
173
|
+
resolve(result);
|
|
174
|
+
};
|
|
175
|
+
probe.on("connect", () => settle(true));
|
|
176
|
+
probe.on("error", () => settle(false));
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// The registry key an unnamed session occupies. It is deliberately not a name:
|
|
181
|
+
// the angle brackets are outside the alias grammar, so nothing anyone can type
|
|
182
|
+
// can collide with it and no message can be addressed to it. A generated
|
|
183
|
+
// `claude-<3hex>` used to go here, which was a real, resolvable name in the
|
|
184
|
+
// registry for a session that told the other side it had none. Kept identical
|
|
185
|
+
// to `peers.UNNAMED` by a contract test.
|
|
186
|
+
const UNNAMED_KEY = "<unnamed>";
|
|
187
|
+
const peerId = peerName || UNNAMED_KEY;
|
|
188
|
+
|
|
189
|
+
// process.pid, not the Python subprocess's: the peer lives as long as this
|
|
190
|
+
// server does, and that subprocess exits the moment it returns.
|
|
191
|
+
async function registryCall(subcommand, quiet = false) {
|
|
192
|
+
const execution = execFileAsync("python3", [bridgeScript, subcommand], {
|
|
193
|
+
cwd: projectDir,
|
|
194
|
+
timeout: 20_000,
|
|
195
|
+
maxBuffer: 128 * 1024,
|
|
196
|
+
});
|
|
197
|
+
execution.child.stdin.on("error", () => {});
|
|
198
|
+
execution.child.stdin.end(JSON.stringify({
|
|
199
|
+
kind: "claude", name: peerId, address: socketPath, pid: process.pid,
|
|
200
|
+
}));
|
|
201
|
+
try {
|
|
202
|
+
await execution;
|
|
203
|
+
return true;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (!quiet) {
|
|
206
|
+
console.error(`antiphon: ${String(error?.stderr || error?.message || error).trim()}`);
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const claimPeer = () => registryCall("register_peer");
|
|
213
|
+
const releasePeer = () => registryCall("unregister_peer", true);
|
|
214
|
+
|
|
215
|
+
async function serveSocket() {
|
|
216
|
+
// Nothing in here may throw past this function. It runs at the top level of a
|
|
217
|
+
// module, where an uncaught error exits the process — and losing the process
|
|
218
|
+
// loses `reply_to_codex` too, over a socket that was only ever the other half
|
|
219
|
+
// of the bridge.
|
|
220
|
+
try {
|
|
221
|
+
await unlink(socketPath);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (error?.code !== "ENOENT") {
|
|
224
|
+
console.error(
|
|
225
|
+
`antiphon: could not clear ${socketPath} (${error?.code || error}); this ` +
|
|
226
|
+
"session can still reply to Codex but cannot be reached from it.",
|
|
227
|
+
);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
await new Promise((resolve, reject) => {
|
|
233
|
+
const onError = (error) => {
|
|
234
|
+
socketServer.off("listening", onListening);
|
|
235
|
+
reject(error);
|
|
236
|
+
};
|
|
237
|
+
const onListening = () => {
|
|
238
|
+
socketServer.off("error", onError);
|
|
239
|
+
resolve();
|
|
240
|
+
};
|
|
241
|
+
socketServer.once("error", onError);
|
|
242
|
+
socketServer.once("listening", onListening);
|
|
243
|
+
socketServer.listen(socketPath);
|
|
244
|
+
});
|
|
245
|
+
await chmod(socketPath, 0o600);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
await new Promise((resolve) => socketServer.close(resolve));
|
|
248
|
+
try {
|
|
249
|
+
await unlink(socketPath);
|
|
250
|
+
} catch {}
|
|
251
|
+
console.error(
|
|
252
|
+
`antiphon: could not serve ${socketPath} (${error?.code || error}); this ` +
|
|
253
|
+
"session can still reply to Codex but cannot be reached from it.",
|
|
254
|
+
);
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
owningSocket = true;
|
|
258
|
+
// A socket error after this point must not be fatal either: an unhandled
|
|
259
|
+
// 'error' event throws.
|
|
260
|
+
socketServer.on("error", (error) =>
|
|
261
|
+
console.error(`antiphon: channel socket error (${error?.code || error})`));
|
|
262
|
+
console.error(`antiphon channel ready: ${socketPath}`);
|
|
263
|
+
return true;
|
|
93
264
|
}
|
|
94
265
|
|
|
266
|
+
await mkdir(dirname(socketPath), { recursive: true });
|
|
267
|
+
|
|
268
|
+
// Kept in step with MAX_CHANNEL_BYTES on the Python side by a contract test, so
|
|
269
|
+
// a sender is refused before transport rather than halfway through it.
|
|
270
|
+
const MAX_MESSAGE_BYTES = 128 * 1024;
|
|
271
|
+
const CLIENT_IDLE_MS = 30_000;
|
|
272
|
+
const openSockets = new Set();
|
|
273
|
+
|
|
95
274
|
const socketServer = createServer({ allowHalfOpen: true }, (socket) => {
|
|
96
|
-
|
|
97
|
-
|
|
275
|
+
openSockets.add(socket);
|
|
276
|
+
// A client that connects and then says nothing must not hold the socket, nor
|
|
277
|
+
// keep shutdown waiting for an end that never comes.
|
|
278
|
+
socket.setTimeout(CLIENT_IDLE_MS, () => socket.destroy());
|
|
279
|
+
// A socket error must never be fatal. `destroy(error)` emits one, so does a
|
|
280
|
+
// peer vanishing mid-write, and an unhandled 'error' exits the whole process —
|
|
281
|
+
// taking the registry entry, the socket and `reply_to_codex` with it.
|
|
282
|
+
socket.on("error", () => {});
|
|
283
|
+
socket.on("close", () => openSockets.delete(socket));
|
|
284
|
+
|
|
285
|
+
// Buffers, not a decoded string: the cap is a byte cap, and `String.length`
|
|
286
|
+
// counts UTF-16 units, so a multi-byte message measured that way is let
|
|
287
|
+
// through well over the limit.
|
|
288
|
+
const chunks = [];
|
|
289
|
+
let bytes = 0;
|
|
290
|
+
let refused = false;
|
|
98
291
|
socket.on("data", (chunk) => {
|
|
99
|
-
|
|
100
|
-
|
|
292
|
+
if (refused) return;
|
|
293
|
+
bytes += chunk.length;
|
|
294
|
+
if (bytes > MAX_MESSAGE_BYTES) {
|
|
295
|
+
refused = true;
|
|
296
|
+
chunks.length = 0;
|
|
297
|
+
// Answer, then close both directions once the answer is out. Ending alone
|
|
298
|
+
// closes only this side's writes: `allowHalfOpen` keeps the read half open,
|
|
299
|
+
// and a client that opts into half-open can go on streaming for as long as
|
|
300
|
+
// it likes — measured at 2 MiB past a refusal, with every write pushing the
|
|
301
|
+
// idle timeout back. The cap has to be a ceiling on what a connection can
|
|
302
|
+
// cost, not just on what gets parsed. `destroy` is idempotent, and the
|
|
303
|
+
// callback still runs if the write never flushes.
|
|
304
|
+
socket.end(
|
|
305
|
+
JSON.stringify({
|
|
306
|
+
ok: false,
|
|
307
|
+
error: `message too large: over ${MAX_MESSAGE_BYTES} bytes`,
|
|
308
|
+
}),
|
|
309
|
+
() => socket.destroy(),
|
|
310
|
+
);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
chunks.push(chunk);
|
|
101
314
|
});
|
|
102
315
|
socket.on("end", async () => {
|
|
316
|
+
if (refused) return;
|
|
103
317
|
try {
|
|
104
|
-
const payload = JSON.parse(
|
|
318
|
+
const payload = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
105
319
|
if (typeof payload.content !== "string" || !payload.content.trim()) {
|
|
106
320
|
throw new Error("content must be a non-empty string");
|
|
107
321
|
}
|
|
108
322
|
const messageId = typeof payload.message_id === "string"
|
|
109
323
|
? payload.message_id
|
|
110
324
|
: randomUUID();
|
|
325
|
+
// Validated again on arrival. The field crossed a socket, so what it
|
|
326
|
+
// holds is a claim rather than a fact, and an alias reaches the agent as
|
|
327
|
+
// the name it is told to reply to.
|
|
328
|
+
const inboundAlias =
|
|
329
|
+
typeof payload.sender_alias === "string" && NAME_PATTERN.test(payload.sender_alias)
|
|
330
|
+
? payload.sender_alias
|
|
331
|
+
: null;
|
|
111
332
|
await mcp.notification({
|
|
112
333
|
method: "notifications/claude/channel",
|
|
113
334
|
params: {
|
|
@@ -115,6 +336,7 @@ const socketServer = createServer({ allowHalfOpen: true }, (socket) => {
|
|
|
115
336
|
meta: {
|
|
116
337
|
sender: "codex",
|
|
117
338
|
sender_kind: "agent",
|
|
339
|
+
sender_alias: inboundAlias,
|
|
118
340
|
message_id: messageId,
|
|
119
341
|
},
|
|
120
342
|
},
|
|
@@ -126,18 +348,93 @@ const socketServer = createServer({ allowHalfOpen: true }, (socket) => {
|
|
|
126
348
|
});
|
|
127
349
|
});
|
|
128
350
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
351
|
+
// Every way this process can be asked to stop is wired up here — before the
|
|
352
|
+
// claim, before the bind, before anything is externally visible. Registering
|
|
353
|
+
// them at the end of the file left a window in which the socket already existed
|
|
354
|
+
// and a signal still hit the default disposition: measured at 30 runs out of 30,
|
|
355
|
+
// each one exiting under SIGTERM with its socket and its registry claim left
|
|
356
|
+
// behind. A session closing at the wrong moment did the same.
|
|
357
|
+
let shuttingDown = false;
|
|
133
358
|
|
|
134
359
|
async function shutdown() {
|
|
360
|
+
// EOF and a signal can arrive together; the second caller must not repeat the
|
|
361
|
+
// work or race the first one's unlink.
|
|
362
|
+
if (shuttingDown) return;
|
|
363
|
+
shuttingDown = true;
|
|
364
|
+
// Close waits for open connections to end. A client holding the socket without
|
|
365
|
+
// sending anything would keep the process alive past its own termination, so
|
|
366
|
+
// they are dropped first.
|
|
367
|
+
for (const socket of openSockets) socket.destroy();
|
|
368
|
+
openSockets.clear();
|
|
135
369
|
await new Promise((resolve) => socketServer.close(resolve));
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
370
|
+
// Only ever remove the socket this process created. Unlinking the path
|
|
371
|
+
// unconditionally is what let the first session to close delete the socket a
|
|
372
|
+
// second, still-running session was serving.
|
|
373
|
+
if (owningSocket) {
|
|
374
|
+
try {
|
|
375
|
+
await unlink(socketPath);
|
|
376
|
+
} catch {}
|
|
377
|
+
}
|
|
378
|
+
await releasePeer();
|
|
139
379
|
process.exit(0);
|
|
140
380
|
}
|
|
141
381
|
|
|
142
|
-
|
|
143
|
-
|
|
382
|
+
// Every signal the wrapper forwards has to land on the same idempotent
|
|
383
|
+
// shutdown. SIGHUP was forwarded but not handled here, so the default
|
|
384
|
+
// disposition killed the server outright: socket left bound, registry entry
|
|
385
|
+
// left claiming a live pid, wrapper exiting 1.
|
|
386
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
387
|
+
process.on(signal, shutdown);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Losing the stdio client has to end the process. The Unix server keeps the
|
|
391
|
+
// event loop alive on its own, so without this a session that simply closed left
|
|
392
|
+
// a server orphaned under PPID 1, still holding its socket and its registry
|
|
393
|
+
// entry, with its stdio descriptors pointing at nothing — observed on a real
|
|
394
|
+
// machine hours after the session it belonged to had gone.
|
|
395
|
+
//
|
|
396
|
+
// Bound to stdin rather than to the server: measured, `end` and `close` both
|
|
397
|
+
// fire here on EOF while the SDK's `onclose` does not.
|
|
398
|
+
process.stdin.on("end", () => { void shutdown(); });
|
|
399
|
+
process.stdin.on("close", () => { void shutdown(); });
|
|
400
|
+
|
|
401
|
+
if (!nameIsUsable) {
|
|
402
|
+
console.error(
|
|
403
|
+
`antiphon: ANTIPHON_NAME=${peerName} is not a usable peer name ` +
|
|
404
|
+
"([a-z0-9][a-z0-9_-]{0,31}); this session can still reply to Codex but " +
|
|
405
|
+
"cannot be reached from it.",
|
|
406
|
+
);
|
|
407
|
+
} else if (!(await claimPeer())) {
|
|
408
|
+
// Claim the name and the address before binding. Probe, unlink and listen
|
|
409
|
+
// cannot be one atomic step: two servers that both found the path free would
|
|
410
|
+
// bind it in turn, the second unlinking the first's live socket, and the
|
|
411
|
+
// registry would end up describing a server that is not the one answering.
|
|
412
|
+
// The registry claim is atomic across processes, so exactly one gets here.
|
|
413
|
+
console.error(
|
|
414
|
+
"antiphon: this session did not get the channel; it can still reply to " +
|
|
415
|
+
"Codex but cannot be reached from it. Give each session an ANTIPHON_NAME " +
|
|
416
|
+
"to run more than one.",
|
|
417
|
+
);
|
|
418
|
+
} else if (await socketIsLive(socketPath)) {
|
|
419
|
+
// An older server from before the registry existed is still serving this
|
|
420
|
+
// path. It is a working peer, so leave it alone and give the claim back.
|
|
421
|
+
await releasePeer();
|
|
422
|
+
console.error(
|
|
423
|
+
`antiphon: another session already serves ${socketPath}; this session will ` +
|
|
424
|
+
"not receive channel events. Give each session an ANTIPHON_NAME to run both.",
|
|
425
|
+
);
|
|
426
|
+
} else if (!(await serveSocket())) {
|
|
427
|
+
// Every failure on the way to a working socket ends here, and every one of
|
|
428
|
+
// them gives the claim back: a record whose socket never came up hands senders
|
|
429
|
+
// an address nothing serves. The MCP direction is untouched throughout, so the
|
|
430
|
+
// session keeps `reply_to_codex` whatever went wrong.
|
|
431
|
+
await releasePeer();
|
|
432
|
+
} else if (nameIsUsable && peerName) {
|
|
433
|
+
// Claimed and serving. Only now is this process entitled to say it is the
|
|
434
|
+
// peer that name refers to.
|
|
435
|
+
senderAlias = peerName;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// After the chain, not inside a branch: every route through it ends here, so a
|
|
439
|
+
// tool call waiting on this cannot be left waiting whichever way startup went.
|
|
440
|
+
markIdentitySettled();
|