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/BACKLOG.md
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# Antiphon product backlog
|
|
2
|
+
|
|
3
|
+
Last reviewed: 2026-08-30
|
|
4
|
+
|
|
5
|
+
Priorities here describe product risk, not release promises. The bridge keeps
|
|
6
|
+
two invariants across every item: it preserves who said something, and it
|
|
7
|
+
refuses ambiguity rather than guessing or broadcasting.
|
|
8
|
+
|
|
9
|
+
## P0 — Lossless, paged context transfer
|
|
10
|
+
|
|
11
|
+
This item is now a phase ledger rather than one open problem: the delivery and
|
|
12
|
+
paging mechanics shipped, and what remains open is named below instead of
|
|
13
|
+
hiding under a general "lossless" claim.
|
|
14
|
+
|
|
15
|
+
### Shipped before the paging plan
|
|
16
|
+
|
|
17
|
+
Provenance-safe parsers (a user message beginning with `<` is no longer
|
|
18
|
+
mistaken for bridge metadata), the byte-offset reader and per-source cursor
|
|
19
|
+
with generation fingerprints, the delivery lock beside each peer cursor, and
|
|
20
|
+
the write-and-flush-before-advance transaction.
|
|
21
|
+
|
|
22
|
+
### Completed by the paging plan
|
|
23
|
+
|
|
24
|
+
- Oldest-first atomic pages of completed source records: an ordinary full page
|
|
25
|
+
targets 8,000 UTF-8 bytes and at most 40 records, and a record is never
|
|
26
|
+
split across pages.
|
|
27
|
+
- The non-tool 420-character cut and the 2,600-character summary trim are
|
|
28
|
+
gone; whitespace, indentation and line structure are preserved exactly.
|
|
29
|
+
- `has_more` is visible on every page, explicitly scoped to the currently
|
|
30
|
+
discovered sources.
|
|
31
|
+
- An oversized record is handed whole to the automatic hooks, whose hosts
|
|
32
|
+
were measured (2026-08-30, Claude Code 2.1.251 and Codex CLI 0.151.0) to
|
|
33
|
+
spill above 10,000 characters and expose a path; both 400,251-character
|
|
34
|
+
probes matched their spill files by SHA-256. Codex's MCP tool result did
|
|
35
|
+
**not** meet that assumption — the transport kept the bytes but the model
|
|
36
|
+
could identify neither content nor a saved path — so `antiphon_read`
|
|
37
|
+
refuses an oversized record without advancing, and the next automatic hook
|
|
38
|
+
delivers it. That is a measured host behaviour, not an inference about its
|
|
39
|
+
internal truncation.
|
|
40
|
+
- A rolling-upgrade-safe v3 page key: the legacy v2 value is preserved
|
|
41
|
+
byte-for-byte for still-running old processes and is never trusted as a
|
|
42
|
+
delivered frontier. Any present legacy key, and any malformed or unreadable
|
|
43
|
+
existing cursor, conservatively replays the currently discovered sources
|
|
44
|
+
from byte zero — measured at 69 Claude-source and 53 Codex-source pages on
|
|
45
|
+
the reviewed snapshots — with a fixed replay reason visible on every page
|
|
46
|
+
until the final persisted one clears it.
|
|
47
|
+
|
|
48
|
+
### Still open, by name
|
|
49
|
+
|
|
50
|
+
- Stable event ids and full tool-call retrieval: tool calls remain compressed
|
|
51
|
+
one-line summaries with no `antiphon_read(id)` route.
|
|
52
|
+
- The durable source catalog and the degraded-discovery marker: discovery
|
|
53
|
+
still reads the newest 3 transcripts per side, and `has_more: false` cannot
|
|
54
|
+
distinguish complete discovery from that window.
|
|
55
|
+
- Backward paging into history an older version already marked seen.
|
|
56
|
+
- The last-record content anchor (an in-place rewrite that keeps inode,
|
|
57
|
+
length and first line still resumes silently).
|
|
58
|
+
- Descriptor-safe reading of registry-supplied transcript paths.
|
|
59
|
+
- Direct-channel spill for the 128 KiB `antiphon_send` cap.
|
|
60
|
+
- Retirement of the preserved v2 sibling key once pre-v3 processes and
|
|
61
|
+
rollback support are no longer needed.
|
|
62
|
+
|
|
63
|
+
## P1 — Source-aware multi-peer pull context
|
|
64
|
+
|
|
65
|
+
Live push is explicitly addressed and never broadcast. Passive pull context is
|
|
66
|
+
project-wide awareness, which is useful, but today it merges transcripts under
|
|
67
|
+
generic `Claude`/`Codex` labels. With several terminals that can look like one
|
|
68
|
+
agent said another agent's words.
|
|
69
|
+
|
|
70
|
+
- Record and validate the source peer for every transcript used by pull.
|
|
71
|
+
- Label each event with the source alias (or honestly `unnamed`) and stable
|
|
72
|
+
session identity.
|
|
73
|
+
- Keep project-wide awareness separate from task dispatch: an event addressed
|
|
74
|
+
to `api` may be visible to `ui`, but must remain visibly addressed to `api`.
|
|
75
|
+
- Add an explicit filtering policy only if users ask for it; do not silently
|
|
76
|
+
infer Claude↔Codex pairs from matching aliases.
|
|
77
|
+
|
|
78
|
+
## P1 — Relayed human words are not the reader's own user
|
|
79
|
+
|
|
80
|
+
`build_summary` labels the other side's human as `YOU`. The block header says
|
|
81
|
+
which side it came from, but the line itself reads `[11:04] YOU: rewrite the
|
|
82
|
+
migration`, and nothing tells the reading agent that this is a person talking
|
|
83
|
+
to *somebody else*. An agent that treats it as its own user's instruction has
|
|
84
|
+
been handed authority nobody gave it — in a bridge whose whole invariant is
|
|
85
|
+
preserving who said something.
|
|
86
|
+
|
|
87
|
+
Provenance and authority are different questions, and this label answers only
|
|
88
|
+
the first. The fix is to say both: relay the words under a label that names them
|
|
89
|
+
as relayed, and state once, where the reader cannot miss it, that they are
|
|
90
|
+
context rather than a direct instruction. The existing header and footer already
|
|
91
|
+
carry that tone; the per-line label is the part that lies.
|
|
92
|
+
|
|
93
|
+
Worth settling with it: whether the relayed label should also carry the speaking
|
|
94
|
+
peer's alias when one is set, and whether an agent should ever act on a relayed
|
|
95
|
+
instruction without its own user confirming.
|
|
96
|
+
|
|
97
|
+
## P1 — Large direct-message attachments
|
|
98
|
+
|
|
99
|
+
The direct channel has a separate, honest 128 KiB byte cap. Keep it until an
|
|
100
|
+
oversized message has a recoverable path:
|
|
101
|
+
|
|
102
|
+
- atomically write mode-0600 content under `.antiphon/messages/`;
|
|
103
|
+
- send a size, SHA-256 hash and local reference instead of truncating;
|
|
104
|
+
- validate every reference beneath the project state directory;
|
|
105
|
+
- define acknowledgement, retry, TTL and total-quota behavior;
|
|
106
|
+
- show pending storage in `antiphon status` and clean it without deleting an
|
|
107
|
+
unread message silently.
|
|
108
|
+
|
|
109
|
+
This is separate from passive pull, whose old 2,600-character trim is retired
|
|
110
|
+
— pull now pages complete records. Ordinary long SQL and code already fit
|
|
111
|
+
under 128 KiB when sent through a channel tool.
|
|
112
|
+
|
|
113
|
+
## P1 — A marker in anything but the turn's last message is dropped
|
|
114
|
+
|
|
115
|
+
`push` reads the other side's newest assistant text through `last_claude_reply`
|
|
116
|
+
or `last_codex_reply`, and both keep only the most recent assistant record —
|
|
117
|
+
`chunks = texts` overwrites on each one, with the Claude side even documenting
|
|
118
|
+
it: "each new assistant message supersedes the last". One turn is not one
|
|
119
|
+
record. An agent that writes a progress message containing `@claude do this`
|
|
120
|
+
and then a closing message without markers has its instruction silently
|
|
121
|
+
dropped, because only the closing message is ever examined.
|
|
122
|
+
|
|
123
|
+
Observed, not theorised: it happened in this project during development. One
|
|
124
|
+
side reported sending a marker line, the other side received nothing, and later
|
|
125
|
+
messages arrived normally — the marker had been in an intermediate message of a
|
|
126
|
+
multi-part turn.
|
|
127
|
+
|
|
128
|
+
The obvious repair is wrong. Joining every assistant record in the tail window
|
|
129
|
+
would sweep up markers from previous turns and resend them: the dedupe
|
|
130
|
+
fingerprint compares the joined text, so a window that grows by one record each
|
|
131
|
+
turn produces a different fingerprint every time and pushes again. The fix needs
|
|
132
|
+
a boundary for "this turn" rather than a wider join.
|
|
133
|
+
|
|
134
|
+
### What has to be decided
|
|
135
|
+
|
|
136
|
+
- What delimits a turn on each side. Claude records carry a `promptId`; Codex
|
|
137
|
+
rollouts bracket turns with `task_started` / `task_complete` `event_msg`
|
|
138
|
+
records. Neither is verified as reliable for this purpose yet.
|
|
139
|
+
- Whether a marker in an intermediate message should be sent when it appears or
|
|
140
|
+
held until the turn ends. Sending immediately is what the author meant; it
|
|
141
|
+
also means a turn can push several times, which the fingerprint must handle
|
|
142
|
+
per marker rather than per joined blob.
|
|
143
|
+
- Whether the same boundary belongs in the pull path, which has its own reasons
|
|
144
|
+
to group records and now has an atomic-record model to group them with.
|
|
145
|
+
|
|
146
|
+
Until then the workaround is the one people find by accident: put the marker in
|
|
147
|
+
the last thing the turn says.
|
|
148
|
+
|
|
149
|
+
## P2 — A refused active send does not say the message will still arrive
|
|
150
|
+
|
|
151
|
+
When the direct channel refuses a send, the tools report the host's error and
|
|
152
|
+
stop there. Observed twice in one session: the Codex host answered
|
|
153
|
+
`direct app-server input is not allowed for unloaded spawned sub-agents`, so
|
|
154
|
+
`reply_to_codex` failed. `antiphon status` then showed the same text waiting in
|
|
155
|
+
the other side's pull queue — the passive path had it, and it was delivered on
|
|
156
|
+
the peer's next prompt.
|
|
157
|
+
|
|
158
|
+
The sender could not know that from the error. The reasonable reading of a
|
|
159
|
+
failed send is that the message was lost, which invites repeating it, or
|
|
160
|
+
proceeding as though the peer was never told. Both are worse than waiting.
|
|
161
|
+
|
|
162
|
+
The bridge already knows the answer: passive pull carries everything either
|
|
163
|
+
side wrote, and a refused *active* send changes only the timing. Say so in the
|
|
164
|
+
failure — name the fallback and what it costs, which is a delay until the peer's
|
|
165
|
+
next turn rather than a loss. Worth checking first whether any refusal exists
|
|
166
|
+
that the pull path genuinely cannot cover; if one does, it needs a different
|
|
167
|
+
message from the ones that merely arrive late.
|
|
168
|
+
|
|
169
|
+
## P1 — `antiphon doctor`
|
|
170
|
+
|
|
171
|
+
Add one read-only command that explains the common “bridge is quiet” cases:
|
|
172
|
+
|
|
173
|
+
- command/package version and which executable `PATH` resolves;
|
|
174
|
+
- Node/Python compatibility;
|
|
175
|
+
- hook, MCP and environment-forwarding configuration;
|
|
176
|
+
- current alias validity, live peers, readiness and stale records;
|
|
177
|
+
- channel socket reachability and Codex queue availability;
|
|
178
|
+
- actionable repair text. A future `--fix` may call the existing idempotent
|
|
179
|
+
setup path, but the default command must not edit anything.
|
|
180
|
+
|
|
181
|
+
## P2 — Reply correlation
|
|
182
|
+
|
|
183
|
+
Explicit `to` remains the safe default when several peers are live. Automatic
|
|
184
|
+
reply routing needs a durable design before implementation:
|
|
185
|
+
|
|
186
|
+
- correlate only after a successful delivery acknowledgement;
|
|
187
|
+
- scope pending messages to the receiving peer;
|
|
188
|
+
- validate the original sender is still the same live session;
|
|
189
|
+
- let explicit `to` override correlation;
|
|
190
|
+
- fail closed when several unanswered senders remain;
|
|
191
|
+
- define expiry and cleanup without losing a late reply.
|
|
192
|
+
|
|
193
|
+
## P2 — Automatic peer identity
|
|
194
|
+
|
|
195
|
+
Aliases are intentionally explicit in the first multi-peer release. A later
|
|
196
|
+
release may make unnamed peers visible, but only after both writers derive the
|
|
197
|
+
same identity on every supported host. There must be no user-settable owner-key
|
|
198
|
+
override, no short-id collision, and no “newest session” fallback once more than
|
|
199
|
+
one candidate is known.
|
|
200
|
+
|
|
201
|
+
There is a concrete Claude-side lead, not yet a contract. On Claude Code
|
|
202
|
+
2.1.251, `claude agents --json --cwd <project>` locally returned the active
|
|
203
|
+
interactive session with `pid`, exact `cwd`, `sessionId` and a generated `name`,
|
|
204
|
+
and the channel server's ancestor chain reached that pid. Before using it:
|
|
205
|
+
|
|
206
|
+
- feature-detect the command and schema; help text calls this background-agent
|
|
207
|
+
management even though JSON currently includes interactive sessions;
|
|
208
|
+
- measure the MCP startup race and use a bounded wait, never “newest” as a
|
|
209
|
+
fallback while the session has not appeared yet;
|
|
210
|
+
- fail anonymous when the server is orphaned or its ancestry cannot be joined
|
|
211
|
+
to exactly one entry;
|
|
212
|
+
- treat `sessionId` as the identity candidate and the generated `name` only as
|
|
213
|
+
untrusted display metadata until uniqueness and lifetime are documented;
|
|
214
|
+
- prove the equivalent Codex MCP/hook join before changing the product rule —
|
|
215
|
+
a Claude-only automatic name would restore the asymmetry this release removed;
|
|
216
|
+
- keep an explicit `ANTIPHON_NAME` as the deliberate override and test upgrade,
|
|
217
|
+
collision and mixed-version behaviour.
|
|
218
|
+
|
|
219
|
+
## P2 — Cross-vendor managed workers
|
|
220
|
+
|
|
221
|
+
A user should be able to tell a live Claude session “have Codex do this”, or a
|
|
222
|
+
live Codex session “have Claude review this”, without manually opening another
|
|
223
|
+
terminal and without making the foreign agent look like a native subagent. The
|
|
224
|
+
right abstraction is an **Antiphon-managed foreign worker**: the parent agent
|
|
225
|
+
can delegate to it and follow its lifecycle, but every event and result still
|
|
226
|
+
names the actual Claude or Codex session that produced it. This preserves the
|
|
227
|
+
bridge's identity invariant; an absent or ambiguous worker is refused rather
|
|
228
|
+
than guessed.
|
|
229
|
+
|
|
230
|
+
The first safe shape is:
|
|
231
|
+
|
|
232
|
+
- expose a small `delegate`, `status`, `result` and `cancel` lifecycle, with a
|
|
233
|
+
stable task id and explicit worker session id;
|
|
234
|
+
- return immediately after acceptance by default, so the parent can continue
|
|
235
|
+
working and collect the result later;
|
|
236
|
+
- label every update and artifact as coming from the foreign worker, never as
|
|
237
|
+
the parent agent's own reasoning or work;
|
|
238
|
+
- give every write-capable task its own Git worktree; a worker must not edit in
|
|
239
|
+
the parent session's checkout or race another worker over the same files;
|
|
240
|
+
- never give the worker a broader permission class than the delegating session
|
|
241
|
+
or an explicit human grant, and never let a worker approve the parent's
|
|
242
|
+
permission requests, merge its own work, or silently widen its sandbox;
|
|
243
|
+
- default the cross-agent hop budget to one. Nested delegation is refused unless
|
|
244
|
+
the user explicitly opts into a higher bounded value, so
|
|
245
|
+
Claude → Codex → Claude cannot become an invisible recursive loop;
|
|
246
|
+
- make `blocked`, `completed`, `failed`, `cancelled` and timeout outcomes
|
|
247
|
+
explicit, and return reviewable evidence such as the diff and test results
|
|
248
|
+
with a completed write task.
|
|
249
|
+
|
|
250
|
+
This must be implemented as an Antiphon lifecycle over host adapters, not by
|
|
251
|
+
pretending that either host natively spawned the other vendor's model. Each host
|
|
252
|
+
already has its own same-vendor nesting story, and neither is a cross-vendor
|
|
253
|
+
contract: Claude Code documents that a subagent inherits the main conversation's
|
|
254
|
+
MCP tools and may itself spawn subagents up to a configurable depth
|
|
255
|
+
(`CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH`, set to `1` to turn nesting off), and on
|
|
256
|
+
this machine Codex CLI 0.151.0 advertises `multi_agent` as a stable feature while
|
|
257
|
+
the generated App Server schema mentioning `spawnAgent` is still experimental.
|
|
258
|
+
Neither establishes a stable way for one vendor to spawn the other's model, and
|
|
259
|
+
this feature must not depend on one without version detection and a tested
|
|
260
|
+
fallback. The bounded-depth precedent is worth copying rather than reinventing:
|
|
261
|
+
a documented, configurable limit is exactly the shape the hop budget above takes.
|
|
262
|
+
|
|
263
|
+
### Decisions still required
|
|
264
|
+
|
|
265
|
+
- Whether `delegate` may target an already-running named peer, always creates a
|
|
266
|
+
fresh managed worker, or exposes both modes explicitly.
|
|
267
|
+
- Whether managed workers are one-task ephemeral sessions or can be resumed,
|
|
268
|
+
and what expiry, cleanup and storage quotas apply.
|
|
269
|
+
- Which host adapters are supported first, and whether an unavailable native
|
|
270
|
+
worker API may fall back to a documented CLI/SDK subprocess.
|
|
271
|
+
- Which task classes may run without another user confirmation, and who may
|
|
272
|
+
accept a worker's patch or merge it after deterministic checks pass.
|
|
273
|
+
- Whether a synchronous wait mode is worth exposing in addition to the safer
|
|
274
|
+
asynchronous default.
|
|
275
|
+
|
|
276
|
+
No claim is made yet that a Claude worker can appear in Codex's native agent UI,
|
|
277
|
+
or that a Codex worker can appear in Claude Code's native subagent UI. That UX
|
|
278
|
+
would be optional integration work; the portable contract is Antiphon's own
|
|
279
|
+
named worker, task lifecycle and evidence trail.
|
|
280
|
+
|
|
281
|
+
## Observed, not adopted — Claude Code's per-session messaging socket
|
|
282
|
+
|
|
283
|
+
Measured on one macOS machine, 2026-08-30: a Claude Code session exports
|
|
284
|
+
`CLAUDE_CODE_MESSAGING_SOCKET`, `CLAUDE_CODE_MESSAGING_TOKEN` and
|
|
285
|
+
`CLAUDE_CODE_SESSION_ID` into the processes it starts, including hooks and
|
|
286
|
+
stdio MCP servers. The socket path pointed at a real Unix domain socket under a
|
|
287
|
+
temporary directory, the token was a 32-character string, and the session id was
|
|
288
|
+
a UUID. Nothing was connected to and no token value was read.
|
|
289
|
+
|
|
290
|
+
This is recorded because it is easy to find and tempting to use, and because the
|
|
291
|
+
temptation should be answered once rather than every time somebody notices it.
|
|
292
|
+
It is not documented, its path is named after a process id, and it exists on one
|
|
293
|
+
side of a bridge whose entire purpose is the asymmetry between two hosts —
|
|
294
|
+
Codex CLI has no equivalent. A published package that made an undocumented
|
|
295
|
+
internal interface its transport would break silently on the first release that
|
|
296
|
+
moved it, and the failure would look like the bridge going quiet.
|
|
297
|
+
|
|
298
|
+
If a first-party, documented agent-to-agent transport ever ships on both sides,
|
|
299
|
+
this is the entry to revisit. Until then Antiphon owns its own sockets.
|
|
300
|
+
|
|
301
|
+
## P2 — Multi-line Stop markers
|
|
302
|
+
|
|
303
|
+
Stop markers currently carry one line. Channel tools preserve multi-line text
|
|
304
|
+
and are the recommended route for long content. If users need natural-language
|
|
305
|
+
Stop blocks, add an explicit delimited syntax with tests for fenced code,
|
|
306
|
+
embedded marker text, empty blocks and deduplication; never guess continuation
|
|
307
|
+
from arbitrary following prose.
|
|
308
|
+
|
|
309
|
+
## P1 — Re-run the host wrapper census before release
|
|
310
|
+
|
|
311
|
+
`CLAUDE_HOST_WRAPPERS` and `CODEX_HOST_WRAPPERS` in `lib/antiphon.py` hold
|
|
312
|
+
exactly what a census measured (2026-08-30), and nothing else. They will go
|
|
313
|
+
stale as each host adds, renames or drops its own wrapper tags, and the
|
|
314
|
+
obligation to re-measure must not live only in a planning document that ships
|
|
315
|
+
nowhere. Re-run it before every release:
|
|
316
|
+
|
|
317
|
+
- count every `role: user` text record whose text opens with `<`, split by
|
|
318
|
+
side, each one carrying its `promptSource` value (or its absence);
|
|
319
|
+
- for every opening tag that turns up, decide host bookkeeping or a person's
|
|
320
|
+
own words before touching either set — a tag seen on only one side stays out
|
|
321
|
+
of the other's, the way `local-command-caveat` did until it was measured;
|
|
322
|
+
- update the sets, the measurement comment above `CLAUDE_HOST_WRAPPERS`, and
|
|
323
|
+
this entry's date together, so none of the three can drift from the other
|
|
324
|
+
two.
|
|
325
|
+
|
|
326
|
+
The asymmetry that governs a doubtful case: a tag missing from a set lets one
|
|
327
|
+
stray host line leak into a summary — visible, and cheap to fix by adding it.
|
|
328
|
+
A tag wrongly present deletes a person's message — silently, with nothing left
|
|
329
|
+
behind to notice it happened. When the evidence is thin, leave the tag out.
|
package/README.md
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
# Antiphon
|
|
2
2
|
|
|
3
|
-
**Two
|
|
3
|
+
**Two agents in one project, an open-identity bridge.** Claude Code and Codex CLI work side by side — one terminal each, or several on each side — and each sees the other's context and can wake the other when it needs to, without ever faking who the message is from.
|
|
4
4
|
|
|
5
|
-
Antiphon doesn't dispatch work. It only carries messages between the
|
|
5
|
+
Antiphon doesn't dispatch work. It only carries messages between the sides while preserving whether they came from the human user, from Claude, or from Codex.
|
|
6
|
+
|
|
7
|
+
With one terminal per side there is nothing to configure beyond `antiphon setup`: peers go unnamed, messages have only one place to go, and the rest of this page is background. Naming becomes necessary the moment a second session opens on either side — see [Many peers](#many-peers).
|
|
6
8
|
|
|
7
9
|
## How it works
|
|
8
10
|
|
|
9
|
-
No shared log is kept. Both CLIs already write their own transcripts; Antiphon reads and derives from them,
|
|
11
|
+
No shared log is kept. Both CLIs already write their own transcripts; Antiphon reads and derives from them, recording which messages each peer has already seen. An unnamed peer keeps its cursor at `.antiphon/cursor.json`; a named one owns `.antiphon/peers/<side>-<name>/cursor.json`, so two sessions on the same side never advance each other's place.
|
|
10
12
|
|
|
11
|
-
### Pull — context, no wake
|
|
13
|
+
### Pull — shared context, no wake
|
|
12
14
|
|
|
13
15
|
| Direction | Mechanism |
|
|
14
16
|
|---|---|
|
|
@@ -18,32 +20,118 @@ No shared log is kept. Both CLIs already write their own transcripts; Antiphon r
|
|
|
18
20
|
The other side's recent messages enter your turn's context when you type
|
|
19
21
|
something. Nobody is woken up.
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
This path is project-wide awareness, not delivery. It is not addressed to
|
|
24
|
+
anyone, and today it can merge activity from several project transcripts under
|
|
25
|
+
one generic `Claude` or `Codex` label — so with several terminals open, one
|
|
26
|
+
agent's words can arrive looking like another's. Source-aware labelling is a
|
|
27
|
+
tracked P1 item in [BACKLOG.md](BACKLOG.md). Until it lands, do not read pull
|
|
28
|
+
context as a private line between two particular peers.
|
|
22
29
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
|
26
|
-
|
|
30
|
+
### Push — addressed, live wake
|
|
31
|
+
|
|
32
|
+
| Direction | At the end of a turn | Mid-turn |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| Claude → Codex | `Stop` hook + `codex queue` | `reply_to_codex` tool |
|
|
35
|
+
| Codex → Claude | `Stop` hook + MCP Channel | `antiphon_send` tool |
|
|
27
36
|
|
|
28
37
|
A line starting with `@codex` or `@claude` in a reply reaches the other
|
|
29
38
|
agent immediately, even if nobody is typing.
|
|
30
39
|
|
|
40
|
+
Every push is addressed to exactly one peer and is never broadcast. When the
|
|
41
|
+
recipient cannot be shown to be the only candidate, the send is refused rather
|
|
42
|
+
than guessed.
|
|
43
|
+
|
|
44
|
+
Neither side has to wait for its turn to end. Either agent can hand work
|
|
45
|
+
over mid-turn and keep going, so the other starts on it in parallel; the
|
|
46
|
+
answer is collected later from the same turn with `antiphon_read` (Codex)
|
|
47
|
+
or the channel event (Claude). Nothing blocks, and a message delivered by
|
|
48
|
+
a tool is recorded, so ending the turn with the same `@claude` / `@codex`
|
|
49
|
+
line does not send it twice.
|
|
50
|
+
|
|
31
51
|
### How identity is preserved
|
|
32
52
|
|
|
33
53
|
A Claude → Codex message reaches Codex tagged either `[Antiphon bridge] Claude:` (pushed from Claude's Stop hook) or `[Antiphon channel] Claude:` (a direct reply sent through the channel, via the `reply_to_codex` tool) — either way, Codex sees these as Claude's words, not the human user's.
|
|
34
54
|
|
|
55
|
+
The tag is followed by `[from=<alias> id=<uuid>]`, naming which Claude peer spoke so a reply can be addressed back to it. A session started without `ANTIPHON_NAME` shows `from=<unnamed>`: it has no name to be addressed by, and the angle brackets keep that apart from a peer actually called `unnamed`. The id names one delivery attempt — it is not a correlation id, and nothing routes replies by it.
|
|
56
|
+
|
|
35
57
|
A Codex → Claude message never pastes text into the terminal and never impersonates user input. The local MCP server sends Claude Code a `notifications/claude/channel` event. Its metadata looks like:
|
|
36
58
|
|
|
37
59
|
```xml
|
|
38
|
-
<channel source="antiphon" sender="codex" sender_kind="agent" message_id="...">
|
|
60
|
+
<channel source="antiphon" sender="codex" sender_kind="agent" sender_alias="build" message_id="...">
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Claude Code's interface shows this as an incoming channel event, and Claude treats the message as the words of the Codex agent, not of the human user. It sends its reply back with the `reply_to_codex` MCP tool, passing `sender_alias` as `to` whenever it is non-null. A bare reply is refused as soon as any named Codex peer is registered: an unnamed Codex session leaves no registry record, so one visible peer cannot be shown to be the only one running. A `null` `sender_alias` is a peer with no name — it cannot be addressed by name, and a bare reply reaches it only in a project where nothing is registered.
|
|
64
|
+
|
|
65
|
+
Nothing pairs peers up. There is no automatic Claude↔Codex partnership, and no reply correlation: a message is routed only by the name written on it.
|
|
66
|
+
|
|
67
|
+
## Many peers
|
|
68
|
+
|
|
69
|
+
A name is an environment variable read at startup, so it goes in front of the
|
|
70
|
+
command:
|
|
71
|
+
|
|
72
|
+
ANTIPHON_NAME=ui claude --dangerously-load-development-channels server:antiphon
|
|
73
|
+
ANTIPHON_NAME=api claude --dangerously-load-development-channels server:antiphon
|
|
74
|
+
ANTIPHON_NAME=build codex
|
|
75
|
+
ANTIPHON_NAME=review codex
|
|
76
|
+
|
|
77
|
+
Once named, a peer is addressed explicitly — by marker at the start of a line,
|
|
78
|
+
or by the `to` argument of the tool that sends without ending the turn:
|
|
79
|
+
|
|
80
|
+
| From | Marker | Tool |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| Codex → Claude | `@claude:ui` | `antiphon_send(to="ui", text=…)` |
|
|
83
|
+
| Claude → Codex | `@codex:build` | `reply_to_codex(to="build", text=…)` |
|
|
84
|
+
|
|
85
|
+
There is no way to reach several peers at once. A send is delivered to the one
|
|
86
|
+
peer named on it, and to nobody else.
|
|
87
|
+
|
|
88
|
+
### When a bare message is refused
|
|
89
|
+
|
|
90
|
+
The two sides fail closed on different rules, because they leave different
|
|
91
|
+
traces:
|
|
92
|
+
|
|
93
|
+
- **To Claude.** A bare `@claude` works while exactly one Claude peer is live.
|
|
94
|
+
From the second one on, it is refused and you must name one.
|
|
95
|
+
- **To Codex.** A bare `@codex` is refused as soon as *any* named Codex peer is
|
|
96
|
+
registered — even if it is the only one you can see. A Codex session started
|
|
97
|
+
without a name leaves no registry record at all, so a second, unnamed one
|
|
98
|
+
cannot be ruled out, and the bridge will not guess between a peer it can see
|
|
99
|
+
and one it cannot.
|
|
100
|
+
|
|
101
|
+
That asymmetry is why **every terminal in a multi-peer project must be named,
|
|
102
|
+
Codex terminals above all**. Mixing named and unnamed sessions is the one
|
|
103
|
+
configuration that can leave a message impossible to answer: the unnamed peer
|
|
104
|
+
is live, it can send, and there is no name to send a reply back to.
|
|
105
|
+
|
|
106
|
+
### Seeing who is live
|
|
107
|
+
|
|
108
|
+
antiphon status
|
|
109
|
+
|
|
110
|
+
Beyond transcripts and cursors, `status` lists every registered peer with the
|
|
111
|
+
side it runs on, the name it took, and its state — `ready` once it has an
|
|
112
|
+
address to receive on, or `waiting for first turn` before that. Under the list
|
|
113
|
+
it prints the addressing rule that currently applies:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
Peers:
|
|
117
|
+
Claude ui — ready
|
|
118
|
+
Claude api — ready
|
|
119
|
+
Codex build — ready
|
|
120
|
+
Codex review — waiting for first turn
|
|
121
|
+
→ a bare @claude line is refused; address one: @claude:ui, @claude:api
|
|
122
|
+
→ a bare @codex line is refused, because unnamed Codex sessions leave no record; address one: @codex:build, @codex:review
|
|
39
123
|
```
|
|
40
124
|
|
|
41
|
-
|
|
125
|
+
A peer that is `waiting for first turn` is still a candidate: readiness never
|
|
126
|
+
decides who a message goes to, so it cannot silently hand routing to whichever
|
|
127
|
+
session happened to start first. With nothing registered — the unnamed single
|
|
128
|
+
pair — the block is empty, because there is nobody to choose between.
|
|
42
129
|
|
|
43
130
|
## Install
|
|
44
131
|
|
|
45
|
-
Requires Node
|
|
46
|
-
preview and needs Claude Code 2.1.80 or newer
|
|
132
|
+
Requires Node 20+ and Python 3.9+. The Claude Code channel is a research
|
|
133
|
+
preview and needs Claude Code 2.1.80 or newer; recent Claude Code releases
|
|
134
|
+
set their own, higher, Node floor, so check theirs as well.
|
|
47
135
|
|
|
48
136
|
Install the command, either straight from the repository:
|
|
49
137
|
|
|
@@ -51,7 +139,7 @@ Install the command, either straight from the repository:
|
|
|
51
139
|
|
|
52
140
|
or from npm:
|
|
53
141
|
|
|
54
|
-
npm i -g
|
|
142
|
+
npm i -g antiphon
|
|
55
143
|
|
|
56
144
|
Either way the command is `antiphon` — the package name only decides
|
|
57
145
|
where it comes from. Then, in the project the two agents share:
|
|
@@ -73,7 +161,7 @@ Codex hooks once when Codex first shows them.
|
|
|
73
161
|
## Update
|
|
74
162
|
|
|
75
163
|
npm i -g github:serkancangokalp/antiphon # from the repository
|
|
76
|
-
npm i -g
|
|
164
|
+
npm i -g antiphon@latest # from npm
|
|
77
165
|
|
|
78
166
|
cd /your/project && antiphon setup
|
|
79
167
|
|
|
@@ -83,21 +171,23 @@ versions in place; it never creates duplicates.
|
|
|
83
171
|
## Commands
|
|
84
172
|
|
|
85
173
|
```bash
|
|
86
|
-
antiphon status #
|
|
174
|
+
antiphon status # transcripts, cursors, live peers and channel status
|
|
87
175
|
antiphon summary [side] # show the context that would be injected
|
|
88
176
|
antiphon setup # (re)install the project setup
|
|
89
177
|
npm test # Python unit tests + real MCP protocol test
|
|
90
178
|
```
|
|
91
179
|
|
|
92
|
-
`setup` registers
|
|
93
|
-
`.codex/config.toml`, so there is nothing to add by
|
|
180
|
+
`setup` registers Codex's MCP tools — `antiphon_read` and `antiphon_send`
|
|
181
|
+
— in this project's `.codex/config.toml`, so there is nothing to add by
|
|
182
|
+
hand. Note the entry
|
|
94
183
|
names `args = ["mcp"]`: the `channel` server is Claude's side and hands out
|
|
95
184
|
`reply_to_codex`. Aiming Codex at it would let Codex publish messages
|
|
96
185
|
labelled as Claude's — exactly what this bridge exists to prevent — so
|
|
97
186
|
`setup` rewrites that table whenever it is wrong, leaving the rest of the
|
|
98
|
-
file alone.
|
|
187
|
+
file alone. The same table forwards `ANTIPHON_NAME` into the tool process,
|
|
188
|
+
because Codex does not pass the parent environment through on its own.
|
|
99
189
|
|
|
100
|
-
|
|
190
|
+
Without this entry the pull hook still delivers Claude's context at the start of each Codex turn, but Codex loses both tools: it can no longer check the bridge by hand, nor reach Claude before its turn ends.
|
|
101
191
|
|
|
102
192
|
## Limits
|
|
103
193
|
|
|
@@ -106,8 +196,68 @@ The bridge works without this entry; it only lets Codex query the bridge by hand
|
|
|
106
196
|
- Channels is currently a research preview; it requires a claude.ai login or a Console API key. It isn't supported on the Bedrock, Vertex, or Foundry providers. A Team/Enterprise admin may need to enable the feature.
|
|
107
197
|
- The Codex hook asks for re-approval the first time it's used and whenever the hook file changes.
|
|
108
198
|
- Matching is done on the same project's absolute directory.
|
|
109
|
-
- Once a message has been seen, the cursor advances and the same content is never injected twice.
|
|
110
|
-
- Context transfer has a budget of roughly 2600 characters; the newest messages are kept.
|
|
111
199
|
- Unix sockets only — there is no Windows support.
|
|
112
200
|
|
|
201
|
+
### Passive pull pages, and what it still cannot promise
|
|
202
|
+
|
|
203
|
+
The pull path delivers the other side's transcript as pages of completed
|
|
204
|
+
records, oldest first. An ordinary full page targets 8,000 UTF-8 bytes and at
|
|
205
|
+
most 40 completed source records — the byte number is measured against the
|
|
206
|
+
installed hosts' injection limits, not a permanent host guarantee. Non-tool
|
|
207
|
+
records are no longer cut or flattened: line structure, indentation, code and
|
|
208
|
+
SQL formatting travel intact, and a record is never split across pages.
|
|
209
|
+
|
|
210
|
+
A page that leaves work behind says so with a visible `has_more: true` line;
|
|
211
|
+
calling `antiphon_read` again (or simply letting later turns run) drains the
|
|
212
|
+
rest. Either `has_more` value describes only the transcripts discovery can
|
|
213
|
+
currently see — discovery still reads only the newest 3 transcript files per
|
|
214
|
+
side, so `has_more: false` is not an inventory of all project history.
|
|
215
|
+
|
|
216
|
+
One record larger than an ordinary page is handled asymmetrically, from
|
|
217
|
+
measurement rather than preference. Both hosts' automatic prompt hooks save an
|
|
218
|
+
oversized injection to a host-managed file and show the model a preview and the
|
|
219
|
+
path, so the hook hands such a record over whole — which means host-written
|
|
220
|
+
spill files may contain verbatim transcript text, under the host's own
|
|
221
|
+
lifecycle. Codex's MCP tool-result surface showed no such verified path, so
|
|
222
|
+
`antiphon_read` refuses that one record instead: nothing is read or marked
|
|
223
|
+
seen, and the next automatic prompt hook delivers it.
|
|
224
|
+
|
|
225
|
+
Page positions live under an isolated v3 cursor key, `<side>_pages`
|
|
226
|
+
(`claude_pages`, `codex_pages`). The old `<side>_seen` value is
|
|
227
|
+
preserved untouched beside it for still-running pre-upgrade processes and
|
|
228
|
+
rollback — it is never trusted or overwritten by paging code, and it is
|
|
229
|
+
scheduled for
|
|
230
|
+
retirement once pre-v3 processes no longer need it, not a template for
|
|
231
|
+
accumulating keys. Any present legacy value, and equally a malformed or
|
|
232
|
+
unreadable existing cursor file, starts a conservative replay of the currently
|
|
233
|
+
discovered sources from byte zero; only a genuinely missing cursor means a new
|
|
234
|
+
side and keeps the normal six-hour lookback. The old promise that a timestamp
|
|
235
|
+
cursor migrates at its exact boundary is gone: that boundary cannot be trusted
|
|
236
|
+
while an old process may still move it. The replay is bounded but it is not
|
|
237
|
+
small — on the reviewed snapshots it took 69 Claude-source pages and 53
|
|
238
|
+
Codex-source pages, up to that many automatic prompt turns — and every replay
|
|
239
|
+
page carries one of exactly two fixed explanation lines, one for the legacy
|
|
240
|
+
upgrade and one for cursor recovery, until the final successfully persisted
|
|
241
|
+
page clears it, so duplicated history is visible as recovery rather than
|
|
242
|
+
mistaken for a malfunction. A failed delivery leaves the cursor bytes exactly
|
|
243
|
+
as they were.
|
|
244
|
+
|
|
245
|
+
What still loses, by name: tool calls remain compressed one-line summaries
|
|
246
|
+
with no stable-id retrieval yet, discovery has no catalog (the newest-3 window
|
|
247
|
+
above), and there is no backward paging into history an older version already
|
|
248
|
+
marked seen. Those are tracked in [BACKLOG.md](BACKLOG.md).
|
|
249
|
+
|
|
250
|
+
### The Codex-to-Claude channel refuses rather than truncates
|
|
251
|
+
|
|
252
|
+
A message sent from Codex to Claude with `antiphon_send` or `@claude` uses the
|
|
253
|
+
Unix-socket channel. Its serialized payload has a separate 128 KiB byte cap,
|
|
254
|
+
checked by the sender before transport and by the server on arrival. Over that,
|
|
255
|
+
the send fails with an error you can see; it is never silently shortened, so
|
|
256
|
+
ordinary long code and SQL within the cap travel intact.
|
|
257
|
+
|
|
258
|
+
The reverse Claude-to-Codex path uses `codex queue` and does not share that
|
|
259
|
+
explicit Antiphon byte cap. It also has no oversized-message attachment
|
|
260
|
+
protocol yet, so extremely large direct transfers in either direction remain a
|
|
261
|
+
P1 item in [BACKLOG.md](BACKLOG.md).
|
|
262
|
+
|
|
113
263
|
MIT.
|
package/bin/antiphon.mjs
CHANGED
|
@@ -12,6 +12,17 @@ const target = subcommand === "channel"
|
|
|
12
12
|
: { cmd: "python3", args: [join(here, "..", "lib", "antiphon.py"), ...(subcommand ? [subcommand] : []), ...rest] };
|
|
13
13
|
|
|
14
14
|
const child = spawn(target.cmd, target.args, { stdio: "inherit" });
|
|
15
|
+
|
|
16
|
+
// Forward signals rather than dying under them. Without this the wrapper exits
|
|
17
|
+
// first and leaves the channel server orphaned under PPID 1, still holding its
|
|
18
|
+
// socket — and the server never learns it was supposed to stop. Exit is left to
|
|
19
|
+
// the child's own exit handler below, so it gets to clean up first.
|
|
20
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
21
|
+
process.on(signal, () => {
|
|
22
|
+
if (!child.killed) child.kill(signal);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
15
26
|
child.on("exit", (code, signal) => process.exit(signal ? 1 : code ?? 0));
|
|
16
27
|
child.on("error", (error) => {
|
|
17
28
|
process.stderr.write(`antiphon: ${error.message}\n`);
|