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 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 terminals, two separate agents, an open-identity bridge.** While Claude Code and Codex CLI work in the same project, each sees the other's context and can wake the other when it needs to, without ever faking who the message is from.
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 two sides while preserving whether they came from the human user, from Claude, or from Codex.
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, marking in `.antiphon/cursor.json` which messages each side has already seen.
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
- ### Push live wake
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
- | Direction | Mechanism |
24
- |---|---|
25
- | Claude Codex | Claude `Stop` hook + `codex queue` |
26
- | Codex → Claude | Codex `Stop` hook + MCP Channel |
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
- 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.
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 18+ and Python 3. The Claude Code channel is a research
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 @serkancangokalp/antiphon
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 @serkancangokalp/antiphon@latest # from npm
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 # transcript, cursor and channel 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 the `antiphon_read` MCP tool for Codex in this project's
93
- `.codex/config.toml`, so there is nothing to add by hand. Note the entry
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
- The bridge works without this entry; it only lets Codex query the bridge by hand when it suspects the pull hook has gone quiet.
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`);