@brutalsystems/muster 0.8.3 → 0.9.2
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/CANONICAL_ID.md +201 -25
- package/CONTRACT_PROVENANCE.md +182 -0
- package/README.md +13 -7
- package/dist/naming.js +27 -7
- package/dist/run.js +7 -2
- package/package.json +1 -1
- package/test/fixtures/canonical-id.json +498 -69
package/CANONICAL_ID.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Tin Can — Address Format
|
|
2
2
|
|
|
3
|
-
> **Normative for `@brutalsystems/tincan`
|
|
3
|
+
> **Normative for `@brutalsystems/tincan` 1.9.2.**
|
|
4
4
|
>
|
|
5
5
|
> This describes what Tin Can does today, precisely enough for another tool to
|
|
6
6
|
> produce addresses Tin Can will resolve. It is a write-down of shipped
|
|
@@ -11,22 +11,56 @@
|
|
|
11
11
|
> asserted by `test/canonical-id.test.ts`. Copy that fixture rather than
|
|
12
12
|
> reimplementing from prose.
|
|
13
13
|
>
|
|
14
|
+
> Its `tincan_version` field records the release the expectations were verified
|
|
15
|
+
> against, and is bumped every release. **A version difference alone is not
|
|
16
|
+
> drift** — compare the file's contents, or its hash, to tell whether the format
|
|
17
|
+
> actually moved.
|
|
18
|
+
>
|
|
14
19
|
> **The implementation is authoritative.** If this document and Tin Can
|
|
15
20
|
> disagree, Tin Can is right and this document has a bug. The fixture is the
|
|
16
21
|
> regression net that keeps them together.
|
|
17
22
|
|
|
18
23
|
## The pieces
|
|
19
24
|
|
|
20
|
-
An address is built from
|
|
25
|
+
An address is built from three inputs, one of which is usually absent:
|
|
21
26
|
|
|
22
27
|
| Runtime | Name source | Id source |
|
|
23
28
|
|---|---|---|
|
|
24
29
|
| `codex` | thread title, or absent | `thread_id` |
|
|
25
30
|
| `claude-code` | session name from the registry | `session_id` |
|
|
31
|
+
| `opencode` | session slug | `session_id` |
|
|
26
32
|
|
|
27
33
|
Neither name is guaranteed unique, and neither is stable: **a name belongs to a
|
|
28
34
|
process and dies with it.** Key durable records on the id, never on the address.
|
|
29
35
|
|
|
36
|
+
`peers` exposes that id on every peer — `thread_id` for Codex, `session_id` for
|
|
37
|
+
Claude Code and for opencode — so a caller never has to derive it from an
|
|
38
|
+
address.
|
|
39
|
+
|
|
40
|
+
The third input is the **machine**, and it is absent for every peer on this
|
|
41
|
+
one. See [Machine](#machine).
|
|
42
|
+
|
|
43
|
+
## Machine
|
|
44
|
+
|
|
45
|
+
A peer on another machine carries `@<machine>` at the end of its address. A
|
|
46
|
+
peer on this machine carries nothing.
|
|
47
|
+
|
|
48
|
+
auth-refactor a session here
|
|
49
|
+
auth-refactor@m4pro a session on another computer
|
|
50
|
+
|
|
51
|
+
Absent rather than a literal like `localhost`, for two reasons. Every address
|
|
52
|
+
anyone types today keeps working and keeps meaning exactly what it meant. And
|
|
53
|
+
it makes the dangerous direction the explicit one: reaching another computer
|
|
54
|
+
requires saying so.
|
|
55
|
+
|
|
56
|
+
**A bare address never resolves to a remote peer.** Resolution filters by
|
|
57
|
+
whether the input contains `@` before matching anything, so an address with no
|
|
58
|
+
machine can only match a local session — including during prefix matching,
|
|
59
|
+
which is where it would otherwise slip through. A message meant for a local
|
|
60
|
+
peer must not leave the machine because a local session happened to exit.
|
|
61
|
+
|
|
62
|
+
The machine name is slugified by the same rule as a peer name.
|
|
63
|
+
|
|
30
64
|
## Slugify
|
|
31
65
|
|
|
32
66
|
Applied to the runtime-supplied name.
|
|
@@ -59,56 +93,145 @@ is a timestamp, so every thread live on a machine at the same time shares its
|
|
|
59
93
|
first characters. A leading suffix disambiguates nothing. This has been
|
|
60
94
|
specified wrongly before — verify against the fixture.
|
|
61
95
|
|
|
96
|
+
The same holds for opencode, for the same reason by a different route. An
|
|
97
|
+
opencode session id is `ses_` followed by roughly twelve hex characters of
|
|
98
|
+
descending timestamp, then roughly fourteen base62 (`[0-9a-zA-Z]`) random
|
|
99
|
+
characters. Stripping to `[0-9a-f]` and taking the last three lands in that
|
|
100
|
+
random tail, not the timestamp — exactly as it does for a Codex UUIDv7 or a
|
|
101
|
+
Claude Code UUIDv4. `suffixOf('ses_f41a2b3c4ffeExampleSess01Z')` → `'e01'`.
|
|
102
|
+
|
|
62
103
|
- `01a0b9b4-a33e-7ab1-80a0-bb715504a0fb` → `0fb`
|
|
63
104
|
- `5af69d42-2214-41d9-b13f-9c3177eb60ce` → `0ce`
|
|
105
|
+
- `ses_f41a2b3c4ffeExampleSess01Z` → `e01`
|
|
64
106
|
- Uppercase hex is kept, then lowered: `...ABC` → `abc`
|
|
65
107
|
- Fewer than three hex characters yields a shorter suffix: `z-9` → `9`
|
|
66
108
|
- No hex at all yields an empty suffix: `zzzz` → `""`
|
|
67
109
|
|
|
110
|
+
Two caveats specific to opencode's id shape:
|
|
111
|
+
|
|
112
|
+
- **The random tail is base62, not hex, so filtering it down to `[0-9a-f]`
|
|
113
|
+
skews the alphabet.** `a`–`f` are reachable from two source characters each
|
|
114
|
+
(upper- and lower-case), while `0`–`9` are reachable from only one, so the
|
|
115
|
+
surviving hex digits are not uniform. Collision odds for the suffix
|
|
116
|
+
therefore sit slightly above the nominal 1 in 4096 for a uniform 3-hex-digit
|
|
117
|
+
space. Worth stating; not worth changing — see
|
|
118
|
+
[Changing this format](#changing-this-format).
|
|
119
|
+
- **An opencode id can never produce the empty-suffix defect.** The fixed
|
|
120
|
+
`ses_` + hex-timestamp prefix guarantees at least three hex characters are
|
|
121
|
+
always present, so unlike an adversarial or degenerate id (`zzzz` above),
|
|
122
|
+
`suffixOf` never returns `""` for a real opencode session id.
|
|
123
|
+
|
|
68
124
|
## Forms
|
|
69
125
|
|
|
70
126
|
Three forms exist. Two are emitted; all three are accepted as input.
|
|
71
127
|
|
|
72
128
|
| Form | Shape | Emitted |
|
|
73
129
|
|---|---|---|
|
|
74
|
-
| **Display** | `auth-refactor` | by `peers`, when the slug does not collide |
|
|
75
|
-
| **Qualified** | `auth-refactor.7f3` | by `peers`, only on collision |
|
|
76
|
-
| **Canonical** | `codex:auth-refactor.
|
|
130
|
+
| **Display** | `auth-refactor`, `auth-refactor@m4pro` | by `peers`, when the slug does not collide |
|
|
131
|
+
| **Qualified** | `auth-refactor.7f3`, `auth-refactor.7f3@m4pro` | by `peers`, only on collision |
|
|
132
|
+
| **Canonical** | `codex:auth-refactor.01a0b9b4-a33e-7ab1-80a0-bb715504a0fb` | as `canonical_id`, always |
|
|
133
|
+
|
|
134
|
+
**The canonical form carries the whole durable id**, not the three-character
|
|
135
|
+
suffix. That is what makes it unique — see [Known defects](#known-defects),
|
|
136
|
+
where its non-uniqueness used to be recorded as a defect preserved on purpose.
|
|
77
137
|
|
|
78
138
|
The separator is `.` so that an address needs no shell quoting.
|
|
79
139
|
|
|
80
140
|
`canonical_id` is always fully qualified, whether or not there is a collision.
|
|
81
141
|
It is the form to record in logs and to pass between tools.
|
|
82
142
|
|
|
143
|
+
**Do not recover the suffix from a canonical id by string surgery.** Taking the
|
|
144
|
+
last dot-separated segment gave the three-character suffix before 1.0.0 and
|
|
145
|
+
gives the whole durable id now — `01a0b9b4-a33e-7ab1-80a0-bb715504a0fb`, not
|
|
146
|
+
`0fb`. A reimplementation that does this builds qualified addresses that look
|
|
147
|
+
right and match nothing, and the failure surfaces as `unknown`, which names a
|
|
148
|
+
missing peer rather than a malformed address. Reported by muster, where it made
|
|
149
|
+
every `slug.suffix` address fail silently until a test was written for it.
|
|
150
|
+
|
|
151
|
+
Derive the suffix from the **uuid**, with `suffixOf` — the last three hex
|
|
152
|
+
characters of the id with non-hex removed. The canonical form is an address to
|
|
153
|
+
pass along whole, not a record to parse fields out of.
|
|
154
|
+
|
|
83
155
|
## Collision
|
|
84
156
|
|
|
85
157
|
A peer is suffixed in its **display** form when either:
|
|
86
158
|
|
|
87
|
-
- another peer in the same listing has the same slug
|
|
159
|
+
- another peer in the same listing has the same slug **on the same machine**, or
|
|
88
160
|
- the peer has no name (`rawName === null`), which is always suffixed.
|
|
89
161
|
|
|
162
|
+
Collisions are counted per machine. The same slug on two computers is already
|
|
163
|
+
two different addresses, so suffixing both would add noise to distinguish
|
|
164
|
+
things that were never confusable.
|
|
165
|
+
|
|
90
166
|
Otherwise the display form is the bare slug. Only colliding peers are suffixed;
|
|
91
167
|
peers that do not collide keep their bare names in the same listing.
|
|
92
168
|
|
|
169
|
+
**Adding a runtime can change an existing peer's display.** `assignNames`
|
|
170
|
+
computes collisions across the *whole* returned list, regardless of runtime.
|
|
171
|
+
So a peer that displayed as a bare slug before opencode support existed can
|
|
172
|
+
start displaying suffixed the moment an opencode peer (or any peer) slugging
|
|
173
|
+
to the same string appears in the same listing — a Codex peer `foo` and an
|
|
174
|
+
opencode session slugging to `foo` both flip from bare `foo` to qualified
|
|
175
|
+
`foo.<suffix>` (see the fixture's "cross-runtime collision" case). This is
|
|
176
|
+
inherent to the existing rule, not new behaviour introduced by opencode
|
|
177
|
+
support, and it is correct: the two peers are genuinely indistinguishable by
|
|
178
|
+
slug. It is called out here because Muster consumes `display`, and this
|
|
179
|
+
should be an expected consequence of adding a runtime, not something its
|
|
180
|
+
maintainer discovers by surprise.
|
|
181
|
+
|
|
93
182
|
## Resolution
|
|
94
183
|
|
|
95
|
-
Input is trimmed and lowercased, then matched in two passes
|
|
184
|
+
Input is trimmed and lowercased, then matched in two passes against the peer
|
|
185
|
+
listing, with one host-local check between them.
|
|
96
186
|
|
|
97
187
|
**Pass 1 — exact.** Matches if the input equals the peer's display form, its
|
|
98
188
|
qualified form, or its canonical id. One match resolves. More than one is
|
|
99
189
|
refused as ambiguous.
|
|
100
190
|
|
|
101
|
-
**
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
191
|
+
**Self check**, only if pass 1 found nothing. If the input names the session
|
|
192
|
+
Tin Can is itself running in, the result is refused as `self`. See
|
|
193
|
+
[Self is not a peer](#self-is-not-a-peer) — this pass is **not** part of the
|
|
194
|
+
fixture contract and a second implementation is not expected to reproduce it.
|
|
195
|
+
|
|
196
|
+
**Pass 2 — prefix**, only if pass 1 and the self check found nothing. Matches
|
|
197
|
+
if the peer's slug or its qualified form *starts with* the input. One match
|
|
198
|
+
resolves. More than one is refused as ambiguous.
|
|
199
|
+
|
|
200
|
+
If nothing matches, the result is refused as unknown.
|
|
201
|
+
|
|
202
|
+
### Self is not a peer
|
|
203
|
+
|
|
204
|
+
A host never lists the session it is running in, so an address naming that
|
|
205
|
+
session reaches pass 2 with nothing to match — or, worse, matches *something
|
|
206
|
+
else*. The check sits between the passes rather than before them, and the
|
|
207
|
+
order is load-bearing in both directions:
|
|
208
|
+
|
|
209
|
+
- **Below pass 1**, so an exact peer name still wins. A peer genuinely called
|
|
210
|
+
`review` receives `review` even when the host is called `review-tools`,
|
|
211
|
+
which the self check matches by prefix.
|
|
212
|
+
- **Above pass 2**, because that pass resolves on a *single* match. A host
|
|
213
|
+
that can only recognise itself by name — a Claude Code host carries no
|
|
214
|
+
session id of its own and falls back to its working directory's basename —
|
|
215
|
+
whose fallback name prefixed exactly one peer would otherwise resolve to
|
|
216
|
+
that peer and deliver. Where the listing spans config dirs, that peer
|
|
217
|
+
belongs to a different account: a note addressed to yourself, delivered to
|
|
218
|
+
a stranger, reported as sent.
|
|
219
|
+
|
|
220
|
+
**This is deliberately outside the fixture.** Every case in
|
|
221
|
+
`test/fixtures/canonical-id.json` resolves with no host identity supplied, and
|
|
222
|
+
all ten behave identically with and without this pass. "Self" is a property of
|
|
223
|
+
the process doing the resolving, not of the address or the listing, so a tool
|
|
224
|
+
that consumes addresses — Muster launches sessions and hands back peer records
|
|
225
|
+
for Tin Can to resolve — has no self to check and nothing to implement. The
|
|
226
|
+
address format is unchanged; what changed is what one particular resolver does
|
|
227
|
+
with its own name.
|
|
106
228
|
|
|
107
229
|
- **Case-insensitive** throughout. `AUTH-REFACTOR.7F3` resolves.
|
|
108
230
|
- **Input is trimmed.** Surrounding whitespace is ignored.
|
|
109
231
|
- **There is no minimum prefix length.** A one-character prefix resolves if it
|
|
110
|
-
is unambiguous. The empty string is a prefix of everything
|
|
111
|
-
|
|
232
|
+
is unambiguous. The empty string is a prefix of everything, and a complete
|
|
233
|
+
name can be a prefix of a longer complete name — see
|
|
234
|
+
[known defects](#known-defects), 3 and 4.
|
|
112
235
|
- **No tie-breaking.** Ambiguity is always refused, never guessed.
|
|
113
236
|
|
|
114
237
|
### Refusal shape
|
|
@@ -117,26 +240,32 @@ If neither pass matches, the result is refused as unknown.
|
|
|
117
240
|
|---|---|
|
|
118
241
|
| `ambiguous` | every matching peer, in **qualified** form |
|
|
119
242
|
| `unknown` | every peer in the listing, in **display** form |
|
|
243
|
+
| `self` | every peer the address *also* prefixed, in **qualified** form — so the caller is told which full name to type instead of guessing one exists |
|
|
120
244
|
|
|
121
245
|
## Known defects
|
|
122
246
|
|
|
123
|
-
Preserved
|
|
247
|
+
Preserved by decision, not by oversight. They are in the fixture so
|
|
124
248
|
that a second implementation matches Tin Can exactly, including where Tin Can is
|
|
125
249
|
wrong. **Do not "correct" them independently** — that produces addresses that
|
|
126
250
|
resolve in one tool and fail silently in the other.
|
|
127
251
|
|
|
128
|
-
**1. `canonical_id` is not unique
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
252
|
+
**1. `canonical_id` is not unique. — FIXED in 1.0.0.**
|
|
253
|
+
|
|
254
|
+
It used to carry the three-character suffix, so two peers whose slugs matched
|
|
255
|
+
*and* whose ids shared their last three hex characters produced the same
|
|
256
|
+
`canonical_id`. Tin Can then refused to resolve either, listing two identical
|
|
257
|
+
candidates — a refusal that told the caller to disambiguate using a string that
|
|
258
|
+
did not disambiguate. Both peers were unaddressable until one exited. Roughly 1
|
|
259
|
+
in 4096 given a slug collision, and slug collisions are not rare: Codex titles
|
|
260
|
+
threads from their first prompt.
|
|
133
261
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
started from similar prompts collide readily.
|
|
262
|
+
The canonical form now carries the whole durable id, which makes a collision
|
|
263
|
+
impossible rather than merely unlikely. Both peers stay addressable.
|
|
137
264
|
|
|
138
|
-
**
|
|
139
|
-
|
|
265
|
+
**The short forms still collide**, and still should: `display` and `qualified`
|
|
266
|
+
are for humans, and two sessions genuinely sharing a name are genuinely
|
|
267
|
+
ambiguous. The difference is that there is now an unambiguous form to fall back
|
|
268
|
+
to. `canonical_id` is a usable key.
|
|
140
269
|
|
|
141
270
|
**2. A name that slugifies to empty is inconsistent with an unnamed peer.**
|
|
142
271
|
Alone in a listing, a peer named `???` displays as `thread` with no suffix,
|
|
@@ -149,6 +278,45 @@ string is a prefix of every slug, `resolve("")` against a single-peer listing
|
|
|
149
278
|
returns that peer rather than refusing. With two or more peers it is refused as
|
|
150
279
|
ambiguous, as expected.
|
|
151
280
|
|
|
281
|
+
**4. A full name that is a prefix of a longer full name resolves to the longer
|
|
282
|
+
one once the exactly-named session is gone.** While a session named `muster` is
|
|
283
|
+
listed, `muster` is an exact hit and pass 2 is never reached. The moment it
|
|
284
|
+
exits, `muster` is nobody's name and becomes merely a prefix of its neighbour
|
|
285
|
+
`muster-b1` — the only match, so it resolves, and the message is delivered to a
|
|
286
|
+
session the caller did not mean and reported as sent. Observed live between two
|
|
287
|
+
sessions; the send was a reply, so `reply_misrouted` caught it, but a new
|
|
288
|
+
message would have gone through.
|
|
289
|
+
|
|
290
|
+
This is not an exotic collision. It happens whenever a short name and a longer
|
|
291
|
+
one built on it coexist — `muster` / `muster-b1`, `ferry` / `ferry-9b`.
|
|
292
|
+
|
|
293
|
+
**It is kept because it cannot be fixed from the listing alone.** The hazard is
|
|
294
|
+
indistinguishable from the behaviour directly above it in this document: `auth`
|
|
295
|
+
→ `auth-refactor` and `muster` → `muster-b1` are the same shape, the same single
|
|
296
|
+
match, and in both cases the input is nobody's current name. The only fact that
|
|
297
|
+
separates them is that a session named exactly `muster` *existed and has gone*,
|
|
298
|
+
and a peer listing cannot carry that fact, because the session's absence is the
|
|
299
|
+
whole premise. Any rule that refuses the second refuses the first: removing the
|
|
300
|
+
prefix pass is verifiable in one edit, and it fails
|
|
301
|
+
`resolve > unambiguous prefix resolves` alongside the defect's own case.
|
|
302
|
+
|
|
303
|
+
**The form that cannot misroute is `canonical_id`.** Pass 2 matches against the
|
|
304
|
+
slug and the qualified form, never against `canonicalId`, so a canonical id can
|
|
305
|
+
never prefix-match a stranger — it resolves to the session it names or refuses
|
|
306
|
+
as unknown:
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
muster gone , resolve("muster") -> OK, muster-b1
|
|
310
|
+
muster gone , resolve("claude-code:muster.18ddaa21-…") -> REFUSED unknown
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
A loud refusal is the outcome a caller wants here. **When you hold a session's
|
|
314
|
+
durable id, address it by `canonical_id` rather than by its bare name** — a
|
|
315
|
+
short name is for a human typing, not for a program that already knows exactly
|
|
316
|
+
which session it means. `expect_id` does not substitute: it is compared after
|
|
317
|
+
resolution and only ever rejects, so it guards the send without selecting the
|
|
318
|
+
recipient.
|
|
319
|
+
|
|
152
320
|
## Changing this format
|
|
153
321
|
|
|
154
322
|
The address format is a contract between Tin Can and tools built against it. A
|
|
@@ -156,6 +324,14 @@ change to slugify, suffix derivation, collision handling, or resolution produces
|
|
|
156
324
|
addresses that resolve in one tool and fail silently in the other — no error,
|
|
157
325
|
just a message delivered nowhere.
|
|
158
326
|
|
|
327
|
+
**Who is built against this today:**
|
|
328
|
+
[Muster](https://github.com/BrutalSystems/muster) launches agent sessions and
|
|
329
|
+
returns a peer record Tin Can must resolve. It does not import Tin Can — the two
|
|
330
|
+
agree on this document and on `test/fixtures/canonical-id.json`, which Muster
|
|
331
|
+
copies verbatim. So a change here is not local to this repo, and the person
|
|
332
|
+
making it is responsible for coordinating the release, not for discovering
|
|
333
|
+
afterwards that the other tool disagreed.
|
|
334
|
+
|
|
159
335
|
So:
|
|
160
336
|
|
|
161
337
|
- **A change to any behaviour described here is a breaking change**, and ships
|
package/CONTRACT_PROVENANCE.md
CHANGED
|
@@ -299,3 +299,185 @@ same-runtime exclusion, or Tin Can state. Its count assertion applies only to
|
|
|
299
299
|
the matching canonical ID in the isolated test environment. Naming hashes do
|
|
300
300
|
not cover listing membership, self-exclusion, or state semantics. A future
|
|
301
301
|
upgrade must test these behaviors separately where relied upon.
|
|
302
|
+
|
|
303
|
+
## Re-sync to Tin Can 1.5.1 — 2026-09-24
|
|
304
|
+
|
|
305
|
+
Both contract copies were replaced wholesale from Tin Can at tag `v1.5.1`,
|
|
306
|
+
commit `b62ca6a719131dbd1b43875fa47604c1c17cd9fb`. This ends the 0.1.0-vintage
|
|
307
|
+
freeze described above: Muster had never been verified against any Tin Can 1.x,
|
|
308
|
+
and the contract suite passed only because `RELEASING.md` pinned it to 0.6.4.
|
|
309
|
+
|
|
310
|
+
| File | SHA-256 |
|
|
311
|
+
| --- | --- |
|
|
312
|
+
| `CANONICAL_ID.md` | `7ec8857238262ff49277488a515f830078df25451cff11dad69c0b6aa820dfa5` |
|
|
313
|
+
| `test/fixtures/canonical-id.json` | `dde6ce24bf259fc0a44685f08fc724232968cd9ec3c839802cd2b37de702a85a` |
|
|
314
|
+
|
|
315
|
+
The fixture carries `tincan_version: 1.5.1` and 39 cases: 8 slugify, 7 suffix,
|
|
316
|
+
12 assignment, 12 resolution. These hashes are the same numbers asserted by
|
|
317
|
+
`test/naming.test.ts` ("contract copies remain frozen"); the two sites must
|
|
318
|
+
always agree, and the earlier divergence between this document's recorded
|
|
319
|
+
fixture hash and the test's is what #48 reports.
|
|
320
|
+
|
|
321
|
+
**Why the tag and not HEAD.** Tin Can's HEAD was one commit past `v1.5.1` when
|
|
322
|
+
this was copied, and the three contract-relevant files were byte-identical
|
|
323
|
+
between the two — confirmed from both sides. HEAD has since gained a 40th
|
|
324
|
+
resolve case, but the fixture's `tincan_version` still reads 1.5.1 because
|
|
325
|
+
those version sites only move on a cut. Copying HEAD and recording it as 1.5.1
|
|
326
|
+
would manufacture exactly the provenance confusion this document exists to
|
|
327
|
+
prevent, so the tag is the acquisition point and the 40th case is a follow-up.
|
|
328
|
+
|
|
329
|
+
**What the 40th case is, and why it matters here.** It freezes a known defect
|
|
330
|
+
Muster reported upstream and Tin Can confirmed as
|
|
331
|
+
[tincan#39](https://github.com/BrutalSystems/tincan/issues/39): a full name
|
|
332
|
+
that is a prefix of a longer full name resolves to the longer one once the
|
|
333
|
+
exactly-named session has gone — `muster` lands on `muster-b1`. Tin Can chose
|
|
334
|
+
to document it rather than change resolution, because no rule can separate it
|
|
335
|
+
from fixture case [4] (`auth` resolving to `auth-refactor`) using anything a
|
|
336
|
+
peer listing carries; an implementation of the stricter rule failed both the
|
|
337
|
+
new case and the existing one. Muster reimplements resolution, so it inherits
|
|
338
|
+
the defect by construction. Adopt the frozen case when it is cut; do not
|
|
339
|
+
special-case it locally, because a local rule is drift by another name.
|
|
340
|
+
|
|
341
|
+
`canonical_id` is the form that cannot misroute — resolution matches it
|
|
342
|
+
exactly and never by prefix. Prefer it over a bare name wherever a durable id
|
|
343
|
+
is already in hand.
|
|
344
|
+
|
|
345
|
+
## Superseded same day: re-sync to Tin Can 1.6.0 — 2026-09-24
|
|
346
|
+
|
|
347
|
+
The 1.5.1 acquisition above **never shipped**. Its release, 0.9.0, failed in CI
|
|
348
|
+
before publishing (see below), and Tin Can cut 1.6.0 while that was being
|
|
349
|
+
fixed. The copies are now from tag `v1.6.0`, commit
|
|
350
|
+
`3d66ceaa718a098bb9e962282d901aff03dd47ff`.
|
|
351
|
+
|
|
352
|
+
| File | SHA-256 |
|
|
353
|
+
| --- | --- |
|
|
354
|
+
| `CANONICAL_ID.md` | `06dd8afe83fa641183fb44b2625dbc684c2bed13c9ac5c8f51a6c1206a00da3e` |
|
|
355
|
+
| `test/fixtures/canonical-id.json` | `a574fe291a515d920aab07f29629100caadb4fc8d8e8130a0851559bf2384aac` |
|
|
356
|
+
|
|
357
|
+
`tincan_version: 1.6.0`, 40 cases: 8 slugify, 7 suffix, 12 assignment, 13
|
|
358
|
+
resolution. This removes the compromise the 1.5.1 section describes — the
|
|
359
|
+
fixture is now labelled for the release that contains it, so the acquisition
|
|
360
|
+
point and the version metadata agree, and no follow-up is outstanding.
|
|
361
|
+
|
|
362
|
+
The 40th case freezes the prefix-resolution defect
|
|
363
|
+
([tincan#39](https://github.com/BrutalSystems/tincan/issues/39)) that Muster
|
|
364
|
+
reported. `naming.ts`, written against the 39-case fixture, passes it
|
|
365
|
+
unchanged — no expected value moved, so this is not an address-format change.
|
|
366
|
+
|
|
367
|
+
**The case was verified to have teeth rather than assumed to.** A case that has
|
|
368
|
+
never failed has not been shown to test anything. Implementing the stricter
|
|
369
|
+
rule it documents — refuse a prefix that ends at a segment boundary — fails the
|
|
370
|
+
new case *and* `resolve: unambiguous prefix resolves` together, in this copy.
|
|
371
|
+
That pair failing together is the argument for Tin Can's decision to document
|
|
372
|
+
the defect rather than change resolution, and it reproduces here.
|
|
373
|
+
|
|
374
|
+
## Re-sync to Tin Can 1.7.0 — 2026-09-24
|
|
375
|
+
|
|
376
|
+
The copies are now from tag `v1.7.0`, commit
|
|
377
|
+
`f2aaeb20ea278c229afe955db59f69301fc6626f`.
|
|
378
|
+
|
|
379
|
+
| File | SHA-256 |
|
|
380
|
+
| --- | --- |
|
|
381
|
+
| `CANONICAL_ID.md` | `8d5e93894e5c43cc854330f7b64ebbdcc450649cfa910c8b4015edf2559125e7` |
|
|
382
|
+
| `test/fixtures/canonical-id.json` | `1db118b22484271932e7cde33c1c80b469563f18f2961532d6910d79ce1e4c3b` |
|
|
383
|
+
|
|
384
|
+
`tincan_version: 1.7.0`, still 40 cases: 8 slugify, 7 suffix, 12 assignment, 13
|
|
385
|
+
resolution. **No expected value moved and no case was added.** Diffed
|
|
386
|
+
`v1.6.0..v1.7.0` upstream before copying: `src/naming.ts` does not appear in the
|
|
387
|
+
diff at all, the fixture's only changed line is its `tincan_version`, and
|
|
388
|
+
`CANONICAL_ID.md` gains fourteen lines of warning with no normative change. So
|
|
389
|
+
`naming.ts` is untouched here and the contract suite is expected to stay 4/4
|
|
390
|
+
without it — this is a re-label plus a doc, not an address-format change.
|
|
391
|
+
|
|
392
|
+
What the doc gains is the hazard this repo hit and reported: recovering the
|
|
393
|
+
three-hex suffix by taking a canonical id's last dot-separated segment returned
|
|
394
|
+
the suffix before 1.0.0 and returns the whole uuid now, so every qualified
|
|
395
|
+
`slug.suffix` address resolves as `unknown` — a missing peer, not a malformed
|
|
396
|
+
address, which is why it stayed quiet here until a test was written for it.
|
|
397
|
+
Tin Can names Muster as where it was found. The rule is to derive the suffix
|
|
398
|
+
from the uuid via `suffixOf` and otherwise pass a canonical id along whole
|
|
399
|
+
rather than parse fields out of it.
|
|
400
|
+
|
|
401
|
+
**A re-sync touches three sites, not two.** Both Tin Can pins —
|
|
402
|
+
`RELEASING.md` and `.github/workflows/publish.yml` — plus the SHA-256 drift
|
|
403
|
+
hashes in `test/naming.test.ts` ("contract copies remain frozen"), which must
|
|
404
|
+
move whenever a copied file changes. The two pins are the ones handed down in
|
|
405
|
+
conversation; the hashes were found by grepping for the old version and the old
|
|
406
|
+
digests rather than by working from that list, so grep the repo instead of
|
|
407
|
+
trusting an enumeration. That advice earned itself twice over: this very
|
|
408
|
+
enumeration missed `README.md`, which carried a literal baseline of its own —
|
|
409
|
+
recorded below.
|
|
410
|
+
|
|
411
|
+
Moving only the first pin is what made v0.9.0 an inert tag that published
|
|
412
|
+
nothing. `test/workflow-npm-pin.test.ts` now fails when the two pins disagree,
|
|
413
|
+
or when the `publish.yml` pin and the fixture's `tincan_version` disagree, and
|
|
414
|
+
that guard was re-confirmed by mutation here rather than assumed: reverting
|
|
415
|
+
`publish.yml` alone to 1.6.0 fails both assertions, and restoring it passes
|
|
416
|
+
them. A stale drift hash fails `npm run check` on its own.
|
|
417
|
+
|
|
418
|
+
### The pin sites became one derived value
|
|
419
|
+
|
|
420
|
+
Recording the fix rather than only the incident. The two Tin Can pins no longer
|
|
421
|
+
restate a version; both compute it from this fixture's `tincan_version`. That
|
|
422
|
+
field is the single place saying which Tin Can the vendored copies describe, so
|
|
423
|
+
a pin can no longer lag it — the v0.9.0 failure class is closed structurally
|
|
424
|
+
instead of guarded after the fact.
|
|
425
|
+
|
|
426
|
+
The guard that replaced the old agreement tests asserts the derivation
|
|
427
|
+
expression itself, not the word `tincan_version`. The first version of it
|
|
428
|
+
checked for the word and **passed against a `publish.yml` whose command had
|
|
429
|
+
been mutated to `ver="1.6.0"`**, because the prose explaining the derivation
|
|
430
|
+
contains that word too. It was caught by running the mutation rather than by
|
|
431
|
+
reading the test. A guard that cannot fail is the thing this document exists to
|
|
432
|
+
prevent, so the failure is written down next to the fix.
|
|
433
|
+
|
|
434
|
+
### README.md was a fourth site, and had already gone stale
|
|
435
|
+
|
|
436
|
+
The enumeration above missed it. `README.md`'s Verification section named its own
|
|
437
|
+
baseline — **Tin Can 0.6.4** in prose, and `@brutalsystems/tincan@0.6.4` in a
|
|
438
|
+
copy-paste command — and had drifted unnoticed while the vendored copies moved to
|
|
439
|
+
1.6.0 and then 1.9.2. Nothing failed, because it gates nothing: it is
|
|
440
|
+
instructions for a human, so the cost was silence rather than an inert tag.
|
|
441
|
+
|
|
442
|
+
That makes it worse than a failing pin, not better. Anyone following the README
|
|
443
|
+
installed a Tin Can four minors behind the one the fixture describes and watched
|
|
444
|
+
the contract suite pass 4/4 — which reads as confirmation that the contract holds
|
|
445
|
+
at a version nobody vendored.
|
|
446
|
+
|
|
447
|
+
Fixed the same way as the other two sites: the command derives `ver` from the
|
|
448
|
+
fixture, and the prose points at `tincan_version` rather than naming a release.
|
|
449
|
+
`test/workflow-npm-pin.test.ts` now includes `README.md` in the derivation loop,
|
|
450
|
+
and adds one assertion that its Verification section names no Tin Can version in
|
|
451
|
+
prose at all, because prose restates a pin as easily as a command does. Both were
|
|
452
|
+
watched failing against the stale README before anything was changed — the
|
|
453
|
+
literal-pin assertion on the command, and `expected 'Tin Can 0.6.4' to be
|
|
454
|
+
undefined` on the prose — and the derived command was then extracted from the
|
|
455
|
+
README and run end to end, installing 1.9.2 and passing the contract suite 4/4.
|
|
456
|
+
|
|
457
|
+
The prose assertion is scoped to `## Verification` on purpose. Elsewhere the
|
|
458
|
+
README records what was observed at a particular Tin Can — "Tin Can 0.6.0
|
|
459
|
+
delivers over v1, so nothing here depends on the v2 path" — which is a historical
|
|
460
|
+
claim and stays pinned to its version deliberately. A whole-file regex would fail
|
|
461
|
+
on those and teach the next person to delete the guard.
|
|
462
|
+
|
|
463
|
+
## Re-sync to Tin Can 1.9.2 — 2026-09-25
|
|
464
|
+
|
|
465
|
+
Copies are from tag `v1.9.2`.
|
|
466
|
+
|
|
467
|
+
| File | SHA-256 |
|
|
468
|
+
| --- | --- |
|
|
469
|
+
| `CANONICAL_ID.md` | `6e0dde03154956a480ae59f4d31b727ecc5a9e735c185fed6e4445f2574263a9` |
|
|
470
|
+
| `test/fixtures/canonical-id.json` | `b91406e068f6bec18629a4d2b9e91bc4d2e5a0487e4b6b4534fd5ba37c7f617a` |
|
|
471
|
+
|
|
472
|
+
`tincan_version: 1.9.2`, still 40 cases. Diffing `v1.7.0..v1.9.2` upstream, the
|
|
473
|
+
ONLY change across all three contract files is the version string in two lines —
|
|
474
|
+
`naming.ts` does not appear, and no fixture case or expected value moved. 1.8.0
|
|
475
|
+
(log chaining, arrival record), 1.9.0 (version reporting in `peers`), 1.9.1 and
|
|
476
|
+
1.9.2 (registration drift) do not touch the address format. Verified here rather
|
|
477
|
+
than taken from upstream's word: they explicitly said not to treat their summary
|
|
478
|
+
as a re-copy signal.
|
|
479
|
+
|
|
480
|
+
**This re-sync needed no pin edit.** The pins derive from `tincan_version`, so
|
|
481
|
+
updating the fixture moved CI on its own — the first cut where the failure class
|
|
482
|
+
that produced v0.9.0 had nothing to act on. The drift hashes still moved, which
|
|
483
|
+
is the third site behaving exactly as documented above.
|
package/README.md
CHANGED
|
@@ -1024,19 +1024,25 @@ The ordinary suite requires tmux, node-pty, POSIX `ps`/`lsof`, and Python 3 for
|
|
|
1024
1024
|
real file locks in the fake Codex executable. It uses isolated runtime homes and
|
|
1025
1025
|
no model APIs. It skips the four explicitly invoked compatibility cases.
|
|
1026
1026
|
|
|
1027
|
-
The verified contract baseline is
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1027
|
+
The verified contract baseline is the Tin Can named by `tincan_version` in the
|
|
1028
|
+
frozen fixture, `test/fixtures/canonical-id.json`. That field is the single place
|
|
1029
|
+
recording which release the vendored copies describe, and both `RELEASING.md` and
|
|
1030
|
+
`publish.yml` derive their pin from it. The suite spawns an installed binary as
|
|
1031
|
+
an MCP subprocess, compares durable IDs, and verifies delivery to fake runtimes
|
|
1032
|
+
through both terminal hosts. A missing binary or missing durable field fails the
|
|
1033
|
+
test. For reproducible verification, install that exact release outside Muster —
|
|
1034
|
+
derived here too, rather than typed, so this command cannot name a release the
|
|
1035
|
+
fixture has moved past:
|
|
1031
1036
|
|
|
1032
1037
|
```sh
|
|
1033
1038
|
contract_dir=$(mktemp -d)
|
|
1034
|
-
|
|
1039
|
+
ver=$(node -p "require('./test/fixtures/canonical-id.json').tincan_version")
|
|
1040
|
+
npm install --prefix "$contract_dir" --no-save "@brutalsystems/tincan@$ver"
|
|
1035
1041
|
MUSTER_TINCAN_BIN="$contract_dir/node_modules/.bin/tincan" npm run test:contract
|
|
1036
1042
|
```
|
|
1037
1043
|
|
|
1038
|
-
Verification uses
|
|
1039
|
-
|
|
1044
|
+
Verification uses that published release installed in an isolated temporary
|
|
1045
|
+
directory. No Tin Can source edits are made.
|
|
1040
1046
|
Fixture version metadata alone does not indicate address-format drift: compare
|
|
1041
1047
|
the case arrays. Those arrays cover naming only, not peer-list membership or
|
|
1042
1048
|
state semantics. Upgrades require separate compatibility verification; passing
|
package/dist/naming.js
CHANGED
|
@@ -19,32 +19,52 @@ export function suffixOf(uuid) {
|
|
|
19
19
|
.slice(-3)
|
|
20
20
|
.toLowerCase();
|
|
21
21
|
}
|
|
22
|
+
/** `@machine`, or nothing at all for a peer on this machine. */
|
|
23
|
+
function at(machine) {
|
|
24
|
+
return machine === undefined || machine === "" ? "" : `@${slugify(machine)}`;
|
|
25
|
+
}
|
|
22
26
|
export function assignNames(peers) {
|
|
23
27
|
const slugs = peers.map((peer) => {
|
|
24
28
|
const s = peer.rawName ? slugify(peer.rawName) : "";
|
|
25
29
|
return s.length > 0 ? s : "thread";
|
|
26
30
|
});
|
|
31
|
+
// Collisions are counted per machine. The same slug on two computers is two
|
|
32
|
+
// different addresses already, so suffixing both would add noise to
|
|
33
|
+
// distinguish things that were never confusable.
|
|
27
34
|
const counts = new Map();
|
|
28
|
-
for (const s of slugs)
|
|
29
|
-
|
|
35
|
+
for (const [i, s] of slugs.entries()) {
|
|
36
|
+
const key = `${s}${at(peers[i].machine)}`;
|
|
37
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
38
|
+
}
|
|
30
39
|
return peers.map((peer, i) => {
|
|
31
40
|
const slug = slugs[i];
|
|
32
41
|
const suffix = suffixOf(peer.uuid);
|
|
33
|
-
const
|
|
42
|
+
const host = at(peer.machine);
|
|
43
|
+
const qualified = `${slug}.${suffix}${host}`;
|
|
34
44
|
// An unnamed thread has no name to stand on, so it always carries its suffix.
|
|
35
|
-
const collides = (counts.get(slug) ?? 0) > 1 || peer.rawName === null;
|
|
45
|
+
const collides = (counts.get(`${slug}${host}`) ?? 0) > 1 || peer.rawName === null;
|
|
36
46
|
return {
|
|
37
47
|
...peer,
|
|
38
48
|
slug,
|
|
39
49
|
suffix,
|
|
40
|
-
canonicalId: `${peer.runtime}:${
|
|
41
|
-
display: collides ? qualified : slug
|
|
50
|
+
canonicalId: `${peer.runtime}:${slug}.${peer.uuid}${host}`,
|
|
51
|
+
display: collides ? qualified : `${slug}${host}`,
|
|
42
52
|
};
|
|
43
53
|
});
|
|
44
54
|
}
|
|
45
55
|
export function resolvePeer(peers, input) {
|
|
46
56
|
const q = input.trim().toLowerCase();
|
|
47
|
-
const qualified = (p) => `${p.slug}.${p.suffix}`;
|
|
57
|
+
const qualified = (p) => `${p.slug}.${p.suffix}${at(p.machine)}`;
|
|
58
|
+
// A bare address names a peer on THIS machine. Prefix matching is what makes
|
|
59
|
+
// that matter: without this gate an address with no `@` could fall through
|
|
60
|
+
// and resolve to a session on another computer, so a message meant for a
|
|
61
|
+
// peer at this desk would leave the machine and be reported as sent. The
|
|
62
|
+
// dangerous direction has to be the explicit one.
|
|
63
|
+
const wantsMachine = q.includes("@");
|
|
64
|
+
peers = peers.filter((p) => {
|
|
65
|
+
const remote = p.machine !== undefined && p.machine !== "";
|
|
66
|
+
return wantsMachine ? remote : !remote;
|
|
67
|
+
});
|
|
48
68
|
const exact = peers.filter((p) => p.display.toLowerCase() === q ||
|
|
49
69
|
qualified(p) === q ||
|
|
50
70
|
p.canonicalId.toLowerCase() === q);
|
package/dist/run.js
CHANGED
|
@@ -26,7 +26,7 @@ import { codexPolicyArgs } from "./codex-policy.js";
|
|
|
26
26
|
import { CodexRpc } from "./codex-rpc.js";
|
|
27
27
|
import { allocateLoopbackPort, OpenCodeHttp } from "./opencode-http.js";
|
|
28
28
|
import { assertOpenCodeVersion, openCodeMcpNames, openCodeSessionLaunch, openCodeTaskLaunch, } from "./opencode-policy.js";
|
|
29
|
-
import { assignNames, resolvePeer, runtimeName, } from "./naming.js";
|
|
29
|
+
import { assignNames, resolvePeer, runtimeName, suffixOf, } from "./naming.js";
|
|
30
30
|
import { selectMcp, prepareMcp, mcpEnvironment } from "./mcp.js";
|
|
31
31
|
import { pollInterval } from "./reap.js";
|
|
32
32
|
import { selectPlugins } from "./plugins.js";
|
|
@@ -783,7 +783,12 @@ export class Muster {
|
|
|
783
783
|
slug: String(e.peer.canonical_id)
|
|
784
784
|
.split(":")[1]
|
|
785
785
|
.replace(/\.[^.]*$/, ""),
|
|
786
|
-
|
|
786
|
+
// From the durable id, not from the canonical id's last dot-segment.
|
|
787
|
+
// That segment used to BE the three-hex suffix; since Tin Can 1.0.0 the
|
|
788
|
+
// canonical form carries the whole uuid, so the old surgery yielded the
|
|
789
|
+
// entire id and every qualified `slug.suffix` address stopped resolving.
|
|
790
|
+
// It failed quietly, as "unknown", which is why it needed a test.
|
|
791
|
+
suffix: suffixOf(e.id),
|
|
787
792
|
canonicalId: String(e.peer.canonical_id),
|
|
788
793
|
display: String(e.peer.name),
|
|
789
794
|
}));
|
package/package.json
CHANGED
|
@@ -1,161 +1,590 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "FROZEN. Describes @brutalsystems/tincan address behaviour as shipped. Adding cases is fine; changing an expected value means the address format changed - see CANONICAL_ID.md, 'Changing this format'.",
|
|
3
|
-
"tincan_version": "
|
|
3
|
+
"tincan_version": "1.9.2",
|
|
4
4
|
"spec": "CANONICAL_ID.md",
|
|
5
|
-
|
|
6
5
|
"slugify": [
|
|
7
|
-
{
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
{
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
{
|
|
7
|
+
"case": "spaces and case are folded",
|
|
8
|
+
"input": "Auth Refactor",
|
|
9
|
+
"output": "auth-refactor"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"case": "runs of punctuation collapse to one dash",
|
|
13
|
+
"input": "auth/refactor (v2)",
|
|
14
|
+
"output": "auth-refactor-v2"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"case": "leading and trailing separators are stripped",
|
|
18
|
+
"input": " --Auth-- ",
|
|
19
|
+
"output": "auth"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"case": "non-ascii is not transliterated, it is a separator",
|
|
23
|
+
"input": "café ☕ time",
|
|
24
|
+
"output": "caf-time"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"case": "digits survive",
|
|
28
|
+
"input": "123",
|
|
29
|
+
"output": "123"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"case": "all-caps folds",
|
|
33
|
+
"input": "ALLCAPS",
|
|
34
|
+
"output": "allcaps"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"case": "a name of only punctuation slugifies to empty",
|
|
38
|
+
"input": "???",
|
|
39
|
+
"output": ""
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"case": "empty in, empty out",
|
|
43
|
+
"input": "",
|
|
44
|
+
"output": ""
|
|
45
|
+
}
|
|
15
46
|
],
|
|
16
|
-
|
|
17
47
|
"suffix": [
|
|
18
|
-
{
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{
|
|
48
|
+
{
|
|
49
|
+
"case": "codex thread id (uuidv7): last three hex",
|
|
50
|
+
"input": "01a0b9b4-a33e-7ab1-80a0-bb715504a0fb",
|
|
51
|
+
"output": "0fb"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"case": "claude session id (uuidv4): last three hex",
|
|
55
|
+
"input": "5af69d42-2214-41d9-b13f-9c3177eb60ce",
|
|
56
|
+
"output": "0ce"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"case": "opencode session id (ses_ + hex timestamp + base62 random tail): last three hex",
|
|
60
|
+
"input": "ses_f41a2b3c4ffeExampleSess01Z",
|
|
61
|
+
"output": "e01"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"case": "uppercase hex is lowercased",
|
|
65
|
+
"input": "...ABC",
|
|
66
|
+
"output": "abc"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"case": "non-hex characters are stripped before slicing",
|
|
70
|
+
"input": "z-9",
|
|
71
|
+
"output": "9"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"case": "no hex at all yields an empty suffix",
|
|
75
|
+
"input": "zzzz",
|
|
76
|
+
"output": ""
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"case": "empty in, empty out",
|
|
80
|
+
"input": "",
|
|
81
|
+
"output": ""
|
|
82
|
+
}
|
|
24
83
|
],
|
|
25
|
-
|
|
26
84
|
"assign": [
|
|
27
85
|
{
|
|
28
86
|
"case": "no collision: display is the bare slug, canonical id is always qualified",
|
|
29
|
-
"peers": [
|
|
30
|
-
|
|
87
|
+
"peers": [
|
|
88
|
+
{
|
|
89
|
+
"runtime": "codex",
|
|
90
|
+
"rawName": "Auth refactor",
|
|
91
|
+
"uuid": "00000000-0000-0000-0000-0000000007f3"
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"expect": [
|
|
95
|
+
{
|
|
96
|
+
"display": "auth-refactor",
|
|
97
|
+
"canonicalId": "codex:auth-refactor.00000000-0000-0000-0000-0000000007f3"
|
|
98
|
+
}
|
|
99
|
+
]
|
|
31
100
|
},
|
|
32
101
|
{
|
|
33
102
|
"case": "claude peers are qualified with their runtime",
|
|
34
|
-
"peers": [
|
|
35
|
-
|
|
103
|
+
"peers": [
|
|
104
|
+
{
|
|
105
|
+
"runtime": "claude-code",
|
|
106
|
+
"rawName": "billing-api",
|
|
107
|
+
"uuid": "5af69d42-2214-41d9-b13f-9c3177eb60ce"
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
"expect": [
|
|
111
|
+
{
|
|
112
|
+
"display": "billing-api",
|
|
113
|
+
"canonicalId": "claude-code:billing-api.5af69d42-2214-41d9-b13f-9c3177eb60ce"
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"case": "opencode peer, no collision: display is the bare slug",
|
|
119
|
+
"peers": [
|
|
120
|
+
{
|
|
121
|
+
"runtime": "opencode",
|
|
122
|
+
"rawName": "nimble-wizard",
|
|
123
|
+
"uuid": "ses_7f6e5d4c3b2aNimbleWizardQ7"
|
|
124
|
+
}
|
|
125
|
+
],
|
|
126
|
+
"expect": [
|
|
127
|
+
{
|
|
128
|
+
"display": "nimble-wizard",
|
|
129
|
+
"canonicalId": "opencode:nimble-wizard.ses_7f6e5d4c3b2aNimbleWizardQ7"
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"case": "collision between two opencode sessions with the same slug: both suffixed",
|
|
135
|
+
"peers": [
|
|
136
|
+
{
|
|
137
|
+
"runtime": "opencode",
|
|
138
|
+
"rawName": "swift-otter",
|
|
139
|
+
"uuid": "ses_8e7d6c5b4a39SwiftOtterAlpha"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"runtime": "opencode",
|
|
143
|
+
"rawName": "swift-otter",
|
|
144
|
+
"uuid": "ses_8e7d6c5b4a39SwiftOtterBeta7"
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
"expect": [
|
|
148
|
+
{
|
|
149
|
+
"display": "swift-otter.eaa",
|
|
150
|
+
"canonicalId": "opencode:swift-otter.ses_8e7d6c5b4a39SwiftOtterAlpha"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"display": "swift-otter.ea7",
|
|
154
|
+
"canonicalId": "opencode:swift-otter.ses_8e7d6c5b4a39SwiftOtterBeta7"
|
|
155
|
+
}
|
|
156
|
+
]
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"case": "cross-runtime collision: an opencode peer and a codex peer sharing a slug both flip to suffixed",
|
|
160
|
+
"peers": [
|
|
161
|
+
{
|
|
162
|
+
"runtime": "opencode",
|
|
163
|
+
"rawName": "auth-refactor",
|
|
164
|
+
"uuid": "ses_9f8e7d6c5b4aAuthRefactorX9"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"runtime": "codex",
|
|
168
|
+
"rawName": "Auth refactor",
|
|
169
|
+
"uuid": "01b1c2d3-e4f5-7a6b-90c1-d2e3f4a5b6c7"
|
|
170
|
+
}
|
|
171
|
+
],
|
|
172
|
+
"expect": [
|
|
173
|
+
{
|
|
174
|
+
"display": "auth-refactor.ac9",
|
|
175
|
+
"canonicalId": "opencode:auth-refactor.ses_9f8e7d6c5b4aAuthRefactorX9"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"display": "auth-refactor.6c7",
|
|
179
|
+
"canonicalId": "codex:auth-refactor.01b1c2d3-e4f5-7a6b-90c1-d2e3f4a5b6c7"
|
|
180
|
+
}
|
|
181
|
+
]
|
|
36
182
|
},
|
|
37
183
|
{
|
|
38
184
|
"case": "collision: every colliding peer is suffixed, and only those",
|
|
39
185
|
"peers": [
|
|
40
|
-
{
|
|
41
|
-
|
|
42
|
-
|
|
186
|
+
{
|
|
187
|
+
"runtime": "codex",
|
|
188
|
+
"rawName": "Review phase 1 handoff",
|
|
189
|
+
"uuid": "00000000-0000-0000-0000-000000000aaa"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"runtime": "codex",
|
|
193
|
+
"rawName": "Review phase-1 handoff",
|
|
194
|
+
"uuid": "00000000-0000-0000-0000-000000000bbb"
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"runtime": "codex",
|
|
198
|
+
"rawName": "Something else",
|
|
199
|
+
"uuid": "00000000-0000-0000-0000-000000000ccc"
|
|
200
|
+
}
|
|
43
201
|
],
|
|
44
202
|
"expect": [
|
|
45
|
-
{
|
|
46
|
-
|
|
47
|
-
|
|
203
|
+
{
|
|
204
|
+
"display": "review-phase-1-handoff.aaa",
|
|
205
|
+
"canonicalId": "codex:review-phase-1-handoff.00000000-0000-0000-0000-000000000aaa"
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"display": "review-phase-1-handoff.bbb",
|
|
209
|
+
"canonicalId": "codex:review-phase-1-handoff.00000000-0000-0000-0000-000000000bbb"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"display": "something-else",
|
|
213
|
+
"canonicalId": "codex:something-else.00000000-0000-0000-0000-000000000ccc"
|
|
214
|
+
}
|
|
48
215
|
]
|
|
49
216
|
},
|
|
50
217
|
{
|
|
51
218
|
"case": "an unnamed thread always carries its suffix, even alone",
|
|
52
|
-
"peers": [
|
|
53
|
-
|
|
219
|
+
"peers": [
|
|
220
|
+
{
|
|
221
|
+
"runtime": "codex",
|
|
222
|
+
"rawName": null,
|
|
223
|
+
"uuid": "00000000-0000-0000-0000-0000000007f3"
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
"expect": [
|
|
227
|
+
{
|
|
228
|
+
"display": "thread.7f3",
|
|
229
|
+
"canonicalId": "codex:thread.00000000-0000-0000-0000-0000000007f3"
|
|
230
|
+
}
|
|
231
|
+
]
|
|
54
232
|
},
|
|
55
233
|
{
|
|
56
234
|
"case": "KNOWN DEFECT: a name that slugifies to empty displays bare, unlike an unnamed thread",
|
|
57
|
-
"peers": [
|
|
58
|
-
|
|
235
|
+
"peers": [
|
|
236
|
+
{
|
|
237
|
+
"runtime": "codex",
|
|
238
|
+
"rawName": "???",
|
|
239
|
+
"uuid": "00000000-0000-0000-0000-000000000def"
|
|
240
|
+
}
|
|
241
|
+
],
|
|
242
|
+
"expect": [
|
|
243
|
+
{
|
|
244
|
+
"display": "thread",
|
|
245
|
+
"canonicalId": "codex:thread.00000000-0000-0000-0000-000000000def"
|
|
246
|
+
}
|
|
247
|
+
]
|
|
59
248
|
},
|
|
60
249
|
{
|
|
61
250
|
"case": "unnamed and slugifies-to-empty collide on 'thread', so both are suffixed",
|
|
62
251
|
"peers": [
|
|
63
|
-
{
|
|
64
|
-
|
|
252
|
+
{
|
|
253
|
+
"runtime": "codex",
|
|
254
|
+
"rawName": null,
|
|
255
|
+
"uuid": "00000000-0000-0000-0000-000000000aaa"
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"runtime": "codex",
|
|
259
|
+
"rawName": "???",
|
|
260
|
+
"uuid": "00000000-0000-0000-0000-000000000bbb"
|
|
261
|
+
}
|
|
65
262
|
],
|
|
66
263
|
"expect": [
|
|
67
|
-
{
|
|
68
|
-
|
|
264
|
+
{
|
|
265
|
+
"display": "thread.aaa",
|
|
266
|
+
"canonicalId": "codex:thread.00000000-0000-0000-0000-000000000aaa"
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
"display": "thread.bbb",
|
|
270
|
+
"canonicalId": "codex:thread.00000000-0000-0000-0000-000000000bbb"
|
|
271
|
+
}
|
|
69
272
|
]
|
|
70
273
|
},
|
|
71
274
|
{
|
|
72
275
|
"case": "KNOWN DEFECT: canonical_id is not unique when slug and suffix both collide",
|
|
73
276
|
"peers": [
|
|
74
|
-
{
|
|
75
|
-
|
|
277
|
+
{
|
|
278
|
+
"runtime": "codex",
|
|
279
|
+
"rawName": "Auth refactor",
|
|
280
|
+
"uuid": "11111111-0000-0000-0000-000000000abc"
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
"runtime": "codex",
|
|
284
|
+
"rawName": "auth-refactor",
|
|
285
|
+
"uuid": "22222222-0000-0000-0000-000000000abc"
|
|
286
|
+
}
|
|
287
|
+
],
|
|
288
|
+
"expect": [
|
|
289
|
+
{
|
|
290
|
+
"display": "auth-refactor.abc",
|
|
291
|
+
"canonicalId": "codex:auth-refactor.11111111-0000-0000-0000-000000000abc"
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
"display": "auth-refactor.abc",
|
|
295
|
+
"canonicalId": "codex:auth-refactor.22222222-0000-0000-0000-000000000abc"
|
|
296
|
+
}
|
|
297
|
+
]
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
"case": "a peer on another machine carries @machine in both forms",
|
|
301
|
+
"peers": [
|
|
302
|
+
{
|
|
303
|
+
"runtime": "codex",
|
|
304
|
+
"rawName": "Auth refactor",
|
|
305
|
+
"uuid": "00000000-0000-0000-0000-0000000007f3",
|
|
306
|
+
"machine": "m4pro"
|
|
307
|
+
}
|
|
308
|
+
],
|
|
309
|
+
"expect": [
|
|
310
|
+
{
|
|
311
|
+
"display": "auth-refactor@m4pro",
|
|
312
|
+
"canonicalId": "codex:auth-refactor.00000000-0000-0000-0000-0000000007f3@m4pro"
|
|
313
|
+
}
|
|
314
|
+
]
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
"case": "the same slug on two machines does not collide",
|
|
318
|
+
"peers": [
|
|
319
|
+
{
|
|
320
|
+
"runtime": "codex",
|
|
321
|
+
"rawName": "Auth refactor",
|
|
322
|
+
"uuid": "00000000-0000-0000-0000-0000000007f3"
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
"runtime": "codex",
|
|
326
|
+
"rawName": "Auth refactor",
|
|
327
|
+
"uuid": "00000000-0000-0000-0000-0000000007f3",
|
|
328
|
+
"machine": "m4pro"
|
|
329
|
+
}
|
|
76
330
|
],
|
|
77
331
|
"expect": [
|
|
78
|
-
{
|
|
79
|
-
|
|
332
|
+
{
|
|
333
|
+
"display": "auth-refactor",
|
|
334
|
+
"canonicalId": "codex:auth-refactor.00000000-0000-0000-0000-0000000007f3"
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
"display": "auth-refactor@m4pro",
|
|
338
|
+
"canonicalId": "codex:auth-refactor.00000000-0000-0000-0000-0000000007f3@m4pro"
|
|
339
|
+
}
|
|
80
340
|
]
|
|
81
341
|
}
|
|
82
342
|
],
|
|
83
|
-
|
|
84
343
|
"resolve": [
|
|
85
344
|
{
|
|
86
345
|
"case": "exact display name",
|
|
87
|
-
"peers": [
|
|
346
|
+
"peers": [
|
|
347
|
+
{
|
|
348
|
+
"runtime": "codex",
|
|
349
|
+
"rawName": "Auth refactor",
|
|
350
|
+
"uuid": "00000000-0000-0000-0000-0000000007f3"
|
|
351
|
+
}
|
|
352
|
+
],
|
|
88
353
|
"input": "auth-refactor",
|
|
89
|
-
"expect": {
|
|
354
|
+
"expect": {
|
|
355
|
+
"ok": true,
|
|
356
|
+
"canonicalId": "codex:auth-refactor.00000000-0000-0000-0000-0000000007f3"
|
|
357
|
+
}
|
|
90
358
|
},
|
|
91
359
|
{
|
|
92
360
|
"case": "qualified form, accepted even with no collision",
|
|
93
|
-
"peers": [
|
|
361
|
+
"peers": [
|
|
362
|
+
{
|
|
363
|
+
"runtime": "codex",
|
|
364
|
+
"rawName": "Auth refactor",
|
|
365
|
+
"uuid": "00000000-0000-0000-0000-0000000007f3"
|
|
366
|
+
}
|
|
367
|
+
],
|
|
94
368
|
"input": "auth-refactor.7f3",
|
|
95
|
-
"expect": {
|
|
369
|
+
"expect": {
|
|
370
|
+
"ok": true,
|
|
371
|
+
"canonicalId": "codex:auth-refactor.00000000-0000-0000-0000-0000000007f3"
|
|
372
|
+
}
|
|
96
373
|
},
|
|
97
374
|
{
|
|
98
375
|
"case": "fully qualified canonical id is accepted as input",
|
|
99
|
-
"peers": [
|
|
100
|
-
|
|
101
|
-
|
|
376
|
+
"peers": [
|
|
377
|
+
{
|
|
378
|
+
"runtime": "codex",
|
|
379
|
+
"rawName": "Auth refactor",
|
|
380
|
+
"uuid": "00000000-0000-0000-0000-0000000007f3"
|
|
381
|
+
}
|
|
382
|
+
],
|
|
383
|
+
"input": "codex:auth-refactor.00000000-0000-0000-0000-0000000007f3",
|
|
384
|
+
"expect": {
|
|
385
|
+
"ok": true,
|
|
386
|
+
"canonicalId": "codex:auth-refactor.00000000-0000-0000-0000-0000000007f3"
|
|
387
|
+
}
|
|
102
388
|
},
|
|
103
389
|
{
|
|
104
390
|
"case": "case-insensitive, and input is trimmed",
|
|
105
|
-
"peers": [
|
|
391
|
+
"peers": [
|
|
392
|
+
{
|
|
393
|
+
"runtime": "codex",
|
|
394
|
+
"rawName": "Auth refactor",
|
|
395
|
+
"uuid": "00000000-0000-0000-0000-0000000007f3"
|
|
396
|
+
}
|
|
397
|
+
],
|
|
106
398
|
"input": " AUTH-REFACTOR.7F3 ",
|
|
107
|
-
"expect": {
|
|
399
|
+
"expect": {
|
|
400
|
+
"ok": true,
|
|
401
|
+
"canonicalId": "codex:auth-refactor.00000000-0000-0000-0000-0000000007f3"
|
|
402
|
+
}
|
|
108
403
|
},
|
|
109
404
|
{
|
|
110
405
|
"case": "unambiguous prefix resolves",
|
|
111
406
|
"peers": [
|
|
112
|
-
{
|
|
113
|
-
|
|
407
|
+
{
|
|
408
|
+
"runtime": "codex",
|
|
409
|
+
"rawName": "Auth refactor",
|
|
410
|
+
"uuid": "00000000-0000-0000-0000-0000000007f3"
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
"runtime": "codex",
|
|
414
|
+
"rawName": "Billing sync",
|
|
415
|
+
"uuid": "00000000-0000-0000-0000-000000000b12"
|
|
416
|
+
}
|
|
114
417
|
],
|
|
115
418
|
"input": "auth",
|
|
116
|
-
"expect": {
|
|
419
|
+
"expect": {
|
|
420
|
+
"ok": true,
|
|
421
|
+
"canonicalId": "codex:auth-refactor.00000000-0000-0000-0000-0000000007f3"
|
|
422
|
+
}
|
|
117
423
|
},
|
|
118
424
|
{
|
|
119
425
|
"case": "ambiguous prefix is refused, listing every candidate in qualified form",
|
|
120
426
|
"peers": [
|
|
121
|
-
{
|
|
122
|
-
|
|
427
|
+
{
|
|
428
|
+
"runtime": "codex",
|
|
429
|
+
"rawName": "Billing sync",
|
|
430
|
+
"uuid": "00000000-0000-0000-0000-000000000b12"
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
"runtime": "codex",
|
|
434
|
+
"rawName": "Billing report",
|
|
435
|
+
"uuid": "00000000-0000-0000-0000-000000000b34"
|
|
436
|
+
}
|
|
123
437
|
],
|
|
124
438
|
"input": "billing",
|
|
125
|
-
"expect": {
|
|
439
|
+
"expect": {
|
|
440
|
+
"ok": false,
|
|
441
|
+
"reason": "ambiguous",
|
|
442
|
+
"candidates": [
|
|
443
|
+
"billing-sync.b12",
|
|
444
|
+
"billing-report.b34"
|
|
445
|
+
]
|
|
446
|
+
}
|
|
126
447
|
},
|
|
127
448
|
{
|
|
128
449
|
"case": "unknown name is refused, and candidates list every peer in display form",
|
|
129
450
|
"peers": [
|
|
130
|
-
{
|
|
131
|
-
|
|
451
|
+
{
|
|
452
|
+
"runtime": "codex",
|
|
453
|
+
"rawName": "Auth refactor",
|
|
454
|
+
"uuid": "00000000-0000-0000-0000-0000000007f3"
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
"runtime": "codex",
|
|
458
|
+
"rawName": "Billing sync",
|
|
459
|
+
"uuid": "00000000-0000-0000-0000-000000000b12"
|
|
460
|
+
}
|
|
132
461
|
],
|
|
133
462
|
"input": "zzz",
|
|
134
|
-
"expect": {
|
|
463
|
+
"expect": {
|
|
464
|
+
"ok": false,
|
|
465
|
+
"reason": "unknown",
|
|
466
|
+
"candidates": [
|
|
467
|
+
"auth-refactor",
|
|
468
|
+
"billing-sync"
|
|
469
|
+
]
|
|
470
|
+
}
|
|
135
471
|
},
|
|
136
472
|
{
|
|
137
|
-
"case": "
|
|
473
|
+
"case": "two peers whose slug AND short suffix collide are each still addressable by canonical id",
|
|
138
474
|
"peers": [
|
|
139
|
-
{
|
|
140
|
-
|
|
475
|
+
{
|
|
476
|
+
"runtime": "codex",
|
|
477
|
+
"rawName": "Auth refactor",
|
|
478
|
+
"uuid": "11111111-0000-0000-0000-000000000abc"
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
"runtime": "codex",
|
|
482
|
+
"rawName": "auth-refactor",
|
|
483
|
+
"uuid": "22222222-0000-0000-0000-000000000abc"
|
|
484
|
+
}
|
|
141
485
|
],
|
|
142
|
-
"input": "codex:auth-refactor.
|
|
143
|
-
"expect": {
|
|
486
|
+
"input": "codex:auth-refactor.11111111-0000-0000-0000-000000000abc",
|
|
487
|
+
"expect": {
|
|
488
|
+
"ok": true,
|
|
489
|
+
"canonicalId": "codex:auth-refactor.11111111-0000-0000-0000-000000000abc"
|
|
490
|
+
}
|
|
144
491
|
},
|
|
145
492
|
{
|
|
146
493
|
"case": "empty input is a prefix of everything: ambiguous when more than one peer",
|
|
147
494
|
"peers": [
|
|
148
|
-
{
|
|
149
|
-
|
|
495
|
+
{
|
|
496
|
+
"runtime": "codex",
|
|
497
|
+
"rawName": "Auth refactor",
|
|
498
|
+
"uuid": "00000000-0000-0000-0000-0000000007f3"
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
"runtime": "codex",
|
|
502
|
+
"rawName": "Billing sync",
|
|
503
|
+
"uuid": "00000000-0000-0000-0000-000000000b12"
|
|
504
|
+
}
|
|
150
505
|
],
|
|
151
506
|
"input": "",
|
|
152
|
-
"expect": {
|
|
507
|
+
"expect": {
|
|
508
|
+
"ok": false,
|
|
509
|
+
"reason": "ambiguous",
|
|
510
|
+
"candidates": [
|
|
511
|
+
"auth-refactor.7f3",
|
|
512
|
+
"billing-sync.b12"
|
|
513
|
+
]
|
|
514
|
+
}
|
|
153
515
|
},
|
|
154
516
|
{
|
|
155
517
|
"case": "KNOWN DEFECT: empty input resolves to the only peer when there is exactly one",
|
|
156
|
-
"peers": [
|
|
518
|
+
"peers": [
|
|
519
|
+
{
|
|
520
|
+
"runtime": "codex",
|
|
521
|
+
"rawName": "Auth refactor",
|
|
522
|
+
"uuid": "00000000-0000-0000-0000-0000000007f3"
|
|
523
|
+
}
|
|
524
|
+
],
|
|
157
525
|
"input": "",
|
|
158
|
-
"expect": {
|
|
526
|
+
"expect": {
|
|
527
|
+
"ok": true,
|
|
528
|
+
"canonicalId": "codex:auth-refactor.00000000-0000-0000-0000-0000000007f3"
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
"case": "a remote peer resolves by its @machine address",
|
|
533
|
+
"peers": [
|
|
534
|
+
{
|
|
535
|
+
"runtime": "codex",
|
|
536
|
+
"rawName": "Auth refactor",
|
|
537
|
+
"uuid": "00000000-0000-0000-0000-0000000007f3"
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
"runtime": "codex",
|
|
541
|
+
"rawName": "Auth refactor",
|
|
542
|
+
"uuid": "00000000-0000-0000-0000-0000000007f3",
|
|
543
|
+
"machine": "m4pro"
|
|
544
|
+
}
|
|
545
|
+
],
|
|
546
|
+
"input": "auth-refactor@m4pro",
|
|
547
|
+
"expect": {
|
|
548
|
+
"ok": true,
|
|
549
|
+
"canonicalId": "codex:auth-refactor.00000000-0000-0000-0000-0000000007f3@m4pro"
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
"case": "a bare address never reaches another machine",
|
|
554
|
+
"peers": [
|
|
555
|
+
{
|
|
556
|
+
"runtime": "codex",
|
|
557
|
+
"rawName": "Auth refactor",
|
|
558
|
+
"uuid": "00000000-0000-0000-0000-0000000007f3",
|
|
559
|
+
"machine": "m4pro"
|
|
560
|
+
}
|
|
561
|
+
],
|
|
562
|
+
"input": "auth-refactor",
|
|
563
|
+
"expect": {
|
|
564
|
+
"ok": false,
|
|
565
|
+
"reason": "unknown",
|
|
566
|
+
"candidates": []
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
"case": "KNOWN DEFECT: a full name that is a prefix of a longer full name resolves to the longer one once the exactly-named session is gone",
|
|
571
|
+
"peers": [
|
|
572
|
+
{
|
|
573
|
+
"runtime": "claude-code",
|
|
574
|
+
"rawName": "muster-b1",
|
|
575
|
+
"uuid": "22222222-2222-4222-8222-222222222222"
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
"runtime": "claude-code",
|
|
579
|
+
"rawName": "cxx-be",
|
|
580
|
+
"uuid": "33333333-3333-4333-8333-333333333333"
|
|
581
|
+
}
|
|
582
|
+
],
|
|
583
|
+
"input": "muster",
|
|
584
|
+
"expect": {
|
|
585
|
+
"ok": true,
|
|
586
|
+
"canonicalId": "claude-code:muster-b1.22222222-2222-4222-8222-222222222222"
|
|
587
|
+
}
|
|
159
588
|
}
|
|
160
589
|
]
|
|
161
590
|
}
|