projmux 0.15.2 → 0.16.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.
@@ -107,8 +107,11 @@ the same lease. Its address is derived from the exact `AgentRouteRef`; names,
107
107
  tmux `%N`, the provider socket, and the provider token do not enter that address
108
108
  or its protocol. Every operation revalidates Agent UID, Pane UID, activation
109
109
  generation, provider/helper process births, registration generation, and route
110
- incarnation. Normal exit and cleanup unlink only the exact owned coord socket
111
- inode. Projmux never creates a replacement provider listener or relay.
110
+ incarnation. The route incarnation follows the provider conversation (the
111
+ Claude session), so a same-session SessionStart keeps it; the process birth
112
+ and registration generation checks are what fence a replaced helper. Normal
113
+ exit and cleanup unlink only the exact owned coord socket inode. Projmux never
114
+ creates a replacement provider listener or relay.
112
115
 
113
116
  Claude ingress is immediate push. There is no receiver waiter, pending ingress
114
117
  queue, `asyncRewake`, `begin-handoff`, or `no-waiter` state. The helper retains
@@ -127,6 +130,22 @@ failure before any byte is a non-ambiguous `provider-write-zero`; partial bytes,
127
130
  a write error after bytes, or a lost helper response is ambiguous and has
128
131
  `autoResend=false`. A complete write plus helper return is only a transport
129
132
  handoff, not proof that Claude parsed, displayed, processed, or answered it.
133
+ Push content has no length cap of its own: only the 8192-byte serialized
134
+ auth+user frame bounds it, and a size excess ends as
135
+ `provider-frame-too-large: frameBytes=N limitBytes=8192` with zero provider
136
+ bytes; the reply tool argv body stays at 4096 bytes.
137
+
138
+ `agent message send` applies the same budget to a Claude target before
139
+ acceptance, for plain sends and `--reply-to` alike. The sender renders the
140
+ content with the helper's renderer, once with its own executable and once with
141
+ the fixed executable phrase, keeps the longer frame, and counts an assumed
142
+ 48-byte auth token; it never reads a messaging token. A frame over 8192 bytes
143
+ exits nonzero with `provider-frame-too-large: frameBytes=N limitBytes=8192`
144
+ before any receipt is stored or the helper is called. Codex targets are not
145
+ pre-checked. A helper started before this rule still caps push content at 4096
146
+ bytes and fails a larger body with `provider-frame-invalid-content`; the
147
+ sender's receipt then names the rendered content bytes and asks to re-activate
148
+ (restart) the target Agent.
130
149
 
131
150
  The frozen frame is closed to Claude Code `2.1.263`. A different provider
132
151
  version, helper replacement, provider restart, registration replacement, or
@@ -152,10 +171,153 @@ a new explicit command. A version string supplied by the caller is never proof.
152
171
  After qualification, `agent message send` persists the exact immutable broker
153
172
  handoff before any provider byte. A missing, mismatched, or unwritable durable
154
173
  record therefore writes zero. The pushed content is a structured
