projmux 0.16.0 → 0.16.1
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/docs/agent-message-replies.md +77 -14
- package/docs/architecture.md +355 -31
- package/docs/claude-coordination-endpoints.md +35 -13
- package/docs/cli-guide.md +688 -122
- package/docs/cli.md +1114 -222
- package/docs/configuration.md +597 -48
- package/docs/globalization.md +7 -4
- package/docs/hooks.md +466 -50
- package/docs/keybindings.md +9 -5
- package/docs/npm-distribution.md +4 -0
- package/docs/operational-diagnostics.md +270 -7
- package/docs/release.md +4 -0
- package/docs/replacement-contract.md +6 -0
- package/docs/repo-layout.md +3 -0
- package/docs/resource-attribution.md +4 -0
- package/docs/session-restore.md +11 -7
- package/docs/statusbar.md +23 -19
- package/docs/testing.md +68 -18
- package/docs/theme-palette.md +6 -6
- package/docs/tmux-surface-inventory.md +13 -0
- package/docs/upgrading.md +25 -23
- package/docs/usage-tracking.md +16 -1
- package/package.json +5 -5
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
# Explicit reply recovery
|
|
2
2
|
|
|
3
3
|
An explicit Claude reply names the original request with `--reply-to`. The
|
|
4
|
-
broker preserves that request's `conversationRef`, reverses its
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
broker preserves that request's `conversationRef`, reverses its source and
|
|
5
|
+
target Agents, and retains peer, untrusted, coordination-only authority. The
|
|
6
|
+
reply goes to each Agent's current activation, and the original deadline must
|
|
7
|
+
still be current. A reply cannot extend that deadline.
|
|
8
|
+
|
|
9
|
+
Correlation follows the Agent and its provider conversation, not one
|
|
10
|
+
activation. Either Agent may have been relaunched into the same conversation
|
|
11
|
+
since the original, for example by `agent relaunch`: it now runs in a new Pane
|
|
12
|
+
under a new activation, and a reply to the earlier message still commits and
|
|
13
|
+
is delivered there. A reply to another Agent or on another provider is still
|
|
14
|
+
refused as `invalid-explicit-reply-correlation`. When one of the original's
|
|
15
|
+
Agents is now in another provider conversation (another Claude session), the
|
|
16
|
+
reply is refused with `explicit-reply-conversation-changed` and stores
|
|
17
|
+
nothing; the original cannot be answered there, so send a new message without
|
|
18
|
+
`--reply-to`. A `--dialogue-reply-only` Agent's reply tool follows the same
|
|
19
|
+
rule for the original's sender: it still permits a reply to a sender
|
|
20
|
+
relaunched into the same conversation, and denies one to a sender now in
|
|
21
|
+
another conversation. Source metadata remains an unverified routing
|
|
8
22
|
claim; an explicit reply also requires the registered provider's descendant
|
|
9
23
|
caller and any existing qualification and execution guard.
|
|
10
24
|
|
|
@@ -33,7 +47,7 @@ execution guard, use the existing bounded command without that flag:
|
|
|
33
47
|
projmux agent message send uid:<original-source-agent> --reply-to <original-request-ref> -- '<corrected reply>'
|
|
34
48
|
```
|
|
35
49
|
|
|
36
|
-
Operator input from
|
|
50
|
+
Operator input from an operator client has no Agent route to reverse, so a reply
|
|
37
51
|
to it is refused with `explicit-reply-operator-origin` and stores nothing.
|
|
38
52
|
|
|
39
53
|
Within the same provider session, a reply also commits after the Claude lease
|
|
@@ -41,10 +55,52 @@ helper was replaced, for example by compact. The new helper did not push the
|
|
|
41
55
|
original, so it reads the original from the durable store and judges it by the
|
|
42
56
|
same checks. It reads the store only while its own route is the Registry's
|
|
43
57
|
current authority for the Agent; a replaced or unregistered helper reads and
|
|
44
|
-
writes nothing
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
58
|
+
writes nothing, and is refused with `broker-reply-helper-not-current`.
|
|
59
|
+
`broker-reply-original-not-found` means the original is in neither the helper
|
|
60
|
+
nor the store.
|
|
61
|
+
|
|
62
|
+
## Reply refusal tokens
|
|
63
|
+
|
|
64
|
+
Each refusal names its cause with one token. The CLI prints it after
|
|
65
|
+
`replyTo=<ref>:`, the Claude helper returns it as the `reply-refused` reason,
|
|
66
|
+
and the store returns it as the reply conflict reason. A token asks for one
|
|
67
|
+
action, so two causes that need different actions never share one.
|
|
68
|
+
`invalid-explicit-reply-correlation` is kept for a reply that does not match
|
|
69
|
+
its original. Every refusal here except `broker-reply-outcome-unknown` comes
|
|
70
|
+
before the reply is written anywhere: no provider saw it, so following the
|
|
71
|
+
action cannot duplicate it. The retry rules below govern an attempt that was
|
|
72
|
+
stored.
|
|
73
|
+
|
|
74
|
+
| Token | Cause | Action |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| `invalid-explicit-reply-correlation` | The reply's Agents, providers, or `conversationRef` do not match the original. | Check that `--reply-to` names a message you received and that the positional Agent is its sender. |
|
|
77
|
+
| `explicit-reply-conversation-changed` | One of the original's Agents is now in another provider conversation. | Send a new message without `--reply-to`. |
|
|
78
|
+
| `invalid-explicit-reply-envelope` | The reply itself is not a valid envelope, for example a payload over the limit. | Correct the reply, for example shorten it, and send it with a fresh ref. |
|
|
79
|
+
| `explicit-reply-operator-origin` | The original is operator input from an operator client. | There is no Agent to answer; do not reply to it. |
|
|
80
|
+
| `explicit-reply-deadline-expired` | The original's deadline has passed. | Send a new message without `--reply-to`. |
|
|
81
|
+
| `explicit-reply-deadline-extended` | The reply's deadline is later than the original's. | Send the reply without a longer `--ttl`. |
|
|
82
|
+
| `explicit-reply-source-route-stale`, `explicit-reply-target-route-stale` | The replying or the answered Agent's route is not its current one, for example during a relaunch, or the helper serving the reply is not the replying Agent's. | Wait until the Agent is registered again, then send the reply. |
|
|
83
|
+
| `explicit-reply-route-stale` | The helper found a route no longer current at the moment it committed. | As above. |
|
|
84
|
+
| `broker-reply-original-not-found` | The original is in neither the helper nor the store, for example because it was reclaimed. | Send a new message without `--reply-to`. |
|
|
85
|
+
| `broker-reply-original-not-delivered` | The original never reached its target, so there is nothing to answer. | Inspect the original with `agent message status`; do not reply to it. |
|
|
86
|
+
| `broker-reply-original-without-envelope` | The helper holds the ref, but not as an Agent message it can answer. | Send a new message without `--reply-to`. |
|
|
87
|
+
| `broker-reply-helper-closed` | The replying Agent's Claude helper is shutting down, for example for a restart. | Wait for the new helper to register, then send the reply. |
|
|
88
|
+
| `broker-reply-helper-not-current` | The Registry no longer names this helper as the Agent's current one; it was replaced. | Wait for the current helper to register, then send the reply. |
|
|
89
|
+
| `broker-reply-unavailable` | The helper runs without a message broker and can commit no reply. | Relaunch the Agent so it starts a helper with one. |
|
|
90
|
+
| `broker-reply-store-unavailable` | The helper could not use the durable message store. | Check the state directory, then send the reply. |
|
|
91
|
+
| `broker-reply-store-busy` | Another writer held the message store. | Send the reply again. |
|
|
92
|
+
| `broker-reply-store-malformed` | The message store file could not be read as a store. | Inspect the store; do not resend until it reads. |
|
|
93
|
+
| `broker-reply-store-capacity` | The store is full and nothing can be reclaimed; see below. | Wait for records to be reclaimed. |
|
|
94
|
+
| `reply-already-committed` | Another reply to the original was already committed. | Inspect it with `previousRef`; do not resend. |
|
|
95
|
+
| `reply-ref-envelope-mismatch` | The same reply ref was used before with different content. | Use a fresh ref. |
|
|
96
|
+
| `broker-reply-outcome-unknown` | A commit was attempted and its result is not known. | Inspect the reply status; do not resend. |
|
|
97
|
+
|
|
98
|
+
A Claude helper that started before a build keeps that build's tokens until it
|
|
99
|
+
is replaced. An older helper therefore still reports most of these causes as
|
|
100
|
+
`invalid-explicit-reply-correlation`, and the CLI prints whichever token the
|
|
101
|
+
helper sent.
|
|
102
|
+
|
|
103
|
+
## Retries
|
|
48
104
|
|
|
49
105
|
A same-ref call returns the original immutable receipt and never pushes again.
|
|
50
106
|
Changing its payload is refused with the earlier ref and cause. A fresh ref
|
|
@@ -92,7 +148,7 @@ Each line carries the envelope's own key names:
|
|
|
92
148
|
{"schemaVersion":1,"evictedAt":"…","reason":"retention","adapter":"claude-coordination",
|
|
93
149
|
"messageRef":"…","conversationRef":"…","replyTo":"…",
|
|
94
150
|
"state":"delivered","deliveryReason":"unspecified","handoffObserved":false,
|
|
95
|
-
"acceptedAt":"…","terminalAt":"…","payloadBytes":466,
|
|
151
|
+
"acceptedAt":"…","terminalAt":"…","payloadBytes":466,"payloadSHA256":"…",
|
|
96
152
|
"source":{"agentUID":"…","provider":"…"},
|
|
97
153
|
"target":{"agentUID":"…","provider":"…"}}
|
|
98
154
|
```
|
|
@@ -105,14 +161,17 @@ the Pane and activation generation fence a live delivery, and the incarnation
|
|
|
105
161
|
follows the provider conversation; all three mean nothing once the record has
|
|
106
162
|
left the store. The envelope's `deadline` is not written. A line for operator
|
|
107
163
|
input (see [Operator input](claude-coordination-endpoints.md#operator-input)) carries
|
|
108
|
-
`"origin":{"kind":"operator","client":"
|
|
164
|
+
`"origin":{"kind":"operator","client":"<client>"}` in place of `source`; an Agent
|
|
109
165
|
message's line has no `origin`. Every other key is always present.
|
|
110
166
|
|
|
111
167
|
Lines written by earlier builds may still be in the same file. They carry full
|
|
112
168
|
routes (`paneUID`, `activationGeneration`, `incarnation`), a `deadline`, and
|
|
113
169
|
`outcomeUnknown` even when it is `false`, and they read the same way: a reader
|
|
114
170
|
ignores keys it does not expect and reads an absent key as its zero value, so
|
|
115
|
-
both shapes give the same edges, states and times.
|
|
171
|
+
both shapes give the same edges, states and times. Lines written before the
|
|
172
|
+
payload digest existed have no `payloadSHA256` key and read with an empty
|
|
173
|
+
digest, which means "not recorded", not a mismatch; earlier lines are not
|
|
174
|
+
backfilled. All of these are `schemaVersion` 1: every added key is additive.
|
|
116
175
|
|
|
117
176
|
`schemaVersion` starts at 1 and follows the same rule as the
|
|
118
177
|
coordination frame's field of that name: an absent or zero value reads as 1, and
|
|
@@ -121,8 +180,12 @@ discarding the line or failing. It is independent of the durable envelope
|
|
|
121
180
|
version and of the store file version.
|
|
122
181
|
|
|
123
182
|
The log does not keep the message body. It records `payloadBytes`, the original
|
|
124
|
-
payload length, and
|
|
125
|
-
and
|
|
183
|
+
payload length, and `payloadSHA256`, the SHA-256 of the payload's exact bytes
|
|
184
|
+
as 64 lowercase hex characters, and has no `payload` key at all: the log is
|
|
185
|
+
unbounded in time, and its intended consumers need the message graph, not its
|
|
186
|
+
text. The digest lets a consumer that holds the text elsewhere, such as a
|
|
187
|
+
transcript, prove it is the same message; the reader and its consumers compute
|
|
188
|
+
it with the same function (`PayloadSHA256` in `internal/core/agentmessage`).
|
|
126
189
|
|
|
127
190
|
The active log rotates to `history.jsonl.1` once it would pass 8 MiB, replacing
|
|
128
191
|
any earlier `history.jsonl.1`. At most two generations exist, so the log's disk
|
package/docs/architecture.md
CHANGED
|
@@ -418,27 +418,51 @@ Identity and naming:
|
|
|
418
418
|
context may duplicate and is never a selector, reservation, ownerRef, or
|
|
419
419
|
durable identity input. `metadata.labels` remains key/value classification;
|
|
420
420
|
`metadata.annotations` remains non-identifying metadata such as an AI topic.
|
|
421
|
-
- Creator provenance:
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
`projmux.io/creator-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
421
|
+
- Creator provenance: every Agent create records on the new Agent and its
|
|
422
|
+
managed Pane, in the same transaction that commits the Agent, exactly one of
|
|
423
|
+
these, decided once per create (a fan-out records the same one on every
|
|
424
|
+
Agent). `projmux.io/creator-basis` names the kind of evidence, and only the
|
|
425
|
+
keys that evidence proves are written:
|
|
426
|
+
1. `pane-chain`, with `projmux.io/creator-agent` (the creator Agent's bare
|
|
427
|
+
UID) and `projmux.io/creator-pane` (that Agent's managed Pane's bare
|
|
428
|
+
UID): an explicit `create agent` (every spelling, `--create-window`, and
|
|
429
|
+
each Agent of a fan-out) or `create window --provider` ran inside an
|
|
430
|
+
Agent's managed Pane. It is recorded only when the unmasked ambient `%N`
|
|
431
|
+
(`__PROJMUX_RUNTIME_ANCHOR_PANE`, then `TMUX_PANE`) is exactly one live
|
|
432
|
+
Registry Pane, that Pane round-trips with its owning Agent's
|
|
433
|
+
`status.paneRef`, one `display-message` confirms it on the create's own
|
|
434
|
+
app-owned socket and server pid, and the create process descends from its
|
|
435
|
+
`#{pane_pid}`. It always wins.
|
|
436
|
+
2. `explicit`, with `projmux.io/creator-agent` only: the caller declared
|
|
437
|
+
`--creator uid:<agent>` and no pane chain was recorded. The declaration
|
|
438
|
+
must name an Agent in the Registry, or the create is refused before
|
|
439
|
+
anything changes. A declaration that disagrees with a recorded pane chain
|
|
440
|
+
is dropped, and stderr gets one `creator declaration not recorded:
|
|
441
|
+
pane-chain-disagrees (--creator uid:<agent>)` line.
|
|
442
|
+
3. `operator`, with `projmux.io/creator-client` only: the create ran in
|
|
443
|
+
process for a named operator client. The UI intent creates (picker, pane
|
|
444
|
+
menu, `ai split`, launch choice, the new-Window key) record the client
|
|
445
|
+
`ui`; a client layered on projmux that runs creates in process uses the
|
|
446
|
+
`recordOperatorCreator` seam. Such a create observes no pane chain,
|
|
447
|
+
because its process environment says nothing about who asked. A
|
|
448
|
+
`--creator` on it is dropped with the token `operator-client`. There is no
|
|
449
|
+
argv spelling of this basis; `TestNoArgvPathBuildsAnOperatorCreator`
|
|
450
|
+
holds its producers. Client names follow the one rule in
|
|
451
|
+
`internal/core/operatorclient`: 1-32 bytes of lowercase ASCII letters,
|
|
452
|
+
digits, and `-`, starting with a letter.
|
|
453
|
+
4. Nothing, when there is no evidence.
|
|
454
|
+
|
|
455
|
+
A failed pane-chain check changes nothing else about the create. When
|
|
456
|
+
nothing is recorded, stderr gets one `creator not recorded: <reason>` line
|
|
457
|
+
only if the ambient Pane is a live Agent Pane but a later check fails; an
|
|
435
458
|
ambient Pane that is malformed, unregistered, or a Window-owned shell is
|
|
436
|
-
silent. These keys are
|
|
437
|
-
authentication, like a message `--source
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
commands run under the app-server, not below the Pane's
|
|
441
|
-
|
|
459
|
+
silent. The creator Agent is never the created Agent. These keys are
|
|
460
|
+
provenance, not authentication, like a message `--source`: no permission,
|
|
461
|
+
route, or selector reads them, and `explicit` can be forged, which is what
|
|
462
|
+
its basis says. An absent key does not mean a human created the Agent: a
|
|
463
|
+
Codex Agent's own commands run under the app-server, not below the Pane's
|
|
464
|
+
process, so they record nothing unless they declare `--creator`. Nothing
|
|
465
|
+
backfills older Agents.
|
|
442
466
|
|
|
443
467
|
Root lifecycle:
|
|
444
468
|
|
|
@@ -532,6 +556,62 @@ Termination evidence transport:
|
|
|
532
556
|
receipt is the post-delete diagnostic: source, classification, observed time,
|
|
533
557
|
Pane/Agent uid, generation, and wait status only. No command, pane content,
|
|
534
558
|
prompt, transcript, or provider payload is recorded.
|
|
559
|
+
- Deletion records: every explicit deletion that commits its Registry
|
|
560
|
+
transaction appends exactly one JSON line to
|
|
561
|
+
`<state>/deletion-records.jsonl`, next to `termination-receipts.jsonl` and the
|
|
562
|
+
Registry it changed (`${XDG_STATE_HOME:-$HOME/.local/state}/projmux` by default). The writers are
|
|
563
|
+
`delete window|pane|agent`, `unregister project` and its deprecated alias
|
|
564
|
+
`delete project`, and `prune agent|project --yes`. The line is
|
|
565
|
+
`{"schemaVersion":1,"at","operation","operationID","via","actor":{"agentUID","paneUID","basis"},"targets":[{"kind","uid","name"}],"affected":[{"kind","uid","name","action"}]}`,
|
|
566
|
+
where `at` is RFC 3339 UTC; `operation` is one of `delete-window`,
|
|
567
|
+
`delete-pane`, `delete-agent`, `unregister-project`, `delete-project`,
|
|
568
|
+
`prune-agent`, `prune-project` (the record's own spelling; the receipt line a
|
|
569
|
+
delete prints spells it `delete.pane` and so on, a separate surface that is
|
|
570
|
+
not unified with it); `operationID` is the delete's intentional termination
|
|
571
|
+
receipt id, or one minted the same way for unregister and prune;
|
|
572
|
+
and `via` is `cli`, `ui` (the generated Pane and Window delete keys), or
|
|
573
|
+
`prune`. `targets` are what the operator named; `affected` is every
|
|
574
|
+
Project, ControlSession, Window, Agent, and Pane present before the commit
|
|
575
|
+
and absent after it (targets included), ordered by kind and then uid, each
|
|
576
|
+
with action `deleted`. No explicit deletion route removes a ControlSession
|
|
577
|
+
today (only a rolled-back transaction does, and that writes no record), so
|
|
578
|
+
that kind is listed for completeness of the two root kinds.
|
|
579
|
+
- The actor is the same pane-chain judgment creator provenance uses, run
|
|
580
|
+
against the pre-commit Registry inside the delete's own transaction, so a
|
|
581
|
+
delete that removes its own Agent still names it. `basis` is `pane-chain`
|
|
582
|
+
when it succeeds, creation's skip token otherwise (`anchor-invalid`,
|
|
583
|
+
`anchor-pane-unregistered`, `anchor-pane-ambiguous`, `caller-pane-not-agent`,
|
|
584
|
+
`pane-ref-mismatch`, `anchor-server-unproven`, `anchor-query-failed`,
|
|
585
|
+
`anchor-server-mismatch`, `anchor-pane-mismatch`,
|
|
586
|
+
`process-chain-unobservable`, `not-pane-descendant`), and empty when there
|
|
587
|
+
was no ambient Pane. The server step is the delete's own: one
|
|
588
|
+
`display-message -t %N` through the route the delete addresses must answer
|
|
589
|
+
with the `$TMUX` socket and server pid, the same `%N`, and the Registry
|
|
590
|
+
Pane's mirrored uid. `prune` has no socket flags and judges on the inherited
|
|
591
|
+
`$TMUX`; `unregister project` judges on its `--socket`/`--socket-path` or
|
|
592
|
+
`$TMUX`. A `ui` deletion is never judged and records an empty actor.
|
|
593
|
+
- A deletion a detached daemon runs (a Codex app-server command, say) is never
|
|
594
|
+
recorded as an Agent's. With no ambient Pane in its environment the actor is
|
|
595
|
+
empty with an empty basis; with an Agent Pane's inherited `TMUX`/`TMUX_PANE`
|
|
596
|
+
but a process that does not descend from that Pane, `basis` is
|
|
597
|
+
`not-pane-descendant` and both uids are empty.
|
|
598
|
+
- **An empty actor does not mean a human ran the deletion.** The record is
|
|
599
|
+
provenance, not authentication.
|
|
600
|
+
- The file is appended with one framed `O_APPEND` write and `fsync`, like the
|
|
601
|
+
termination journal, and is never truncated, rotated, or read back by
|
|
602
|
+
projmux. It is written after the commit and before the result is printed or
|
|
603
|
+
a self-targeted kill is queued. A record that cannot be written never fails
|
|
604
|
+
or changes the delete: stdout and the exit code are unchanged and stderr gets
|
|
605
|
+
exactly one `deletion not recorded: <token>` line, where token is
|
|
606
|
+
`state-dir-unavailable`, `append-failed`, or `operation-id-unavailable`.
|
|
607
|
+
`--dry-run`, a refusal, a declined confirmation, and a failed commit write
|
|
608
|
+
nothing.
|
|
609
|
+
- Non-guarantees: automatic lifecycle teardown (exit reconciliation), internal
|
|
610
|
+
row cleanup (resume handoff, topology replay, initial shell retirement,
|
|
611
|
+
rollback, Project fresh start), and the supervisor's process-exit path write
|
|
612
|
+
no deletion record. A delete issued by a Codex Agent's command runs under the
|
|
613
|
+
app-server rather than below the Pane's process and records
|
|
614
|
+
`not-pane-descendant`. There is no retention policy and no reader CLI.
|
|
535
615
|
- The supervisor resolves its state paths from the pane's own inherited
|
|
536
616
|
environment, which is the tmux **server's** environment rather than the
|
|
537
617
|
environment of the CLI call that created the pane. That is the correct
|
|
@@ -663,9 +743,21 @@ Agent provider session ref:
|
|
|
663
743
|
- Codex's turn id is deliberately **not** stored. A turn addresses one turn
|
|
664
744
|
inside the conversation and changes on every hook event, so it is not a
|
|
665
745
|
pointer to the conversation.
|
|
666
|
-
- Transcript **paths** are recorded as the hook reported them
|
|
667
|
-
|
|
668
|
-
|
|
746
|
+
- Transcript **paths** are recorded as the hook reported them, and no
|
|
747
|
+
transcript content is read to fill the ref. The Registry, hook ingest, and
|
|
748
|
+
Agent resume paths (`agent resume`, `agent relaunch`, and the topology replay
|
|
749
|
+
below) never read provider config files, and they read transcript
|
|
750
|
+
**contents** only in these documented places:
|
|
751
|
+
- The three bounded tail readers that
|
|
752
|
+
[hooks.md](hooks.md#claude-code-hook-ingest) lists and scopes: `Stop` hook
|
|
753
|
+
ingest, the held coordination message release, and the Agent's pane
|
|
754
|
+
supervisor. Each reads at most the last 256 KiB of one Claude transcript,
|
|
755
|
+
and none writes what it read to the Registry. `Stop` keeps the last
|
|
756
|
+
assistant text as its notification row's text; hooks.md says where that
|
|
757
|
+
text goes.
|
|
758
|
+
- The explicit, user-run `agent sessions backfill` (see "Agent session
|
|
759
|
+
history (Claude)" below), which reads Claude transcript contents read-only
|
|
760
|
+
and only to attribute past sessions from delivered coordination frames.
|
|
669
761
|
- The field is additive inside `schemaVersion: 1`. It is an optional pointer
|
|
670
762
|
with `omitempty`, so a registry written before it existed decodes with a nil
|
|
671
763
|
ref, validates, and re-encodes byte-identically. Bumping the envelope would
|
|
@@ -687,6 +779,214 @@ Agent provider session ref:
|
|
|
687
779
|
hook whose provider contradicts the Agent's `spec.provider` is refused with
|
|
688
780
|
zero mutations.
|
|
689
781
|
|
|
782
|
+
Agent session history (Claude and Codex):
|
|
783
|
+
|
|
784
|
+
- `status.sessionRef` holds one conversation and is overwritten when an Agent
|
|
785
|
+
moves to another one. The conversations a Claude or Codex Agent left are kept in
|
|
786
|
+
an append-only file outside the Registry,
|
|
787
|
+
`<state>/agent-session-history.jsonl` (next to `deletion-records.jsonl`;
|
|
788
|
+
`${XDG_STATE_HOME:-$HOME/.local/state}/projmux` by default). The Registry
|
|
789
|
+
schema and its version are unchanged.
|
|
790
|
+
- One line is appended each time a committed Registry write binds or replaces
|
|
791
|
+
a Claude session id or Codex thread id. Re-observing the same conversation,
|
|
792
|
+
including a same-thread Codex resume or endpoint handover, appends nothing.
|
|
793
|
+
The writers are a closed set of four functions: `persistAgentSessionRef`
|
|
794
|
+
(hook ingest) and `persistManagedAgentInteractionWithActivationPolicy`
|
|
795
|
+
(managed-Agent interaction commit) in `internal/app/agent_session_ref.go`,
|
|
796
|
+
`openIntentAgent` (resume-picker create and intent native Codex create) in
|
|
797
|
+
`internal/app/create_intent.go`, and `createAgent` (native Codex fresh
|
|
798
|
+
create) in `internal/app/create_agent.go`.
|
|
799
|
+
`TestClaudeSessionRefWritersRecordHistory` pins the callers of
|
|
800
|
+
`RecordAgentSessionRef`, and
|
|
801
|
+
`TestSessionHistoryObservedRowWritersCarryAffiliation` pins the set of four,
|
|
802
|
+
that each builds its row with `sessionhistory.ObservedRecordFor` (directly or
|
|
803
|
+
through `claudeSessionHistoryRecord`) from its transaction's `working`
|
|
804
|
+
Registry, that each reaches a post-commit append helper, and that no other
|
|
805
|
+
code builds an `observed` row with `RecordFor`. The append runs after the
|
|
806
|
+
commit and never fails its caller: a hook logs one `session-history` line to
|
|
807
|
+
`ai-ingest.log`, a create prints one
|
|
808
|
+
`agent session history not recorded: append-failed` line on stderr.
|
|
809
|
+
- Each line is `{"agentUID","provider","sessionId","transcriptPath","observedAt","source"}`:
|
|
810
|
+
`provider` is `claude` or `codex`, `observedAt` is the ref's RFC 3339 UTC observation
|
|
811
|
+
time, and `transcriptPath` is the path the Claude hook reported (empty for
|
|
812
|
+
Codex; never read by the
|
|
813
|
+
writers or by `agent sessions list`). `source` is `observed` for every line
|
|
814
|
+
a writer appends; the read side adds `current` for the Registry's ref; the
|
|
815
|
+
only producer of `estimated` is `agent sessions backfill` (below), whose
|
|
816
|
+
lines add one key, `lastRecordAt`. Lines are one framed `O_APPEND` write under an
|
|
817
|
+
exclusive `flock`, then `fsync`; the file is `0600` in a `0700` directory.
|
|
818
|
+
Readers skip and count an unparsable line.
|
|
819
|
+
- An `observed` line also carries the Agent's affiliation in three trailing
|
|
820
|
+
keys, `projectUID`, `windowUID`, and `agentName`, so a session stays
|
|
821
|
+
attributable after its Agent is deleted from the Registry. The writer
|
|
822
|
+
resolves them inside the same transaction, from that transaction's working
|
|
823
|
+
Registry (`sessionhistory.ResolveAffiliation`), link by link: `agentName`
|
|
824
|
+
when the Agent exists, `windowUID` when its ownerRef is an existing Window,
|
|
825
|
+
`projectUID` when that Window's ownerRef is an existing Project. A key whose
|
|
826
|
+
link does not resolve is omitted; nothing is guessed, and a chain that does
|
|
827
|
+
not resolve never withholds the line or fails the writer. `current` and
|
|
828
|
+
`estimated` rows carry none of the three. When `agent sessions list` merges
|
|
829
|
+
a conversation seen more than once, each of the three is the non-empty value
|
|
830
|
+
of the latest row that carries one.
|
|
831
|
+
- A line `agent sessions attribute` (below) writes adds a fourth key after
|
|
832
|
+
them, `affiliationBasis: "registry"`: its affiliation is the one the current
|
|
833
|
+
Registry gave the Agent when the user ran it, not its writer's transaction.
|
|
834
|
+
No other writer sets the key; an `observed` line without it is recorded at
|
|
835
|
+
write time. Merging carries `affiliationBasis` with `projectUID`: it is the
|
|
836
|
+
basis of the row that supplied the kept `projectUID`.
|
|
837
|
+
- `projmux agent sessions list <agent-ref> [-o json]` and the Go read function
|
|
838
|
+
`sessionhistory.List(stateDir, agent)` return the same rows: history joined
|
|
839
|
+
with the Registry's current ref, one row per `(agentUID, provider, sessionId)` in
|
|
840
|
+
`observedAt` order. A conversation seen more than once keeps its latest
|
|
841
|
+
observation time and transcript path, and is `current` when the Registry
|
|
842
|
+
names it; otherwise it is `observed` if any of its rows is, and `estimated`
|
|
843
|
+
only when every row is, so a backfilled estimate never hides an observation.
|
|
844
|
+
The JSON envelope adds `agentName` and `corruptLines`.
|
|
845
|
+
- `projmux agent sessions project <project-ref> [-o json]` and the Go read
|
|
846
|
+
function `sessionhistory.ListProject(stateDir, registry, projectUID)` list
|
|
847
|
+
the sessions attributed to one exact Project (resolved like any Project
|
|
848
|
+
ref). It is read-only. A Project no longer in the Registry is queried by its
|
|
849
|
+
exact `uid:<uid>` ref, where `<uid>` has exactly the shape projmux mints for
|
|
850
|
+
a Project: the listing then comes from the history rows alone,
|
|
851
|
+
`projectName` is `""`, and the table names the Project `project/uid:<uid>`.
|
|
852
|
+
A name ref, or a `uid:` ref of another kind or shape, resolves against the
|
|
853
|
+
Registry only and fails as before when it matches nothing.
|
|
854
|
+
- Input: every `claude` or `codex` line of the history file, plus one
|
|
855
|
+
`current` row for each Registry Agent with a supported provider.
|
|
856
|
+
- Attribution, per row: a row with a `projectUID` is attributed with its
|
|
857
|
+
own `projectUID`, `windowUID`, and `agentName`, never re-resolved, with
|
|
858
|
+
basis `registry` when its `affiliationBasis` is `registry` (it holds after
|
|
859
|
+
the Agent is deleted, with `inRegistry: false`) and `recorded` otherwise;
|
|
860
|
+
otherwise, a row whose Agent is in the current Registry with a complete
|
|
861
|
+
Agent -> Window -> Project chain is attributed with basis `registry`,
|
|
862
|
+
using the current values; any other row is unattributed. Rows are never
|
|
863
|
+
attributed from deletion records, Registry backups, a transcript's folder
|
|
864
|
+
or cwd, or time proximity.
|
|
865
|
+
- A session is `(provider, sessionId)`. A session none of whose rows is
|
|
866
|
+
attributed counts in `unattributed`; one whose attributed rows name two or
|
|
867
|
+
more Projects counts in `ambiguous` and is listed under none; one naming
|
|
868
|
+
exactly the requested Project is listed. Both counts, and `corruptLines`,
|
|
869
|
+
are over the whole history, not per Project.
|
|
870
|
+
- Each listed session has `provider`, `sessionId`, `transcriptPath`,
|
|
871
|
+
`observedAt`, `lastRecordAt` (when any row has one), and `source`, merged
|
|
872
|
+
over its attributed rows with the `agent sessions list` rules, and
|
|
873
|
+
`agents`: one entry per distinct Agent of those rows, in first-appearance
|
|
874
|
+
order (history file order, then the current rows), with `agentUID`,
|
|
875
|
+
`agentName`, `windowUID`, `inRegistry` (the Agent exists in the current
|
|
876
|
+
Registry), and `basis` (`recorded` wins when an Agent has both). Sessions
|
|
877
|
+
are ordered by `observedAt`.
|
|
878
|
+
- JSON: `{"projectUID","projectName","sessions","unattributed","ambiguous","corruptLines"}`;
|
|
879
|
+
`sessions` is `[]` when empty. The table prints
|
|
880
|
+
`project/<name> has no recorded sessions` when empty, marks an Agent no
|
|
881
|
+
longer in the Registry `(deleted)`, and prints one stderr line for each
|
|
882
|
+
non-zero `corruptLines`, `unattributed`, and `ambiguous` count.
|
|
883
|
+
- `projmux agent sessions backfill [--dry-run] [-o json]` recovers a subset of
|
|
884
|
+
the conversations from before this history existed. It is the one explicit,
|
|
885
|
+
user-run reader of transcript contents. Apart from the bounded tail readers
|
|
886
|
+
that [hooks.md](hooks.md#claude-code-hook-ingest) lists (`Stop` hook ingest,
|
|
887
|
+
the held coordination message release, and the pane supervisor), the
|
|
888
|
+
Registry, hook ingest, Agent resume, and `agent sessions list` never read a
|
|
889
|
+
transcript, and none of those tail readers adds a row to this history.
|
|
890
|
+
- Input: the top-level Claude transcripts
|
|
891
|
+
`${CLAUDE_CONFIG_DIR:-$HOME/.claude}/projects/<project>/<session>.jsonl`
|
|
892
|
+
(a blank `CLAUDE_CONFIG_DIR` is unset). Only regular files exactly one
|
|
893
|
+
directory deep are scanned: symlinks, directories, and deeper files such
|
|
894
|
+
as `<session>/subagents/*.jsonl` are not. Files are opened read-only and
|
|
895
|
+
never written; a missing projects directory scans nothing.
|
|
896
|
+
- Attribution: only `type: "user"` records that are not `isSidechain: true`
|
|
897
|
+
count, and within them only a string `message.content` or its
|
|
898
|
+
`type: "text"` blocks; tool results, assistant records, attachments, and
|
|
899
|
+
queue operations can quote a frame and are ignored. At each occurrence of
|
|
900
|
+
`{"kind":"projmux-coordination"` in that text, one JSON value is decoded;
|
|
901
|
+
a frame with that `kind` and a non-empty `target.agentUID` names its
|
|
902
|
+
target, and an occurrence that does not decode is skipped and counted.
|
|
903
|
+
A transcript naming exactly one distinct target Agent is attributed to
|
|
904
|
+
it; none is `noFrame`, two or more is `ambiguous`, and neither is ever
|
|
905
|
+
resolved from the folder name, the working directory, or time proximity.
|
|
906
|
+
- Meaning of `estimated`: the session received a delivered projmux message
|
|
907
|
+
addressed to that Agent. It is a subset of past sessions -- a session that
|
|
908
|
+
never received a message is not recovered -- and an attribution, not an
|
|
909
|
+
observation. `sessionId` is the file name, `transcriptPath` the absolute
|
|
910
|
+
file path, `observedAt` the transcript's first record `timestamp`, and
|
|
911
|
+
`lastRecordAt` its last one (both over every parseable record, RFC 3339
|
|
912
|
+
UTC). A candidate with no parseable timestamp is `unreadable`.
|
|
913
|
+
- Precedence and idempotency: a session already `observed` in the history
|
|
914
|
+
or `current` in the Registry for any Agent is `skippedObserved`; one
|
|
915
|
+
already `estimated` is `alreadyEstimated`; only the rest are appended, as
|
|
916
|
+
framed lines like every other writer's. The run holds the history file's
|
|
917
|
+
exclusive `flock` across the read, the check, and the append (not the
|
|
918
|
+
transcript scan), so concurrent runs cannot add a session twice and a
|
|
919
|
+
second run appends nothing.
|
|
920
|
+
- Report: `scanned`, `attributed`, `alreadyEstimated`, `skippedObserved`,
|
|
921
|
+
`ambiguous`, `noFrame`, `unreadable` (each transcript lands in exactly one
|
|
922
|
+
of these six), `malformedLines` (transcript lines that are not JSON
|
|
923
|
+
objects, skipped), `malformedFrames`, plus `dryRun`, `projectsDir`,
|
|
924
|
+
`historyPath`, and `rows` (the attributed rows; `[]` when none).
|
|
925
|
+
`--dry-run` reports exactly what a real run would append and writes
|
|
926
|
+
nothing, not even the state directory.
|
|
927
|
+
- Removing the estimated rows: with no projmux command running that could
|
|
928
|
+
write the history, keep every other line and swap the file in atomically
|
|
929
|
+
(the blank lines of the framed format are simply dropped):
|
|
930
|
+
|
|
931
|
+
```sh
|
|
932
|
+
h="${XDG_STATE_HOME:-$HOME/.local/state}/projmux/agent-session-history.jsonl"
|
|
933
|
+
jq -c 'select(.source != "estimated")' "$h" > "$h.tmp" && chmod 600 "$h.tmp" && mv "$h.tmp" "$h"
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
A line `jq` cannot parse stops it with an error before the `mv`; readers
|
|
937
|
+
already skip such lines, so remove or repair it first.
|
|
938
|
+
- `projmux agent sessions attribute [--dry-run] [-o json]` persists what the
|
|
939
|
+
current Registry proves about the conversations whose history carries no
|
|
940
|
+
Project, so `agent sessions project` keeps listing them after their Agent
|
|
941
|
+
is deleted. It reads only the history file and the current Registry: never
|
|
942
|
+
deletion records, Registry backups, or transcripts. It persists only what
|
|
943
|
+
the current Registry proves, and never restores an Agent that is no longer
|
|
944
|
+
in it.
|
|
945
|
+
- Input: every `(agentUID, provider, sessionId)` of the file's `claude` and
|
|
946
|
+
`codex` lines, in file order, then each Registry Agent's current ref
|
|
947
|
+
(supported providers only) whose key the file does not name.
|
|
948
|
+
- Precedence: a key with a line that has a `projectUID` is
|
|
949
|
+
`alreadyAttributed`; a key whose Agent is not in the Registry, or whose
|
|
950
|
+
Agent -> Window -> Project chain does not resolve, is `unresolved`; both
|
|
951
|
+
are left untouched. Every other key is `attributed`: one line is appended
|
|
952
|
+
for it, with `observedAt`, `transcriptPath`, and `lastRecordAt` merged
|
|
953
|
+
over its lines and its current ref with the `agent sessions list` rules;
|
|
954
|
+
`source` the highest-ranked source of its lines, or `observed` for a key
|
|
955
|
+
only the Registry names (`current` is never written); `projectUID`,
|
|
956
|
+
`windowUID`, and `agentName` from the current Registry; and
|
|
957
|
+
`affiliationBasis: "registry"`.
|
|
958
|
+
- Report: `scanned` (`attributed` + `alreadyAttributed` + `unresolved`),
|
|
959
|
+
`attributed`, `alreadyAttributed`, `unresolved`, `corruptLines`, plus
|
|
960
|
+
`dryRun`, `historyPath`, and `rows` (the attributed lines; `[]` when
|
|
961
|
+
none).
|
|
962
|
+
- Idempotency and locking: like backfill, a run holds the history file's
|
|
963
|
+
exclusive `flock` across the read, the classification, and one write of
|
|
964
|
+
every framed line, and releases it before the `fsync`. A second run
|
|
965
|
+
appends nothing, because every key it attributed now has a `projectUID`.
|
|
966
|
+
Existing lines are never rewritten. A run with nothing to append creates
|
|
967
|
+
nothing, and `--dry-run` reads without the lock, writes nothing, and
|
|
968
|
+
reports exactly what a real run would append.
|
|
969
|
+
- Undoing it, the same way as removing estimated rows:
|
|
970
|
+
|
|
971
|
+
```sh
|
|
972
|
+
h="${XDG_STATE_HOME:-$HOME/.local/state}/projmux/agent-session-history.jsonl"
|
|
973
|
+
jq -c 'select(.affiliationBasis != "registry")' "$h" > "$h.tmp" && chmod 600 "$h.tmp" && mv "$h.tmp" "$h"
|
|
974
|
+
```
|
|
975
|
+
- Non-guarantees: no session end time is recorded for observed rows;
|
|
976
|
+
conversations from before this history existed appear only as the `current`
|
|
977
|
+
row unless `agent sessions backfill` attributes them; Claude and Codex
|
|
978
|
+
conversation changes are recorded, Antigravity's are not; a line edited or
|
|
979
|
+
deleted by hand is not recovered; the history is kept after the Agent is deleted; the Registry
|
|
980
|
+
commit and the append are not atomic, so a crash between them loses that one
|
|
981
|
+
line. There is no retention policy. Lines written before the affiliation
|
|
982
|
+
keys existed carry none, so once their Agent is deleted they count only in
|
|
983
|
+
`unattributed` unless `agent sessions attribute` persisted their
|
|
984
|
+
affiliation while the Agent was still in the Registry; `agentName` is a
|
|
985
|
+
write-time snapshot and does not follow a later rename; a deleted Project
|
|
986
|
+
can no longer be named by name, only by its exact `uid:` ref, and then only
|
|
987
|
+
the history rows that carry its `projectUID` are listed; `agent sessions
|
|
988
|
+
project` computes no token totals.
|
|
989
|
+
|
|
690
990
|
Agent launch argv (workspace / task boundary):
|
|
691
991
|
|
|
692
992
|
- One Agent launch hands the provider CLI two independent things in a single
|
|
@@ -1040,6 +1340,10 @@ Resolved resource graph (`internal/core/resourcegraph`):
|
|
|
1040
1340
|
first and the inherited `$TMUX` socket path second. There is no implicit
|
|
1041
1341
|
default-server probe: with no transport the graph is a Registry-only snapshot
|
|
1042
1342
|
whose runtime answers are all `unknown`, and a sibling socket is never read.
|
|
1343
|
+
The Registry views `get` and `describe` add one rung after `$TMUX`: they
|
|
1344
|
+
have no socket flag, so outside tmux they observe the app socket
|
|
1345
|
+
`-L projmux` (see *Runtime observation and resource status*). That is still
|
|
1346
|
+
one exact server, never the default one.
|
|
1043
1347
|
- **Bounded and pure.** One observation costs one option probe plus three list
|
|
1044
1348
|
queries whatever the size of the server, is memoized for the invocation rather
|
|
1045
1349
|
than cached with a TTL — closing a pane must make the *next* command report it
|
|
@@ -1132,14 +1436,25 @@ Runtime observation and resource status:
|
|
|
1132
1436
|
live tmux object still mirrors its `@projmux_window_uid` /
|
|
1133
1437
|
`@projmux_pane_uid`. A registry object bound to nothing live is an **orphan**,
|
|
1134
1438
|
and an orphan is not live.
|
|
1135
|
-
- `selector.ObservedStatus(missingRoot, bound)` is the **single**
|
|
1136
|
-
rule in the codebase, and every kind goes through it.
|
|
1137
|
-
everything, then `bound`
|
|
1138
|
-
|
|
1139
|
-
|
|
1439
|
+
- `selector.ObservedStatus(missingRoot, bound, unobserved)` is the **single**
|
|
1440
|
+
derivation rule in the codebase, and every kind goes through it.
|
|
1441
|
+
`missing-root` outranks everything, then `bound` is `live`, then a resource
|
|
1442
|
+
whose observation could not be taken is `unknown`, and only a readable
|
|
1443
|
+
observation with nothing bound is `offline` -- the same precedence the
|
|
1444
|
+
resource graph applies. The MissingRoot precedence contract is unchanged, and
|
|
1445
|
+
it now applies to a Window or Pane whose owning Project lost its root even
|
|
1446
|
+
while tmux is still running them.
|
|
1447
|
+
- `get` and `describe` build their observation from the resolved resource
|
|
1448
|
+
graph, so a row's STATUS and the ACTIONS `registryview` offers for it come
|
|
1449
|
+
from one judgement. An `unknown` row offers only `delete`: it was not seen
|
|
1450
|
+
live, so `open` has nothing to move to, and it was not seen absent, so
|
|
1451
|
+
`start`/`resume` could target something already running.
|
|
1140
1452
|
- A **Project** is the one kind whose runtime object is a tmux *session*, which
|
|
1141
|
-
has no `@projmux` uid of its own
|
|
1142
|
-
|
|
1453
|
+
has no `@projmux` uid of its own. On `get` and `describe` its status is the
|
|
1454
|
+
graph's observation of that session; an identity-only resolver that took no
|
|
1455
|
+
observation (`selector.New`) still reads the stored `status.session`
|
|
1456
|
+
projection (see *Resource metadata model* for its writers) and never reports
|
|
1457
|
+
`unknown`.
|
|
1143
1458
|
- An **Agent** owns no tmux object of its own — there is no `@projmux_agent_uid`
|
|
1144
1459
|
and there must not be one, because an Agent outlives the managed Pane it is
|
|
1145
1460
|
bound to. Its runtime object is **that managed Pane**, named by
|
|
@@ -1166,6 +1481,15 @@ Runtime observation and resource status:
|
|
|
1166
1481
|
exactly that. It is also not a per-route reconcile, because the read verbs
|
|
1167
1482
|
load the registry read-only and must never materialize
|
|
1168
1483
|
`<state>/projmux/metadata/`.
|
|
1484
|
+
- **The server a view observes** is the one its caller is attached to: the
|
|
1485
|
+
inherited absolute `$TMUX` socket, even when that is not the app server.
|
|
1486
|
+
With no `$TMUX` -- another terminal, an IDE, a script -- `get` and `describe`
|
|
1487
|
+
observe the app socket `-L projmux`, the server `create` and
|
|
1488
|
+
`start project` run resources on, so a view outside tmux renders the same
|
|
1489
|
+
rows as the same view inside the app server. An app socket with no server
|
|
1490
|
+
behind it is observed as an absent server: everything reads `offline`, which
|
|
1491
|
+
is what is running there. `get runtime` and `reconcile resources` keep their
|
|
1492
|
+
own rule (flag or `$TMUX`, otherwise no transport).
|
|
1169
1493
|
- A failed inventory query yields an **empty** observation, never a fallback to
|
|
1170
1494
|
a stored value. Empty can only downgrade a resource to offline; it can never
|
|
1171
1495
|
invent a live one, and "nothing is live" is the truthful answer for a machine
|
|
@@ -1651,7 +1975,7 @@ a discovery input *and* the only record that a directory mattered. They are five
|
|
|
1651
1975
|
separate authorities now, and the boundaries are the point.
|
|
1652
1976
|
|
|
1653
1977
|
- **Workdirs and project roots are scan roots.** `PROJMUX_MANAGED_ROOTS`,
|
|
1654
|
-
`PROJMUX_PROJDIR` and
|
|
1978
|
+
`PROJMUX_PROJDIR` and `$HOME/.config/projmux/workdirs` name directories to look
|
|
1655
1979
|
inside. Looking inside a directory registers nothing. On Windows they are
|
|
1656
1980
|
OS-native paths and stay OS-native paths; nothing normalizes them into identity.
|
|
1657
1981
|
- **A discovered child is an unregistered candidate.** It is a filesystem fact
|