@brutalsystems/muster 0.8.3 → 0.9.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/CANONICAL_ID.md +189 -25
- package/CONTRACT_PROVENANCE.md +71 -0
- 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.6.0.**
|
|
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,21 +93,47 @@ 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
|
|
|
@@ -84,31 +144,82 @@ It is the form to record in logs and to pass between tools.
|
|
|
84
144
|
|
|
85
145
|
A peer is suffixed in its **display** form when either:
|
|
86
146
|
|
|
87
|
-
- another peer in the same listing has the same slug
|
|
147
|
+
- another peer in the same listing has the same slug **on the same machine**, or
|
|
88
148
|
- the peer has no name (`rawName === null`), which is always suffixed.
|
|
89
149
|
|
|
150
|
+
Collisions are counted per machine. The same slug on two computers is already
|
|
151
|
+
two different addresses, so suffixing both would add noise to distinguish
|
|
152
|
+
things that were never confusable.
|
|
153
|
+
|
|
90
154
|
Otherwise the display form is the bare slug. Only colliding peers are suffixed;
|
|
91
155
|
peers that do not collide keep their bare names in the same listing.
|
|
92
156
|
|
|
157
|
+
**Adding a runtime can change an existing peer's display.** `assignNames`
|
|
158
|
+
computes collisions across the *whole* returned list, regardless of runtime.
|
|
159
|
+
So a peer that displayed as a bare slug before opencode support existed can
|
|
160
|
+
start displaying suffixed the moment an opencode peer (or any peer) slugging
|
|
161
|
+
to the same string appears in the same listing — a Codex peer `foo` and an
|
|
162
|
+
opencode session slugging to `foo` both flip from bare `foo` to qualified
|
|
163
|
+
`foo.<suffix>` (see the fixture's "cross-runtime collision" case). This is
|
|
164
|
+
inherent to the existing rule, not new behaviour introduced by opencode
|
|
165
|
+
support, and it is correct: the two peers are genuinely indistinguishable by
|
|
166
|
+
slug. It is called out here because Muster consumes `display`, and this
|
|
167
|
+
should be an expected consequence of adding a runtime, not something its
|
|
168
|
+
maintainer discovers by surprise.
|
|
169
|
+
|
|
93
170
|
## Resolution
|
|
94
171
|
|
|
95
|
-
Input is trimmed and lowercased, then matched in two passes
|
|
172
|
+
Input is trimmed and lowercased, then matched in two passes against the peer
|
|
173
|
+
listing, with one host-local check between them.
|
|
96
174
|
|
|
97
175
|
**Pass 1 — exact.** Matches if the input equals the peer's display form, its
|
|
98
176
|
qualified form, or its canonical id. One match resolves. More than one is
|
|
99
177
|
refused as ambiguous.
|
|
100
178
|
|
|
101
|
-
**
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
179
|
+
**Self check**, only if pass 1 found nothing. If the input names the session
|
|
180
|
+
Tin Can is itself running in, the result is refused as `self`. See
|
|
181
|
+
[Self is not a peer](#self-is-not-a-peer) — this pass is **not** part of the
|
|
182
|
+
fixture contract and a second implementation is not expected to reproduce it.
|
|
183
|
+
|
|
184
|
+
**Pass 2 — prefix**, only if pass 1 and the self check found nothing. Matches
|
|
185
|
+
if the peer's slug or its qualified form *starts with* the input. One match
|
|
186
|
+
resolves. More than one is refused as ambiguous.
|
|
187
|
+
|
|
188
|
+
If nothing matches, the result is refused as unknown.
|
|
189
|
+
|
|
190
|
+
### Self is not a peer
|
|
191
|
+
|
|
192
|
+
A host never lists the session it is running in, so an address naming that
|
|
193
|
+
session reaches pass 2 with nothing to match — or, worse, matches *something
|
|
194
|
+
else*. The check sits between the passes rather than before them, and the
|
|
195
|
+
order is load-bearing in both directions:
|
|
196
|
+
|
|
197
|
+
- **Below pass 1**, so an exact peer name still wins. A peer genuinely called
|
|
198
|
+
`review` receives `review` even when the host is called `review-tools`,
|
|
199
|
+
which the self check matches by prefix.
|
|
200
|
+
- **Above pass 2**, because that pass resolves on a *single* match. A host
|
|
201
|
+
that can only recognise itself by name — a Claude Code host carries no
|
|
202
|
+
session id of its own and falls back to its working directory's basename —
|
|
203
|
+
whose fallback name prefixed exactly one peer would otherwise resolve to
|
|
204
|
+
that peer and deliver. Where the listing spans config dirs, that peer
|
|
205
|
+
belongs to a different account: a note addressed to yourself, delivered to
|
|
206
|
+
a stranger, reported as sent.
|
|
207
|
+
|
|
208
|
+
**This is deliberately outside the fixture.** Every case in
|
|
209
|
+
`test/fixtures/canonical-id.json` resolves with no host identity supplied, and
|
|
210
|
+
all ten behave identically with and without this pass. "Self" is a property of
|
|
211
|
+
the process doing the resolving, not of the address or the listing, so a tool
|
|
212
|
+
that consumes addresses — Muster launches sessions and hands back peer records
|
|
213
|
+
for Tin Can to resolve — has no self to check and nothing to implement. The
|
|
214
|
+
address format is unchanged; what changed is what one particular resolver does
|
|
215
|
+
with its own name.
|
|
106
216
|
|
|
107
217
|
- **Case-insensitive** throughout. `AUTH-REFACTOR.7F3` resolves.
|
|
108
218
|
- **Input is trimmed.** Surrounding whitespace is ignored.
|
|
109
219
|
- **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
|
-
|
|
220
|
+
is unambiguous. The empty string is a prefix of everything, and a complete
|
|
221
|
+
name can be a prefix of a longer complete name — see
|
|
222
|
+
[known defects](#known-defects), 3 and 4.
|
|
112
223
|
- **No tie-breaking.** Ambiguity is always refused, never guessed.
|
|
113
224
|
|
|
114
225
|
### Refusal shape
|
|
@@ -117,26 +228,32 @@ If neither pass matches, the result is refused as unknown.
|
|
|
117
228
|
|---|---|
|
|
118
229
|
| `ambiguous` | every matching peer, in **qualified** form |
|
|
119
230
|
| `unknown` | every peer in the listing, in **display** form |
|
|
231
|
+
| `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
232
|
|
|
121
233
|
## Known defects
|
|
122
234
|
|
|
123
|
-
Preserved
|
|
235
|
+
Preserved by decision, not by oversight. They are in the fixture so
|
|
124
236
|
that a second implementation matches Tin Can exactly, including where Tin Can is
|
|
125
237
|
wrong. **Do not "correct" them independently** — that produces addresses that
|
|
126
238
|
resolve in one tool and fail silently in the other.
|
|
127
239
|
|
|
128
|
-
**1. `canonical_id` is not unique
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
240
|
+
**1. `canonical_id` is not unique. — FIXED in 1.0.0.**
|
|
241
|
+
|
|
242
|
+
It used to carry the three-character suffix, so two peers whose slugs matched
|
|
243
|
+
*and* whose ids shared their last three hex characters produced the same
|
|
244
|
+
`canonical_id`. Tin Can then refused to resolve either, listing two identical
|
|
245
|
+
candidates — a refusal that told the caller to disambiguate using a string that
|
|
246
|
+
did not disambiguate. Both peers were unaddressable until one exited. Roughly 1
|
|
247
|
+
in 4096 given a slug collision, and slug collisions are not rare: Codex titles
|
|
248
|
+
threads from their first prompt.
|
|
133
249
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
started from similar prompts collide readily.
|
|
250
|
+
The canonical form now carries the whole durable id, which makes a collision
|
|
251
|
+
impossible rather than merely unlikely. Both peers stay addressable.
|
|
137
252
|
|
|
138
|
-
**
|
|
139
|
-
|
|
253
|
+
**The short forms still collide**, and still should: `display` and `qualified`
|
|
254
|
+
are for humans, and two sessions genuinely sharing a name are genuinely
|
|
255
|
+
ambiguous. The difference is that there is now an unambiguous form to fall back
|
|
256
|
+
to. `canonical_id` is a usable key.
|
|
140
257
|
|
|
141
258
|
**2. A name that slugifies to empty is inconsistent with an unnamed peer.**
|
|
142
259
|
Alone in a listing, a peer named `???` displays as `thread` with no suffix,
|
|
@@ -149,6 +266,45 @@ string is a prefix of every slug, `resolve("")` against a single-peer listing
|
|
|
149
266
|
returns that peer rather than refusing. With two or more peers it is refused as
|
|
150
267
|
ambiguous, as expected.
|
|
151
268
|
|
|
269
|
+
**4. A full name that is a prefix of a longer full name resolves to the longer
|
|
270
|
+
one once the exactly-named session is gone.** While a session named `muster` is
|
|
271
|
+
listed, `muster` is an exact hit and pass 2 is never reached. The moment it
|
|
272
|
+
exits, `muster` is nobody's name and becomes merely a prefix of its neighbour
|
|
273
|
+
`muster-b1` — the only match, so it resolves, and the message is delivered to a
|
|
274
|
+
session the caller did not mean and reported as sent. Observed live between two
|
|
275
|
+
sessions; the send was a reply, so `reply_misrouted` caught it, but a new
|
|
276
|
+
message would have gone through.
|
|
277
|
+
|
|
278
|
+
This is not an exotic collision. It happens whenever a short name and a longer
|
|
279
|
+
one built on it coexist — `muster` / `muster-b1`, `ferry` / `ferry-9b`.
|
|
280
|
+
|
|
281
|
+
**It is kept because it cannot be fixed from the listing alone.** The hazard is
|
|
282
|
+
indistinguishable from the behaviour directly above it in this document: `auth`
|
|
283
|
+
→ `auth-refactor` and `muster` → `muster-b1` are the same shape, the same single
|
|
284
|
+
match, and in both cases the input is nobody's current name. The only fact that
|
|
285
|
+
separates them is that a session named exactly `muster` *existed and has gone*,
|
|
286
|
+
and a peer listing cannot carry that fact, because the session's absence is the
|
|
287
|
+
whole premise. Any rule that refuses the second refuses the first: removing the
|
|
288
|
+
prefix pass is verifiable in one edit, and it fails
|
|
289
|
+
`resolve > unambiguous prefix resolves` alongside the defect's own case.
|
|
290
|
+
|
|
291
|
+
**The form that cannot misroute is `canonical_id`.** Pass 2 matches against the
|
|
292
|
+
slug and the qualified form, never against `canonicalId`, so a canonical id can
|
|
293
|
+
never prefix-match a stranger — it resolves to the session it names or refuses
|
|
294
|
+
as unknown:
|
|
295
|
+
|
|
296
|
+
```
|
|
297
|
+
muster gone , resolve("muster") -> OK, muster-b1
|
|
298
|
+
muster gone , resolve("claude-code:muster.18ddaa21-…") -> REFUSED unknown
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
A loud refusal is the outcome a caller wants here. **When you hold a session's
|
|
302
|
+
durable id, address it by `canonical_id` rather than by its bare name** — a
|
|
303
|
+
short name is for a human typing, not for a program that already knows exactly
|
|
304
|
+
which session it means. `expect_id` does not substitute: it is compared after
|
|
305
|
+
resolution and only ever rejects, so it guards the send without selecting the
|
|
306
|
+
recipient.
|
|
307
|
+
|
|
152
308
|
## Changing this format
|
|
153
309
|
|
|
154
310
|
The address format is a contract between Tin Can and tools built against it. A
|
|
@@ -156,6 +312,14 @@ change to slugify, suffix derivation, collision handling, or resolution produces
|
|
|
156
312
|
addresses that resolve in one tool and fail silently in the other — no error,
|
|
157
313
|
just a message delivered nowhere.
|
|
158
314
|
|
|
315
|
+
**Who is built against this today:**
|
|
316
|
+
[Muster](https://github.com/BrutalSystems/muster) launches agent sessions and
|
|
317
|
+
returns a peer record Tin Can must resolve. It does not import Tin Can — the two
|
|
318
|
+
agree on this document and on `test/fixtures/canonical-id.json`, which Muster
|
|
319
|
+
copies verbatim. So a change here is not local to this repo, and the person
|
|
320
|
+
making it is responsible for coordinating the release, not for discovering
|
|
321
|
+
afterwards that the other tool disagreed.
|
|
322
|
+
|
|
159
323
|
So:
|
|
160
324
|
|
|
161
325
|
- **A change to any behaviour described here is a breaking change**, and ships
|
package/CONTRACT_PROVENANCE.md
CHANGED
|
@@ -299,3 +299,74 @@ 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.
|
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.6.0",
|
|
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
|
}
|