155
- `projmux-coordination` object with `untrusted-coordination-only` authority and
156
- the exact source/target routes, `messageRef`, `conversationRef`, `replyTo`, and
157
- payload. It cannot start or steer a user turn, answer an approval, interrupt a
158
- turn, execute a tool, call a connector, or write Codex app-server/model history.
174
+ `projmux-coordination` object with `untrusted-coordination-only` authority,
175
+ the source and target `agentUID` and `provider`, `messageRef`,
176
+ `conversationRef`, `replyTo`, payload, a `sourceNotice`, and a `replyAction`.
177
+ It cannot start or steer a user turn, answer an approval, interrupt a turn,
178
+ execute a tool, call a connector, or write Codex app-server/model history.
179
+
180
+ The frame route is deliberately narrower than the durable one. The delivery
181
+ fences (`paneUID`, `activationGeneration`, `incarnation`) stay on the durable
182
+ envelope: no frame reader uses them, and a reply re-resolves its route from the
183
+ durable record, not from the frame. `sourceNotice` says the source is a claim
184
+ and the payload untrusted peer coordination; it stays on every frame, including
185
+ one whose source and target are the same Agent, because `--source` is an
186
+ unverified claim. That self-anchored frame has the same keys with an empty
187
+ `replyAction`, since there is no peer to answer. The Codex turn body follows
188
+ the same route, notice, and self rules.
189
+
190
+ The object also carries the integer `schemaVersion`, currently `2`. Version 2
191
+ narrowed the routes, shortened `sourceNotice`, and emptied a self-anchored
192
+ frame's `replyAction`; for an Agent frame it only removed keys and changed
193
+ values. Version 2 also has the operator-input variant described below, which
194
+ a reader tells apart by its `source` fields, not by the version. The target's
195
+ helper renders the frame, so a helper started before an upgrade keeps sending
196
+ the older shape until that Agent is activated again. `schemaVersion` names
197
+ that object's shape only and moves independently of the durable envelope
198
+ version and the message store's on-disk version. A missing `schemaVersion`, or
199
+ an explicit `0`, reads as `1`: every frame written before the field existed has
200
+ that shape. A reader meeting a higher `schemaVersion` reads the fields it knows
201
+ and still surfaces the message; an unknown version is never a drop and never an
202
+ error, because a peer message that silently disappears is worse than one read
203
+ by a slightly stale reader. The append-only eviction history log record uses
204
+ the same field name, the same default, and the same higher-version rule.
205
+
206
+ ### Held while the target awaits its operator
207
+
208
+ A push frame reaching Claude while it shows its operator an `AskUserQuestion`
209
+ question, a permission dialog, or an MCP elicitation renders as pending input
210
+ and can push that widget off a narrow screen. So `agent message send` does not
211
+ call the target's helper while the target Agent's effective interaction is
212
+ `approval_required` or `input_required`. It keeps the accepted record in the
213
+ existing `held` state with reason `target-awaiting-operator`, prints
214
+
215
+ ```text
216
+ <messageRef> held target-awaiting-operator delivery resumes automatically when the target Agent's dialog closes; check projmux agent message status <messageRef>; do not resend
217
+ ```
218
+
219
+ and exits 0. Plain sends and a Claude source's `--reply-to` behave the same.
220
+ A send to a target that is not blocked is also held while that target still has
221
+ an earlier held message, so a later message never overtakes an earlier one.
222
+ Codex targets are never held. An observation older than the 30-minute
223
+ interaction freshness window reads as `unknown` and does not block.
224
+
225
+ The hold ends when the target's interaction leaves `approval_required` or
226
+ `input_required`: `UserPromptSubmit`, `Stop`, `StopFailure`, and the events that
227
+ follow an operator answer (`PostToolUse`, `PostToolUseFailure`,
228
+ `PermissionDenied`, `ElicitationResult`) move it on. The last four change the
229
+ interaction to `in_progress` only when it was blocked; otherwise they stay quiet
230
+ and write nothing. That transition starts a detached
231
+ `projmux internal agent-message-release --agent uid:<agent>` process, which
232
+ holds a per-Agent lock and delivers that Agent's held messages one at a time in
233
+ acceptance order through the ordinary push. Before each message it reads the
234
+ Registry again: a removed Agent or a route that no longer accepts the message
235
+ makes it `stale`, a passed deadline makes it `expired`, and a target that
236
+ awaits its operator again is watched as below, with the rest still held behind
237
+ that message. Every send that writes a hold also starts the same release, so a
238
+ hold never depends on a hook arriving later.
239
+
240
+ A denied permission sends no hook in current Claude Code, so the release
241
+ watches a target that awaits its operator instead of stopping. It keeps its
242
+ lock and reads the Registry and the tail of the Agent's own recorded
243
+ `transcript_path` again at a fixed interval of at most 2 seconds, for at most
244
+ 10 minutes or until the earliest deadline still ahead among the held messages,
245
+ whichever comes first. A hook that moves the interaction on lets the message go
246
+ out on the next read. A `turn_duration` line stamped after the blocking
247
+ observation, with no assistant `tool_use` after it, means that turn has ended
248
+ and its dialog is gone: the release records the `response_complete` interaction
249
+ the `Stop` hook would have written, Registry state only and only if that same
250
+ observation is still current, and delivers at once. A missing path, an
251
+ unreadable file or line, or any other doubt keeps the message held; when the
252
+ window ends the message stays held as below.
253
+
254
+ When the target's helper does not answer the release's route probe and the
255
+ target is not awaiting its operator, or when the Registry cannot be read, the
256
+ release cannot judge that message yet. It keeps its lock and judges the same
257
+ message again after 2 seconds, doubling the wait up to 30 seconds, for at most
258
+ 10 minutes or until the earliest deadline still ahead among the held
259
+ messages, whichever comes first. Each attempt checks the deadline and the
260
+ blocking interaction again, a message whose deadline has passed becomes
261
+ `expired` instead of being waited for, and later messages still wait behind
262
+ this one. When that window ends the release stops and the message stays held;
263
+ it becomes `expired` at its deadline unless a later release delivers it first.
264
+
265
+ The TTL still applies: a held message is not extended, and one still held at
266
+ its deadline becomes `expired` with reason `deadline-expired`.
267
+ `agent message status <messageRef>` answers a held message from the durable
268
+ store, never from the target's helper, which has not seen it; it prints the
269
+ same held line, `expired` once the deadline has passed, and the delivered or
270
+ other terminal result once the release has run.
271
+
272
+ ### Operator input
273
+
274
+ A durable envelope can also carry operator input: text a person wrote through
275
+ the projmux web client, which is not an Agent. It is represented, and every
276
+ reader accepts and labels it, but no command or web route creates it yet.
277
+
278
+ - **Envelope.** Operator input carries `"origin":{"kind":"operator","client":"web"}`
279
+ and no `source` key; its authority is
280
+ `{"kind":"operator","trust":"untrusted","permission":"coordination-only"}`,
281
+ which grants none of the turn, steer, or config permissions a person at the
282
+ terminal has. Its target must be a Claude Agent (refused otherwise with
283
+ `operator-origin-target-not-claude`), and it is never a reply. An Agent
284
+ message has no `origin` key at all: an absent origin is the Agent origin,
285
+ there is no explicit `"kind":"agent"` encoding, and an Agent envelope's bytes
286
+ are unchanged. A mixed shape (an origin with any source field, or no origin
287
+ and no valid source route) is invalid. The durable envelope `version` stays
288
+ `2`.
289
+ - **Replies.** Operator input has no Agent route to reverse, so a reply to it
290
+ is refused with `explicit-reply-operator-origin`, whether it comes from
291
+ `agent message send --reply-to`, the Claude reply tool, or the store.
292
+ - **Store.** The message store's on-disk version is `3` only while at least one
293
+ stored record is operator input; otherwise every write is version `2`,
294
+ including the first write after the last operator record is reclaimed. A
295
+ version `1` or `2` file that contains an `origin` is malformed. A Claude
296
+ helper started from an older build shares the store file and refuses any
297
+ version but `1` and `2` and any unknown field, and an activation keeps its
298
+ helper across an install, so this rule keeps a store of Agent messages
299
+ readable by those helpers, and keeps a rollback to such a build safe until
300
+ operator input is written.
301
+ - **Frame.** The operator frame's `source` is `{"kind":"operator","client":"web"}`
302
+ in place of an Agent route, `sourceNotice` is "Operator input that arrived
303
+ through the projmux web client; projmux did not verify the person.", and
304
+ `replyAction` is empty. The other keys are those of an Agent frame. The
305
+ helper proves only the target current, since there is no source route.
306
+ - **Readers.** The web transcript reader shows operator input as a `user`
307
+ turn with `via` `projmux-web` and no `from`, judged by the `source` fields
308
+ alone. A frame without an origin keeps its existing reading: one whose
309
+ source and target are the same Agent is the operator's own `user` turn.
310
+ `agent message status` labels operator input `source=operator (web)` in a trailing text column and prints
311
+ an `origin` object and no `source` in JSON; an Agent message's output is
312
+ unchanged. A reclaimed operator record's history line carries `origin` and
313
+ no `source`.
314
+
315
+ Why operator input needs a durable envelope at all: a Claude session has no
316
+ path for user-turn input other than typing into its terminal, and projmux
317
+ forbids sending keys on the Agent input path. The coordination push is the
318
+ only channel that reaches a Claude session without keys, so operator input
319
+ travels as an envelope and is labelled for what it is. Codex needs none of
320
+ this: it already takes web input as a native user turn.
159
321
 
