instar 1.3.454 → 1.3.455
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/dist/cli.js +9 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/test-as-self.d.ts +17 -0
- package/dist/commands/test-as-self.d.ts.map +1 -1
- package/dist/commands/test-as-self.js +57 -0
- package/dist/commands/test-as-self.js.map +1 -1
- package/dist/permissions/testing/SlackScenarioHarness.d.ts +97 -5
- package/dist/permissions/testing/SlackScenarioHarness.d.ts.map +1 -1
- package/dist/permissions/testing/SlackScenarioHarness.js +190 -3
- package/dist/permissions/testing/SlackScenarioHarness.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +4 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +36 -1
- package/dist/server/routes.js.map +1 -1
- package/dist/threadline/ThreadlineBootstrap.d.ts +0 -3
- package/dist/threadline/ThreadlineBootstrap.d.ts.map +1 -1
- package/dist/threadline/ThreadlineBootstrap.js +1 -35
- package/dist/threadline/ThreadlineBootstrap.js.map +1 -1
- package/dist/threadline/client/ThreadlineClient.d.ts +32 -0
- package/dist/threadline/client/ThreadlineClient.d.ts.map +1 -1
- package/dist/threadline/client/ThreadlineClient.js +112 -13
- package/dist/threadline/client/ThreadlineClient.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +78 -78
- package/src/scaffold/templates.ts +4 -1
- package/upgrades/1.3.455.md +46 -0
- package/upgrades/side-effects/slack-test-as-self-harness.md +155 -0
- package/upgrades/side-effects/threadline-dup-identity-pr1-resolver.md +162 -0
- package/upgrades/side-effects/threadline-dup-identity-pr2-orphan-removal.md +89 -0
|
@@ -1404,11 +1404,14 @@ Create worktrees for collaborator repos with \`instar worktree create <branch>\`
|
|
|
1404
1404
|
instar test-as-self --no-roundtrip # deploy + verify only (no bot needed)
|
|
1405
1405
|
instar test-as-self --bot-token <secret-drop-id> # + a real Telegram round-trip via a throwaway bot
|
|
1406
1406
|
instar test-as-self --keep # leave the throwaway running for inspection
|
|
1407
|
+
instar test-as-self --slack # Slack permission demonstration (each principal+request → expected decision + audit entry)
|
|
1407
1408
|
\`\`\`
|
|
1408
1409
|
|
|
1409
1410
|
**Structural guards (you cannot foot-gun these):** \`--target\` can never be your canonical agent home or a protected agent (e.g. Bob); \`--bot-token\` refuses a raw token on the command line — pass a Secret Drop ID and the token is retrieved in-memory, never via argv. It emits a single JSON report; exit 0 = all steps PASS.
|
|
1410
1411
|
|
|
1411
|
-
|
|
1412
|
+
**\`--slack\` — the test-as-self-for-Slack demonstration (credential-free):** extends the throwaway-agent primitive from "is the agent alive?" to "does it enforce the RIGHT decision for each (principal, request) pair?". It runs the deterministic scenario suite through the SAME observer the live Slack adapter calls (resolver → permission gate → decision ledger) and asserts BOTH the verdict AND that the matching audit/ledger entry landed — "verified, not narrated". No Slack tokens, no throwaway deploy. The same suite is reachable over HTTP: \`GET /permissions/scenario-suite\` (logic-only view) and \`POST /permissions/scenario-suite/run\` (audit-asserting). Exit 0 = every row produced its expected decision AND its audit entry.
|
|
1413
|
+
|
|
1414
|
+
**Proactive trigger:** when you're about to ship or just shipped a change touching the deploy/lifeline/server-startup path, run this against a throwaway home first — don't guess from logs. When you touch the Slack org permission system (\`src/permissions/\`), run \`instar test-as-self --slack\` (or \`POST /permissions/scenario-suite/run\`) to prove the gate still enforces every (principal, request) row.
|
|
1412
1415
|
`;
|
|
1413
1416
|
|
|
1414
1417
|
if (hasTelegram) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
When two registrations for the same agent name existed on the Threadline relay — a live one and a
|
|
9
|
+
stale leftover (a "dead twin") — a sender resolving that name could land on the dead one, and its
|
|
10
|
+
messages silently vanished (delivered-but-never-received). The fix is in the client name resolver
|
|
11
|
+
(`ThreadlineClient`): each known agent now carries a live/offline flag (sourced from the live
|
|
12
|
+
presence status the relay already reports), name resolution **prefers the single live registration**,
|
|
13
|
+
re-discovers a stale cache so a dead twin can't win, and — when two registrations are *genuinely*
|
|
14
|
+
both live (a real two-machine agent, or an impostor) — surfaces a disambiguation prompt instead of
|
|
15
|
+
silently guessing. A companion change removes the dead code (`loadOrCreateIdentityKeys`) that minted
|
|
16
|
+
the orphan identity which became the dead twin in the first place, so no new duplicates are created
|
|
17
|
+
fleet-wide. No relay deploy is required — the relay already reported the right information; the client
|
|
18
|
+
was discarding it.
|
|
19
|
+
|
|
20
|
+
## What to Tell Your User
|
|
21
|
+
|
|
22
|
+
If another agent reported that messages to you were "going through on their side but never arriving"
|
|
23
|
+
— the classic symptom of a wedged agent-to-agent channel — this fixes it: your peer's software now
|
|
24
|
+
resolves your name to your live address instead of a stale duplicate. You don't need to do anything;
|
|
25
|
+
it takes effect after your agent updates and restarts. In the meantime, a peer can always reach you
|
|
26
|
+
by addressing your exact routing fingerprint instead of your name. If a peer ever gets an "ambiguous
|
|
27
|
+
agent name" prompt, that's intentional — it means two live registrations share your name, and they
|
|
28
|
+
should pick by fingerprint.
|
|
29
|
+
|
|
30
|
+
## Summary of New Capabilities
|
|
31
|
+
|
|
32
|
+
This is a reliability/correctness fix, not a new feature — agent-to-agent name resolution now lands
|
|
33
|
+
on the live registration and never silently delivers to a dead duplicate. Agents also stop creating
|
|
34
|
+
an unused orphan identity file on boot.
|
|
35
|
+
|
|
36
|
+
## Evidence
|
|
37
|
+
|
|
38
|
+
- 17 unit tests drive the real on-path resolver (`ThreadlineClient.resolveAgent` / `findAgentByName`):
|
|
39
|
+
live-vs-dead → resolves live; two-online → surfaces ambiguity; offline-only cache → re-discovers;
|
|
40
|
+
merge retains crypto keys; rate-limit early-resolve; partial-match parity.
|
|
41
|
+
- 1 relay-backed end-to-end test stands up a real relay with a live + a dead "echo" and asserts a
|
|
42
|
+
sender resolves the name to the live fingerprint, never the dead twin.
|
|
43
|
+
- 12 `ThreadlineBootstrap` unit tests (incl. "does NOT create identity-keys.json" + a CI guard that
|
|
44
|
+
the orphan-minting cannot be re-introduced). Typecheck clean; 1693 existing threadline tests green.
|
|
45
|
+
- Spec (converged iter 3, approved): `docs/specs/threadline-duplicate-identity-resolution.md`;
|
|
46
|
+
side-effects review with independent second-pass CONCUR.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Side-Effects Review — test-as-self for Slack (permission demonstration harness)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `slack-test-as-self-harness`
|
|
4
|
+
**Date:** `2026-06-09`
|
|
5
|
+
**Author:** `Echo (instar-dev agent)`
|
|
6
|
+
**Second-pass reviewer:** `not required (Tier 1)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Pillar 4 / milestone 4 of the Slack org permission system (`SLACK-ORG-INTEGRATION-SPEC.md` §8).
|
|
11
|
+
This is a **test/demonstration harness over the EXISTING `SlackPermissionGate`** — it adds no
|
|
12
|
+
new runtime decision logic. It extends the existing `SlackScenarioHarness` (six gate-direct rows,
|
|
13
|
+
decision-only) with: (a) two more scenario rows — a granted-member-floor (→ allow/`floor-granted`)
|
|
14
|
+
and an unregistered outsider (→ refuse/`unregistered`) — completing the deterministic
|
|
15
|
+
credential-free subset; (b) a `StaticGrantStore` (deterministic fixture mirroring
|
|
16
|
+
`MandateBackedGrantStore`) and a `CastUserLookup` registry resolver, both plain test doubles that
|
|
17
|
+
feed the existing gate; (c) `runAuditedScenarioSuite`, which drives every row through the SAME
|
|
18
|
+
`SlackPermissionObserver` the live `SlackAdapter._handleMessage` calls (resolver → gate →
|
|
19
|
+
`PermissionDecisionLedger`) and asserts BOTH the verdict AND the matching audit-ledger entry
|
|
20
|
+
("verified, not narrated"). It exposes an executable surface: `POST /permissions/scenario-suite/run`
|
|
21
|
+
and `instar test-as-self --slack`. Files: `src/permissions/testing/SlackScenarioHarness.ts`,
|
|
22
|
+
`src/server/routes.ts`, `src/commands/test-as-self.ts`, `src/cli.ts`, `src/scaffold/templates.ts`,
|
|
23
|
+
plus three new test files and two updated test files.
|
|
24
|
+
|
|
25
|
+
## Decision-point inventory
|
|
26
|
+
|
|
27
|
+
This change touches NO runtime decision point. Every component invokes the existing gate and
|
|
28
|
+
reads the existing ledger; nothing gates, blocks, filters, or constrains agent behavior.
|
|
29
|
+
|
|
30
|
+
- `SlackPermissionGate.evaluate` — **pass-through** — unchanged; the harness only calls it.
|
|
31
|
+
- `buildSliceZeroGate()` (test helper) — **modify** — now constructs the gate with a
|
|
32
|
+
`StaticGrantStore`. Deny-by-default for everyone except the single Grace fixture, so no
|
|
33
|
+
existing row's verdict changes; it solely enables the `floor-granted` path for the new row.
|
|
34
|
+
- `StaticGrantStore` / `CastUserLookup` — **add** — test fixtures (a `GrantStore` and a
|
|
35
|
+
`UserLookup` test-double). No authority; they only supply inputs to the existing gate.
|
|
36
|
+
- `POST /permissions/scenario-suite/run` / `test-as-self --slack` — **add** — read-only/dev
|
|
37
|
+
executable wrappers that run the suite into a throwaway temp dir and return a report.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 1. Over-block
|
|
42
|
+
|
|
43
|
+
No block/allow surface — over-block not applicable. The harness asserts the existing gate's
|
|
44
|
+
verdicts; it never itself rejects any input. (The gate's own over/under-block behavior is what
|
|
45
|
+
the harness *measures*, and is governed by the gate's own spec, unchanged here.)
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 2. Under-block
|
|
50
|
+
|
|
51
|
+
No block/allow surface — under-block not applicable. One harness-level honesty note: if a future
|
|
52
|
+
gate change altered a verdict, the suite would catch it as a row mismatch (that is the point — it
|
|
53
|
+
is a regression wall). The audit-assertion half also catches the subtler failure where a verdict
|
|
54
|
+
is returned but the ledger write silently fails.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 3. Level-of-abstraction fit
|
|
59
|
+
|
|
60
|
+
Correct layer. This is a **demonstration/test harness** sitting ABOVE the gate, deliberately
|
|
61
|
+
driving the production observe path (`SlackPermissionObserver`) rather than re-implementing it.
|
|
62
|
+
It reuses the real `SlackPermissionGate`, real `SlackPermissionObserver`, real
|
|
63
|
+
`SlackPrincipalResolver`, and real `PermissionDecisionLedger`. The `StaticGrantStore` is the
|
|
64
|
+
deterministic, credential-free mirror of `MandateBackedGrantStore` (the live A4 path needs a
|
|
65
|
+
PIN-gated signed mandate that cannot run in CI) — it implements the SAME `GrantStore` interface
|
|
66
|
+
so the gate's `floor-granted` branch is exercised identically; only the grant's provenance
|
|
67
|
+
differs (a fixture vs a signed mandate), which is exactly the deterministic-vs-live seam (§8.3).
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 4. Signal vs authority compliance
|
|
72
|
+
|
|
73
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
74
|
+
|
|
75
|
+
- [x] **No — this change has no block/allow surface.**
|
|
76
|
+
|
|
77
|
+
The harness/route/CLI hold no authority. They invoke the existing gate and read its ledger. The
|
|
78
|
+
`StaticGrantStore` is a test fixture feeding the existing gate, not a new authority. Nothing here
|
|
79
|
+
makes a brittle blocking decision.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 5. Interactions
|
|
84
|
+
|
|
85
|
+
- **Shadowing:** none. The harness runs the gate in isolation; it does not sit in any live
|
|
86
|
+
message path. (The full-pipeline test invokes `_handleMessage` directly in a test process with
|
|
87
|
+
a fake bot token — it never touches a real Slack workspace.)
|
|
88
|
+
- **Double-fire:** none. The audit-asserting run uses its OWN ledger in a fresh temp dir, so it
|
|
89
|
+
cannot collide with the live `/permissions/decisions` ledger or any live observer.
|
|
90
|
+
- **Races:** none. Each suite run is hermetic (its own temp state dir).
|
|
91
|
+
- **Feedback loops:** none. The behavior baseline (`RelationshipBehaviorStore`) is NOT wired into
|
|
92
|
+
the harness observer, so running the suite writes no durable baseline that could feed back into
|
|
93
|
+
the live anomaly scorer.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 6. External surfaces
|
|
98
|
+
|
|
99
|
+
- **Other agents / users / external systems:** none. No real Slack tokens, no network calls to
|
|
100
|
+
Slack (the fake-token reaction/user-lookup calls inside `_handleMessage` fail closed with
|
|
101
|
+
`invalid_auth` and are swallowed — they are best-effort UI sugar, not assertions).
|
|
102
|
+
- **Persistent state:** none in the running agent. The audit-asserting suite writes its ledger
|
|
103
|
+
into `os.tmpdir()`, never `ctx.config.stateDir`; an integration test explicitly asserts the
|
|
104
|
+
returned `ledgerPath` is not under the route's configured state dir.
|
|
105
|
+
- **CLAUDE.md template:** the agent template's `## Test-As-Self` section gains a documented
|
|
106
|
+
`--slack` line + the two HTTP endpoints (Agent Awareness Standard). New agents receive it via
|
|
107
|
+
`init`; this is a doc surface, not runtime behavior — see Migration Parity note in the conclusion.
|
|
108
|
+
- **Timing/runtime conditions we don't control:** none — the suite is fully deterministic
|
|
109
|
+
(heuristic classifier + static baselines + static grants + injectable nothing-from-network).
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 7. Rollback cost
|
|
114
|
+
|
|
115
|
+
Pure additive code + tests + docs. Rollback = revert the commit and ship a patch. No persistent
|
|
116
|
+
state to migrate (the suite's only writes are throwaway temp dirs). No agent-state repair. No
|
|
117
|
+
user-visible regression during the rollback window (this is a maintainer/dev demonstration tool,
|
|
118
|
+
not a user-facing capability). The one behavior tweak (`buildSliceZeroGate` gains a grant store)
|
|
119
|
+
is confined to the test harness and is deny-by-default for all but the Grace fixture.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Conclusion
|
|
124
|
+
|
|
125
|
+
The review found no decision-point surface and no signal-vs-authority concern: every component
|
|
126
|
+
invokes the existing gate and reads the existing ledger. The change's value is precisely that it
|
|
127
|
+
makes the gate's governance *watchable and self-verified* — each (principal, request) row produces
|
|
128
|
+
its expected verdict AND its matching audit entry, through the same observer the live adapter uses.
|
|
129
|
+
The deterministic-vs-live seam is honest (the live-Slack-workspace layer is explicitly out of scope
|
|
130
|
+
and remains operator-driven). All three test tiers are green; `tsc` and `lint` are clean. Clear to
|
|
131
|
+
commit (Tier 1). **Migration Parity note:** the only agent-installed artifact touched is the
|
|
132
|
+
CLAUDE.md template doc line for a dev-only tool — the route + CLI ship as compiled dist that every
|
|
133
|
+
agent receives automatically on update; no `PostUpdateMigrator` entry is warranted for a doc line
|
|
134
|
+
on a maintainer-only surface (consistent with how the existing `/permissions/scenario-suite` route
|
|
135
|
+
is treated).
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Second-pass review (if required)
|
|
140
|
+
|
|
141
|
+
Not required — Tier 1 (a test/demonstration harness with no block/allow surface and no session
|
|
142
|
+
lifecycle / gate / sentinel / watchdog runtime change).
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Evidence pointers
|
|
147
|
+
|
|
148
|
+
- `tests/unit/slack-scenario-audit-harness.test.ts` — 16 tests (cast, fixtures, both decision
|
|
149
|
+
boundaries per row, audit-entry assertion, durable-ledger read-back).
|
|
150
|
+
- `tests/integration/slack-permission-pipeline.test.ts` — 3 tests (every row through
|
|
151
|
+
`_handleMessage` → observer → ledger; granted-member + unregistered-outsider live-path rows).
|
|
152
|
+
- `tests/integration/permissions-routes.test.ts` — updated; `GET /permissions/scenario-suite`
|
|
153
|
+
(8 rows) + `POST /permissions/scenario-suite/run` (verdict AND audit per row).
|
|
154
|
+
- `tests/e2e/slack-scenario-suite-route.test.ts` — 2 tests (feature alive: 200 + full green report).
|
|
155
|
+
- CLI smoke: `node dist/cli.js test-as-self --slack` → all 8 rows `audit✓`, `VERDICT: PASS`, exit 0.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Side-Effects Review — Threadline duplicate-identity resolver (PR1: client live-preference)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `threadline-dup-identity-pr1-resolver`
|
|
4
|
+
**Date:** `2026-06-09`
|
|
5
|
+
**Author:** `Instar Agent (echo)`
|
|
6
|
+
**Second-pass reviewer:** `independent reviewer subagent — CONCUR (Phase 5; high-risk: name→fingerprint dispatch resolution)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Closes the duplicate-identity silent-drop class on the exact path a sender uses
|
|
11
|
+
(`threadline_send` → `ThreadlineClient.resolveAgent` → `findAgentByName`). Driven by the
|
|
12
|
+
converged spec `docs/specs/threadline-duplicate-identity-resolution.md` (changes A/B/C).
|
|
13
|
+
Single file touched: `src/threadline/client/ThreadlineClient.ts`. (A) `KnownAgent` gains an
|
|
14
|
+
`online` flag, merged — never replaced — from the relay's `discover_result.status` (the relay
|
|
15
|
+
already derives that status from live presence; the client was discarding it). (B)
|
|
16
|
+
`findAgentByName` prefers the single live registration among same-name rows in both the exact and
|
|
17
|
+
partial branches, and still throws ambiguity when ≥2 are online (never silently picks). (C)
|
|
18
|
+
`resolveAgent` re-discovers once (cooldown-gated, rate-limit-aware) when the cached match is
|
|
19
|
+
offline, so a stale dead twin can be superseded by a live one. Tests: 17 unit on the real
|
|
20
|
+
resolver + 1 relay-backed e2e (live-vs-dead echo → resolves to live).
|
|
21
|
+
|
|
22
|
+
## Decision-point inventory
|
|
23
|
+
|
|
24
|
+
- `ThreadlineClient.findAgentByName` (name → fingerprint resolution) — **modify** — adds a
|
|
25
|
+
live-registration preference before the existing ambiguity throw; the throw is preserved for
|
|
26
|
+
genuinely-ambiguous (≥2 online, or all offline >1) cases.
|
|
27
|
+
- `ThreadlineClient.resolveAgent` (cache → re-discover orchestration) — **modify** — adds one
|
|
28
|
+
cooldown-gated re-discovery when the resolved match is offline.
|
|
29
|
+
- `ThreadlineClient.discover` (relay discovery wait) — **modify** — adds a rate-limit-filtered
|
|
30
|
+
early-resolve so a throttled re-discovery fails fast instead of hanging to the 10s timeout.
|
|
31
|
+
- `ThreadlineClient` discover-result ingestion — **modify** — merge (not replace) so a keyless
|
|
32
|
+
discovery frame cannot strip crypto keys; map `status` → `online`.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 1. Over-block
|
|
37
|
+
|
|
38
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
39
|
+
|
|
40
|
+
No new block/allow surface. The one place resolution can now *fail* where it previously
|
|
41
|
+
"succeeded" is when ≥2 same-name rows are simultaneously online — but that failure is the
|
|
42
|
+
existing ambiguity throw, which is the correct, pre-existing behavior (require `name:fingerprint`).
|
|
43
|
+
A legitimately-multi-machine agent (two keys, both online) now gets that loud ambiguity rather
|
|
44
|
+
than an arbitrary pick — intended, and the error text points at the disambiguation syntax + saved
|
|
45
|
+
nicknames. No legitimate single-recipient send is newly rejected: a live-vs-dead pair now resolves
|
|
46
|
+
(previously it threw or silently dropped); a single offline peer still resolves (returns the
|
|
47
|
+
offline fingerprint, preserving offline-queue semantics).
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 2. Under-block
|
|
52
|
+
|
|
53
|
+
**What failure modes does this still miss?**
|
|
54
|
+
|
|
55
|
+
Name discovery remains trust-on-first-use and unauthenticated at the relay (the `name` is not
|
|
56
|
+
cryptographically bound — pre-existing, documented in the spec's §Security). If the real agent is
|
|
57
|
+
genuinely offline AND only an impostor with the same name is online, the exactly-one-online rule
|
|
58
|
+
will prefer the impostor — but ONLY during a real outage of the genuine agent (an impostor cannot
|
|
59
|
+
make itself the *only* online "echo" while the real one is connected, because presence is keyed per
|
|
60
|
+
fingerprint). This is a pre-existing TOFU property the change makes more deterministic in that
|
|
61
|
+
narrow window; the spec records it and leaves room for a future `verified`-fingerprint tiebreak.
|
|
62
|
+
Not closed here by design.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 3. Level-of-abstraction fit
|
|
67
|
+
|
|
68
|
+
**Is this at the right layer?**
|
|
69
|
+
|
|
70
|
+
Yes — and this is the headline correction from convergence. v1 of the spec put the fix at the
|
|
71
|
+
relay's `RegistryStore.search()` ORDER BY, which is OFF the instar send path (the client ingests
|
|
72
|
+
discovery into a fingerprint-keyed Map and resolves locally). The fix belongs in the client
|
|
73
|
+
resolver — the lowest layer that actually decides name → fingerprint for a send — and it CONSUMES
|
|
74
|
+
a signal the relay already produces (live `status`) rather than re-implementing liveness. No new
|
|
75
|
+
parallel gate; it feeds off existing infrastructure.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 4. Signal vs authority compliance
|
|
80
|
+
|
|
81
|
+
**Required reference:** docs/signal-vs-authority.md
|
|
82
|
+
|
|
83
|
+
- [x] No — this change produces/consumes a selection signal; it has no brittle blocking authority.
|
|
84
|
+
|
|
85
|
+
The change is a *selection preference* (prefer the live registration). It never silently picks
|
|
86
|
+
among multiple live registrations — it surfaces the existing ambiguity throw for the operator/caller
|
|
87
|
+
to disambiguate. No message is blocked, dropped, or rewritten. Liveness is sourced from the relay's
|
|
88
|
+
live-presence-derived `status`, not a brittle local heuristic.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 5. Interactions
|
|
93
|
+
|
|
94
|
+
- **Shadowing:** the online-preference runs *before* the existing ambiguity throw in
|
|
95
|
+
`findAgentByName`; it does not shadow the throw — it narrows when the throw fires (only ≥2 online,
|
|
96
|
+
or >1 all-offline). The `fingerprintPrefix` (`name:fingerprint`) path still wins ahead of both,
|
|
97
|
+
unchanged.
|
|
98
|
+
- **Double-fire:** the offline re-discovery is gated by a per-name 30s cooldown
|
|
99
|
+
(`lastRediscoverByName`), so a burst of sends to an offline target cannot re-query the relay
|
|
100
|
+
repeatedly; the existing cache-miss re-discovery path is unchanged (not cooldown-gated).
|
|
101
|
+
- **Races:** `discover()` now registers an `error` listener alongside `discover-result`; both share
|
|
102
|
+
a `settled` guard + `cleanup()` so a late frame after either fires is a no-op. The error listener
|
|
103
|
+
filters strictly on `code === RATE_LIMITED`, so an unrelated error frame (e.g. a concurrent send's
|
|
104
|
+
`RECIPIENT_OFFLINE`) cannot spuriously resolve discovery (unit-tested).
|
|
105
|
+
- **Feedback loops:** none — discovery output feeds resolution, not back into discovery (the
|
|
106
|
+
cooldown bounds the one re-query).
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 6. External surfaces
|
|
111
|
+
|
|
112
|
+
- **Other agents:** the merge-not-replace fix actually PREVENTS a latent regression — a keyless
|
|
113
|
+
discovery frame no longer strips a peer's cached crypto keys, so E2E `send()` to a
|
|
114
|
+
previously-keyed peer is not downgraded to plaintext. Name-resolved sends to freshly-discovered
|
|
115
|
+
peers continue to use the relay plaintext-authenticated path (transport TLS + Ed25519), unchanged.
|
|
116
|
+
- **External systems:** none. No relay deploy (the relay already sends live status). No HTTP route
|
|
117
|
+
added or changed. No config knob, no dashboard surface, no CLAUDE.md template change (transparent
|
|
118
|
+
correctness fix).
|
|
119
|
+
- **Persistent state:** none — `knownAgents` and `lastRediscoverByName` are process-local, never
|
|
120
|
+
persisted. No DB/ledger/migration.
|
|
121
|
+
- **Timing:** the offline re-discovery adds at most one discovery round-trip (≤ the 10s discover
|
|
122
|
+
timeout, and fails fast on rate-limit) per resolve when the cached match is offline and the
|
|
123
|
+
cooldown allows.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 7. Rollback cost
|
|
128
|
+
|
|
129
|
+
Pure client-side code change in one file. Rollback = revert the PR; `findAgentByName`/`resolveAgent`
|
|
130
|
+
return to throw-on-ambiguity / cache-hit behavior. No persistent state, no data migration, no agent
|
|
131
|
+
state repair. Reaches the fleet on the next server/session restart after auto-update activates (the
|
|
132
|
+
in-memory cache self-heals on restart). No user-visible regression during the rollback window.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Conclusion
|
|
137
|
+
|
|
138
|
+
The review produced no design changes beyond what convergence already folded in. The change is a
|
|
139
|
+
narrowly-scoped, single-file client resolver fix that consumes an existing relay signal, never
|
|
140
|
+
silently picks among live registrations, and carries a comprehensive on-path test suite (17 unit +
|
|
141
|
+
1 relay-backed e2e, all green; 1693 existing threadline tests unaffected; typecheck clean). High-risk
|
|
142
|
+
classification (name→fingerprint dispatch resolution) triggers the Phase-5 second-pass review below.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Second-pass review (if required)
|
|
147
|
+
|
|
148
|
+
**Reviewer:** independent reviewer subagent
|
|
149
|
+
**Independent read of the artifact: concur**
|
|
150
|
+
|
|
151
|
+
Audited the actual implementation against the spec, both test files, the relay wire contract
|
|
152
|
+
(`relay/types.ts`), and the route callsite (`routes.ts:17500-17626`). Confirmed grounded in code:
|
|
153
|
+
the merge provably retains `publicKey`/`x25519PublicKey` (keyless frame → `??` keeps existing);
|
|
154
|
+
`pickSingleOnline` returns the single online match and the ambiguity throw fires for ≥2-online in
|
|
155
|
+
BOTH the exact and partial branches; `resolveAgent` re-discovers only on an offline match, returns
|
|
156
|
+
the offline fingerprint when still offline (no 404), and propagates the ambiguity throw (caught at
|
|
157
|
+
the route as a 500 with disambiguation guidance); `discover()` early-resolves only on
|
|
158
|
+
`code===RATE_LIMITED`. Four findings, all MINOR: the `online===undefined` case (now covered by added
|
|
159
|
+
tests), a `status`-absent frame mapping to `online:false` (harmless — `status` is non-optional on the
|
|
160
|
+
wire), a vestigial `status` property on the stored object (no consumer reads it), and an artifact
|
|
161
|
+
test-count miscount (now corrected to 17). No over-block, no authority creep, no external-surface
|
|
162
|
+
change. Clear to ship.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Side-Effects Review — Threadline orphan-identity removal (PR2: stop minting identity-keys.json)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `threadline-dup-identity-pr2-orphan-removal`
|
|
4
|
+
**Date:** `2026-06-09`
|
|
5
|
+
**Author:** `Instar Agent (echo)`
|
|
6
|
+
**Second-pass reviewer:** `not required (Tier 1 — dead-code removal, no decision surface)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Removes `ThreadlineBootstrap.loadOrCreateIdentityKeys` and the now-dead `identityKeys` field
|
|
11
|
+
from `ThreadlineBootstrapResult` (change D of the converged spec
|
|
12
|
+
`docs/specs/threadline-duplicate-identity-resolution.md`). That function minted an orphan
|
|
13
|
+
`threadline/identity-keys.json` keypair on every agent boot, independent of the canonical
|
|
14
|
+
`identity.json` — the exact artifact that became the dead "echo" twin polluting the relay. Its
|
|
15
|
+
output was consumed nowhere (verified: zero `.identityKeys` readers in `src/` and `tests/`;
|
|
16
|
+
`server.ts` never destructures it). The handshake and relay-client paths source identity via
|
|
17
|
+
`HandshakeManager.getOrCreateIdentity()` / `IdentityManager` (canonical `identity.json`) and are
|
|
18
|
+
untouched. Files: `src/threadline/ThreadlineBootstrap.ts` (remove function, field, call, return,
|
|
19
|
+
two now-unused imports, the orphan-only constant) + `tests/unit/threadline/ThreadlineBootstrap.test.ts`
|
|
20
|
+
(delete the orphan-file persistence/permissions/corrupted-regeneration tests, drop the
|
|
21
|
+
`result.identityKeys` assertions, add a "does NOT create identity-keys.json" test + a CI guard that
|
|
22
|
+
`loadOrCreateIdentityKeys` is absent from the file).
|
|
23
|
+
|
|
24
|
+
## Decision-point inventory
|
|
25
|
+
|
|
26
|
+
- No decision point. This removes a write-only code path (`loadOrCreateIdentityKeys`) that gated
|
|
27
|
+
nothing, blocked nothing, and whose output fed nothing. Pure dead-code removal.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 1. Over-block
|
|
32
|
+
|
|
33
|
+
No block/allow surface — over-block not applicable.
|
|
34
|
+
|
|
35
|
+
## 2. Under-block
|
|
36
|
+
|
|
37
|
+
No block/allow surface — under-block not applicable.
|
|
38
|
+
|
|
39
|
+
## 3. Level-of-abstraction fit
|
|
40
|
+
|
|
41
|
+
Correct layer: the orphan keypair was a vestige of an older identity model superseded by
|
|
42
|
+
`IdentityManager` (canonical `identity.json`). Removing the minting function (not the on-disk file)
|
|
43
|
+
is the minimal, right-altitude change. #479 deliberately fenced off this removal as needing its own
|
|
44
|
+
spec ("deleting risks the handshake path"); this PR is that spec's change D, and the handshake-path
|
|
45
|
+
risk is rebutted with evidence — `HandshakeManager.getOrCreateIdentity()` reads
|
|
46
|
+
`{stateDir}/threadline/identity.json`, never the orphan `identity-keys.json` (verified).
|
|
47
|
+
|
|
48
|
+
## 4. Signal vs authority compliance
|
|
49
|
+
|
|
50
|
+
- [x] No — this change has no block/allow surface (dead-code removal).
|
|
51
|
+
|
|
52
|
+
## 5. Interactions
|
|
53
|
+
|
|
54
|
+
- **Shadowing / double-fire:** none — the removed function had no callers beyond the single
|
|
55
|
+
bootstrap call site, and its result flowed nowhere.
|
|
56
|
+
- **Races:** none — it only wrote an unused file; removing it removes one boot-time fs write.
|
|
57
|
+
- **Feedback loops:** none.
|
|
58
|
+
- Existing `identity-keys.json` files already on disk become permanently inert (they already were —
|
|
59
|
+
nothing read them since #479). They are NOT deleted by this change (a destructive fleet file-sweep
|
|
60
|
+
is tracked separately, out of scope).
|
|
61
|
+
|
|
62
|
+
## 6. External surfaces
|
|
63
|
+
|
|
64
|
+
- **Other agents / relay:** strictly improves coherence — no new orphan identity is ever minted, so
|
|
65
|
+
the duplicate-registration source is closed fleet-wide. No new orphan can be registered on the relay.
|
|
66
|
+
- **Persistent state:** stops creating one unused file per agent. Existing inert files left in place
|
|
67
|
+
(no migration). No DB/ledger change.
|
|
68
|
+
- **External systems / config / dashboard / CLAUDE.md template:** none.
|
|
69
|
+
|
|
70
|
+
## 7. Rollback cost
|
|
71
|
+
|
|
72
|
+
Pure code-deletion. Rollback = revert the PR; `loadOrCreateIdentityKeys` is reinstated and resumes
|
|
73
|
+
writing the (unused) file. No data migration, no agent-state repair, no user-visible regression.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Conclusion
|
|
78
|
+
|
|
79
|
+
Dead-code removal closing the source of the duplicate-identity artifact, paired with the companion
|
|
80
|
+
client resolver fix (PR1). No decision surface, no external behavior change beyond ceasing to write
|
|
81
|
+
an unused file. The #479 handshake-risk fence is rebutted with verified evidence. 12 bootstrap unit
|
|
82
|
+
tests green (including the new "no orphan file" assertion + CI guard); typecheck clean; no other
|
|
83
|
+
consumer of the removed field exists. Tier 1: small, low-risk, second-pass not required.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Second-pass review (if required)
|
|
88
|
+
|
|
89
|
+
**Reviewer:** not required (Tier 1 — dead-code removal, no block/allow or lifecycle decision surface).
|