160
322
  Reply egress uses only documented official `Stop.last_assistant_message` plus
161
323
  one delivered Projmux-owned pending record at the same boundary. Push ingress
@@ -171,9 +333,9 @@ Codex Agent self-claims it.
171
333
  | Ready and idle | Immediate one-frame push; model visibility is separate evidence |
172
334
  | Active tool/turn | Push never interrupts; next official human boundary makes reply correlation ambiguous |
173
335
  | Provider exit or activation restart | Old generation writes and claims zero |
174
- | Same-generation registration/helper replacement | Old incarnation writes zero; fresh exact-version qualification required |
336
+ | Same-generation registration/helper replacement | Incarnation unchanged within the same Claude session; the old helper still writes zero, enforced by the lease authority; fresh exact-version qualification still required |
175
337
  | Provider version replacement | Old qualification is cleared; unqualified writes zero |
176
- | Codex endpoint replacement | Old incarnation self-claim zero; exact new incarnation may claim |
338
+ | Codex endpoint replacement | Incarnation unchanged; the old endpoint's self-claim is still zero by the exact authority check; the exact new endpoint may claim |
177
339
 
178
340
  Message state is receipt-only. It performs no Registry Agent-interaction or
179
341
  tmux badge write, so it cannot overwrite `in_progress`, approval-required, or
@@ -212,16 +374,21 @@ independently.
212
374
  validator's verdict on preserved real frame shapes. Neither it nor L20 proves
213
375
  installed-provider compatibility, value drift (the corpus keeps top-level key
214
376
  shape with placeholder values), or acceptance of unknown frames and fields (the
215
- closed vocabulary rejects them by design). The L20 fixture emits none of the
216
- corpus's dropped side frames. Two recorded real shapes are rejected today:
377
+ closed vocabulary rejects them by design). L20 replays every corpus item whose
378
+ verdict is `drop` through the production reply-only validator: the Claude
379
+ fixture reads the corpus at run time from `PROJMUX_FAKE_CLAUDE_OBSERVED_FRAMES`,
380
+ emits each dropped side frame with its key set unchanged after init and before
381
+ its startup result, and the scenario requires the emitted names to equal the
382
+ corpus drop set. Those frames are key-shape placeholders, not model output. L20
383
+ does not emit the two recorded real shapes that are rejected today:
217
384
  `result-success-25-keys` (the result allowlist lacks `origin`) and
218
385
  `system-init-allowlist-diff` (the init allowlist lacks `memory_paths` and
219
386
  `terminal_slash_commands`). Shapes without preserved evidence are gaps, not
220
- items: `command_lifecycle` keys and values, a non-null assistant
221
- `context_management`, `system` `thinking_tokens`, hook and user `tool_result`
222
- frames, the exact real init key set, every nested value, `rate_limit_event`
223
- envelope keys beyond `type` and `rate_limit_info`, and the lost earlier shape
224
- inventories.
387
+ items, so neither the corpus nor L20 exercises them: `command_lifecycle` keys
388
+ and values, a non-null assistant `context_management`, `system`
389
+ `thinking_tokens`, hook and user `tool_result` frames, the exact real init key
390
+ set, every nested value, `rate_limit_event` envelope keys beyond `type` and
391
+ `rate_limit_info`, and the lost earlier shape inventories.
225
392
 
226
393
  Provider sources: [SessionStart and Stop hooks](https://code.claude.com/docs/en/hooks)
227
394
  and [cross-session messaging](https://code.claude.com/docs/en/cross-session-messaging).
@@ -229,17 +396,21 @@ and [cross-session messaging](https://code.claude.com/docs/en/cross-session-mess
229
396
  Once a delivered message has unresolved reply ambiguity (an overlapping human
230
397
  turn, multiple pending messages, expiry, or an uncertain write/reply outcome),
231
398
  a later idle Stop cannot restore automatic reply correlation. The helper keeps
232
- push ingress available but refuses automatic replies for that incarnation.
233
- Use the documented public recovery on the same Agent UID and qualify its new
234
- activation before resuming automatic dialogue. Source and target routes are
235
- revalidated after durable handoff and immediately before the provider write.
399
+ push ingress available but refuses automatic replies for the rest of its
400
+ lifetime. Use the documented public recovery on the same Agent UID and qualify
401
+ its new activation before resuming automatic dialogue. Source and target
402
+ routes are revalidated after durable handoff and immediately before the
403
+ provider write.
236
404
 
237
405
  The final source check also asks the existing Codex broker to verify the exact
238
406
  runtime, connection and binding lease. This read-only IPC observation binds
239
407
  nothing and sends no provider request. An older broker that does not support
240
408
  this observation refuses coordination; it is never restarted implicitly.
241
- Helper store lock contention fails immediately. Concurrent official hooks
242
- invalidate reply correlation without waiting for another hook to finish.
409
+ Helper store lock contention fails a reply commit immediately, before any
410
+ durable write. The handoff and delivery records wait up to two seconds for the
411
+ lock first, so a brief holder cannot report a delivered message as failed.
412
+ Concurrent official hooks invalidate reply correlation without waiting for
413
+ another hook to finish.
243
414
 
244
415
  The live harness begins with the benign `Reply READY.` control. The later broker
245
416
  payload contains its own exact acknowledgement request; the initial user turn