instar 1.3.506 → 1.3.508
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/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +6 -12
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +16 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/coordination/AnchoredAuthorization.d.ts +66 -0
- package/dist/coordination/AnchoredAuthorization.d.ts.map +1 -0
- package/dist/coordination/AnchoredAuthorization.js +71 -0
- package/dist/coordination/AnchoredAuthorization.js.map +1 -0
- package/dist/core/ExternalOperationGate.d.ts +12 -0
- package/dist/core/ExternalOperationGate.d.ts.map +1 -1
- package/dist/core/ExternalOperationGate.js +6 -0
- package/dist/core/ExternalOperationGate.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +27 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +5 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/messaging/TelegramAdapter.d.ts +7 -0
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +9 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +278 -19
- package/dist/server/routes.js.map +1 -1
- package/dist/threadline/ContentClassifier.d.ts +15 -0
- package/dist/threadline/ContentClassifier.d.ts.map +1 -1
- package/dist/threadline/ContentClassifier.js +39 -0
- package/dist/threadline/ContentClassifier.js.map +1 -1
- package/dist/threadline/ConversationStore.d.ts +32 -0
- package/dist/threadline/ConversationStore.d.ts.map +1 -1
- package/dist/threadline/ConversationStore.js +74 -0
- package/dist/threadline/ConversationStore.js.map +1 -1
- package/dist/threadline/NegotiatorGate.d.ts +88 -0
- package/dist/threadline/NegotiatorGate.d.ts.map +1 -0
- package/dist/threadline/NegotiatorGate.js +148 -0
- package/dist/threadline/NegotiatorGate.js.map +1 -0
- package/dist/threadline/NegotiatorLease.d.ts +156 -0
- package/dist/threadline/NegotiatorLease.d.ts.map +1 -0
- package/dist/threadline/NegotiatorLease.js +202 -0
- package/dist/threadline/NegotiatorLease.js.map +1 -0
- package/dist/threadline/ThreadlineEndpoints.d.ts +2 -1
- package/dist/threadline/ThreadlineEndpoints.d.ts.map +1 -1
- package/dist/threadline/ThreadlineEndpoints.js +21 -1
- package/dist/threadline/ThreadlineEndpoints.js.map +1 -1
- package/dist/threadline/ThreadlineMCPServer.d.ts +8 -0
- package/dist/threadline/ThreadlineMCPServer.d.ts.map +1 -1
- package/dist/threadline/ThreadlineMCPServer.js +14 -1
- package/dist/threadline/ThreadlineMCPServer.js.map +1 -1
- package/dist/threadline/mcp-http-client.d.ts.map +1 -1
- package/dist/threadline/mcp-http-client.js +6 -0
- package/dist/threadline/mcp-http-client.js.map +1 -1
- package/dist/threadline/recordInboundAck.d.ts +48 -0
- package/dist/threadline/recordInboundAck.d.ts.map +1 -0
- package/dist/threadline/recordInboundAck.js +46 -0
- package/dist/threadline/recordInboundAck.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +63 -63
- package/src/scaffold/templates.ts +9 -0
- package/upgrades/1.3.507.md +46 -0
- package/upgrades/1.3.508.md +83 -0
- package/upgrades/side-effects/multi-machine-seamlessness-ws41-attention-pool-read.md +83 -0
- package/upgrades/side-effects/threadline-single-negotiator.md +106 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
- **Single-negotiator lease (G1, closes F1):** a per-conversation lease on the
|
|
9
|
+
existing `ConversationStore` names exactly ONE session as the owner of a
|
|
10
|
+
conversation's outbound voice. A warm/keep-alive/side session that is NOT the
|
|
11
|
+
owner has its content send WITHHELD; the most it can emit is a fixed structural
|
|
12
|
+
`holding-notice` to the peer. The lease is the only blocking authority and it
|
|
13
|
+
keys on a STRUCTURAL ownership check `(ownerSessionName, ownerMachineId, epoch)`
|
|
14
|
+
over the CAS record — never on message content. Renew-on-send, 90s TTL, dead-owner
|
|
15
|
+
reclaim, epoch-fenced stale holders, no background timers.
|
|
16
|
+
- **Ships dark + dry-run-first:** `threadline.singleNegotiator.enabled` (default
|
|
17
|
+
false ⇒ pure pass-through, zero behavior change) and `dryRun` (default true when
|
|
18
|
+
enabled ⇒ logs the verdict it WOULD reach to `logs/threadline-negotiator-*.jsonl`
|
|
19
|
+
but still sends). Enforcement (withholding a real send) requires an explicit
|
|
20
|
+
`dryRun:false`. Fail-open on a lease-store error (raises a HIGH attention item —
|
|
21
|
+
never silent), because prose is inert.
|
|
22
|
+
- **Prose is inert (G2) via a POSITIVE boundary:** new `AnchoredAuthorization` type
|
|
23
|
+
+ `requireAnchoredAuthorization` guard — an irreversible-action gate accepts
|
|
24
|
+
authorization ONLY as a typed anchored artifact (mandate / review-exchange /
|
|
25
|
+
operator-confirm reference), never a string/transcript/summary/classifier output.
|
|
26
|
+
Wired into `ExternalOperationGate` (fails CLOSED on prose for an irreversible op).
|
|
27
|
+
`ContentClassifier` gains a SIGNAL-ONLY `detectCommitmentClass` nudge (advisory,
|
|
28
|
+
never blocks, fails open to no-nudge).
|
|
29
|
+
- **Honest acks (G3, closes F4):** the verified relay inbound path
|
|
30
|
+
(`POST /threadline/messages/receive`) now records the implicit delivery ack, via a
|
|
31
|
+
shared `recordInboundAck` funnel that all three inbound-receive sites route
|
|
32
|
+
through — so `/threadline/peers/health`'s `stale:true` reflects reality instead of
|
|
33
|
+
permanent false noise.
|
|
34
|
+
- **New read route** `GET /threadline/negotiator` (bearer-gated, paginated): per-
|
|
35
|
+
conversation holder + epoch + expiry, plus dry-run would-hold / hold / fail-open
|
|
36
|
+
counts.
|
|
37
|
+
- CLAUDE.md template + `PostUpdateMigrator` migration + `ConfigDefaults` default +
|
|
38
|
+
dark-gate classification, so new AND deployed agents learn the capability and
|
|
39
|
+
backfill the config (Agent Awareness + Migration Parity).
|
|
40
|
+
|
|
41
|
+
## What to Tell Your User
|
|
42
|
+
|
|
43
|
+
Threadline is now much harder to confuse when I'm running more than one session at
|
|
44
|
+
once. Before, a background "warm" session could speak in my name on a conversation —
|
|
45
|
+
that's how, on June 11, a side session "confirmed" an irreversible cutover window
|
|
46
|
+
that no coherent single me had actually agreed to. Now exactly one of my sessions
|
|
47
|
+
owns each conversation's voice at a time; any other session is held back and can only
|
|
48
|
+
post a short "the owner will respond" notice — it can never speak content or commit me
|
|
49
|
+
to anything. And a plain chat message can never authorize an irreversible step on its
|
|
50
|
+
own: real commitments only travel through the PIN-anchored approval path, by design.
|
|
51
|
+
This ships OFF by default and in a measure-only mode first, so nothing about how I
|
|
52
|
+
talk to other agents changes until it's proven safe to turn on.
|
|
53
|
+
|
|
54
|
+
## Summary of New Capabilities
|
|
55
|
+
|
|
56
|
+
- A per-conversation negotiator lease guarantees single-voice (one owning session)
|
|
57
|
+
per conversation; warm/side sessions are structurally withheld.
|
|
58
|
+
- Prose carries no authority by construction; irreversible-action gates accept only a
|
|
59
|
+
typed anchored authorization artifact (`AnchoredAuthorization`).
|
|
60
|
+
- Honest agent-to-agent delivery acks on the verified relay inbound path (closes the
|
|
61
|
+
false-`stale` peer-health noise).
|
|
62
|
+
- `GET /threadline/negotiator` surfaces lease state + dry-run telemetry.
|
|
63
|
+
- Off by default; dry-run-first; `threadline.singleNegotiator.*` config.
|
|
64
|
+
|
|
65
|
+
## Evidence
|
|
66
|
+
|
|
67
|
+
- Unit (31): `tests/unit/NegotiatorLease.test.ts` (20 — acquire/renew/expire/epoch-
|
|
68
|
+
fence, CAS race, dead-owner reclaim, backward-compat, holding-notice per-epoch +
|
|
69
|
+
min-interval limits, config resolver, holder-singularity, observability log),
|
|
70
|
+
`tests/unit/AnchoredAuthorization.test.ts` (7), `tests/unit/recordInboundAck.test.ts`
|
|
71
|
+
(4 — clears pending by threadId; the F4 mechanism).
|
|
72
|
+
- Integration (9): `negotiator-send-gate.test.ts` (enforce→held content withheld +
|
|
73
|
+
only the holding notice reaches the peer + NO awaiting-ack record; owner delivered;
|
|
74
|
+
dry-run still sends; advisory nudge; `/threadline/negotiator` 200/bearer/paginated),
|
|
75
|
+
`inbound-ack-wiring.test.ts` (every inbound site funnels through recordInboundAck).
|
|
76
|
+
- E2E (10): `threadline-negotiator-alive.test.ts` (route alive 200/bearer + real
|
|
77
|
+
lease state), `threadline-g2-boundary.test.ts` (import-boundary; gate rejects prose
|
|
78
|
+
/ accepts anchored; holder-singularity invariant).
|
|
79
|
+
- Existing suites green: threadline integration (172), ConversationStore +
|
|
80
|
+
A2ADeliveryTracker + ContentClassifier (137), PostUpdateMigrator + ConfigDefaults
|
|
81
|
+
(452), manifest integrity (30). `pnpm lint` clean; build green.
|
|
82
|
+
- Independent second-pass review (outbound messaging block/allow surface) — CONCUR,
|
|
83
|
+
verdict appended to `upgrades/side-effects/threadline-single-negotiator.md`.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Side-Effects Review — WS4.1 attention pool-scope (read-side)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `multi-machine-seamlessness-ws41-attention-pool-read`
|
|
4
|
+
**Date:** `2026-06-12`
|
|
5
|
+
**Author:** `Instar Agent (echo)`
|
|
6
|
+
**Second-pass reviewer:** `not required (read-only observability merge — no block/allow, no mutation, no session/ownership surface)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
The read-side of WS4.1 (MULTI-MACHINE-SEAMLESSNESS-SPEC §WS4.1): `GET /attention?scope=pool`
|
|
11
|
+
merges every online peer's attention items into one view so the dashboard shows one
|
|
12
|
+
queue across the whole pool. Local items are tagged with this machine's id/nickname
|
|
13
|
+
at read time; remote items with the peer's. Tolerant (per-peer 5s timeout → a
|
|
14
|
+
`pool.failed` marker, never a 500), short-TTL cached (3s, so dashboard polling
|
|
15
|
+
doesn't re-fan-out per request), and P17-coalesced at the merge point (same-key
|
|
16
|
+
NORMAL items across machines collapse to ONE row; HIGH/URGENT never coalesced).
|
|
17
|
+
Plain `GET /attention` stays back-compatible (`{items,count}`), now self-tagged with
|
|
18
|
+
machine identity when the pool is wired. Files: `src/server/routes.ts` (the merge
|
|
19
|
+
helper + the scope branch), `src/messaging/TelegramAdapter.ts` (optional
|
|
20
|
+
`machineId`/`machineNickname` on `AttentionItem`), `src/scaffold/templates.ts` +
|
|
21
|
+
`src/core/PostUpdateMigrator.ts` (Agent Awareness + Migration Parity).
|
|
22
|
+
|
|
23
|
+
**Scope boundary:** this is the READ half only. The durable, operator-bound,
|
|
24
|
+
machine-independent `/ack` (a replicated ack record + mutating mesh verb with its own
|
|
25
|
+
RBAC class + owner revalidation) is the security-sensitive write half and ships as a
|
|
26
|
+
SEPARATE slice under the same approved spec. <!-- tracked: CMT-1416 -->
|
|
27
|
+
|
|
28
|
+
## Decision-point inventory
|
|
29
|
+
|
|
30
|
+
- `GET /attention?scope=pool` merge — **add** — a read-only aggregation. No
|
|
31
|
+
block/allow; never mutates an item; returns a tolerant merged view.
|
|
32
|
+
- The P17 coalesce key — **add** — a read-side display grouping (collapses duplicate
|
|
33
|
+
rows); the write-side per-machine budgets remain the authoritative bound.
|
|
34
|
+
- `AttentionItem.machineId/machineNickname` — **add (optional, read-stamped)** — no
|
|
35
|
+
persistence change; stamped at read time, absent on single-machine installs.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 1. Over-block
|
|
40
|
+
No block/allow surface — a read-only merge. Not applicable.
|
|
41
|
+
|
|
42
|
+
## 2. Under-block
|
|
43
|
+
N/A (read-only). The coalesce could in principle hide a distinct-but-same-key item,
|
|
44
|
+
but HIGH/URGENT are exempt and the per-item store is unchanged (every item is still
|
|
45
|
+
individually addressable via `GET /attention/:id`); the merge only affects the
|
|
46
|
+
pool LIST view.
|
|
47
|
+
|
|
48
|
+
## 3. Level-of-abstraction fit
|
|
49
|
+
Right layer: mirrors the proven `GET /sessions?scope=pool` precedent exactly (route-
|
|
50
|
+
level fan-out + per-peer-timeout + `failed` markers + machine tagging). The store
|
|
51
|
+
(`TelegramAdapter`) stays machine-agnostic; the machine view is composed in the route.
|
|
52
|
+
|
|
53
|
+
## 4. Signal vs authority compliance
|
|
54
|
+
Compliant — pure read, zero authority. It gates nothing.
|
|
55
|
+
|
|
56
|
+
## 5. Interactions
|
|
57
|
+
- The plain `GET /attention` path is unchanged except for optional machine tags
|
|
58
|
+
(additive). The PATCH/DELETE/`:id` routes are untouched.
|
|
59
|
+
- The short-TTL cache is keyed by status; a status-filtered pool read and an
|
|
60
|
+
unfiltered one have separate cache entries (no cross-contamination).
|
|
61
|
+
- Coalesce reads `sourceContext`/`category`/`title` — fields the flood guard already
|
|
62
|
+
populates; no new write contract.
|
|
63
|
+
|
|
64
|
+
## 6. External surfaces
|
|
65
|
+
- New query mode on an existing route. Old callers (no `?scope=pool`) get the
|
|
66
|
+
back-compatible array-shaped... (object `{items,count}`) response unchanged.
|
|
67
|
+
- A peer's `GET /attention` is called with the agent bearer (same pattern as
|
|
68
|
+
sessions pool scope). A peer on an OLD version simply returns its local
|
|
69
|
+
`{items,count}` — merges fine (machine tags filled from the registry).
|
|
70
|
+
- Timing: bounded by the 5s per-peer timeout; a slow/dark peer contributes a
|
|
71
|
+
`failed` marker, never delays past the timeout.
|
|
72
|
+
|
|
73
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
74
|
+
**proxied-on-read** — the merged read fans out to peers per request (cached 3s),
|
|
75
|
+
each item tagged with its owning machine. No replication, no durable cross-machine
|
|
76
|
+
state in this slice (that is the deferred `/ack` write half). The pool registry's
|
|
77
|
+
online flag is consulted to skip a known-dark peer cheaply. Phase-C clean: the
|
|
78
|
+
fan-out is per-online-peer and the coalesce is O(items) — no 2-peer assumption.
|
|
79
|
+
|
|
80
|
+
## 8. Rollback cost
|
|
81
|
+
Trivial: the scope branch is additive; reverting the PR restores today's local-only
|
|
82
|
+
`GET /attention`. No durable state written, no migration beyond the additive CLAUDE.md
|
|
83
|
+
bullet (idempotent, content-sniffed).
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Side-Effects Review — Threadline Single-Negotiator Lock + Honest Acks (Robustness Phase 1)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `threadline-single-negotiator`
|
|
4
|
+
**Date:** `2026-06-12`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `independent reviewer subagent — CONCUR`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Phase 1 of the Threadline Robustness program (CMT-1362), making the 2026-06-11 warm-session cutover-lock incident structurally impossible on our side. Three guarantees: **G1** a per-conversation *negotiator lease* so at most one session owns a conversation's outbound voice (a warm/side session is withheld and can only emit a fixed holding notice); **G2** prose is inert — a normal Threadline message never creates a binding/authorization record, enforced *positively* by a typed `AnchoredAuthorization` boundary that irreversible-action gates must accept instead of prose; **G3** honest ack wiring — the verified relay inbound path now records the implicit delivery ack (closes the F4 false-`stale` noise) via a shared `recordInboundAck` funnel. Files touched: `src/threadline/ConversationStore.ts` (lease fields + `acquireOrRenewLease`), new `src/threadline/NegotiatorLease.ts` + `NegotiatorGate.ts` + `recordInboundAck.ts`, new `src/coordination/AnchoredAuthorization.ts`, `src/threadline/ContentClassifier.ts` (advisory commitment signal), `src/threadline/ThreadlineMCPServer.ts` + `mcp-http-client.ts` (surface held/note/advisory to the session), `src/threadline/ThreadlineEndpoints.ts` (receive-route ack funnel), `src/server/routes.ts` (send gate + `GET /threadline/negotiator` + funnel refactor), `src/commands/server.ts` (funnel refactor), `src/core/ExternalOperationGate.ts` (anchored-auth boundary), `src/config/ConfigDefaults.ts` + `src/core/devGatedFeatures.ts` + `src/core/PostUpdateMigrator.ts` + `src/scaffold/templates.ts` (config default + dark-gate classification + migration + awareness).
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- `/threadline/relay-send` lease/voice gate (`routes.ts`, via `NegotiatorGate.evaluateSendGate`) — **add** — withholds a non-owner's content send when enforcing; the ONE new blocking action.
|
|
15
|
+
- `ContentClassifier.detectCommitmentClass` — **add** — signal-only advisory nudge; NO authority, never blocks.
|
|
16
|
+
- `ExternalOperationGate.evaluate` authorization input — **modify** — adds an optional `authorization` that, if supplied for an irreversible op, MUST be a typed anchored artifact (fails closed).
|
|
17
|
+
- `recordInboundAck` funnel at all inbound-receive sites — **modify** (refactor of existing inline logic) — recording-only, never gates.
|
|
18
|
+
- `GET /threadline/negotiator` — **add** — read-only, bearer-gated.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. Over-block
|
|
23
|
+
|
|
24
|
+
**What legitimate inputs does this reject that it shouldn't?**
|
|
25
|
+
|
|
26
|
+
The only blocking action is the lease send-gate withholding a non-owner's content send, and it is gated three ways before it can over-block anything real: (a) **default dark** (`enabled:false` ⇒ pure pass-through, no store write); (b) **dry-run-first** when enabled (`dryRun:true` logs the would-be verdict but still sends); (c) when enforcing, it blocks ONLY on the structural ownership check, never on content. Concrete over-block shapes once enforcing is on:
|
|
27
|
+
|
|
28
|
+
- A legitimate second session of the SAME agent that genuinely should speak (e.g. an operator deliberately running two sessions on one conversation) would be withheld. Mitigation: the lease is renew-on-send with a 90s TTL and dead-owner reclaim — if the "real" owner session dies or goes idle past TTL, the next session acquires cleanly. The withheld session also gets an honest `held:true` + note telling it the owner will respond, so it is never a silent drop.
|
|
29
|
+
- A send with **no resolvable owning session identity** (no `INSTAR_SESSION_NAME`) cannot be lease-checked — this is treated as **fail-open + alert**, NOT a withhold, precisely so a missing-identity edge never over-blocks a real send.
|
|
30
|
+
|
|
31
|
+
The dry-run-first rollout exists specifically to measure this over-block (false-positive) rate before enforcement is ever enabled (FD-7).
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 2. Under-block
|
|
36
|
+
|
|
37
|
+
**What failure modes does this still miss?**
|
|
38
|
+
|
|
39
|
+
- **Cross-machine concurrent processing of one conversation (F2).** The lease is per-machine (`conversations.json` is per-machine); single-voice across machines rests on the existing single-holder model (one machine serves a conversation). A genuine split-brain where two machines both process one conversation is explicitly **Phase 3** and is surfaced (not silently missed) by the `detectDuplicateLiveHolders` runtime detector + the holder-singularity test (FD-2).
|
|
40
|
+
- **The peer's own prose.** We cannot force an un-upgraded peer (Dawn on her own train) to treat *her* prose as inert. Phase 1's guarantee is one-sided by design: our side never manufactures authority from prose, and irreversible actions remain gated by their own operator-anchored door. Bilateral content-agreement is Phase 2.
|
|
41
|
+
- **The commitment-class signal is intentionally incomplete** — it is advisory only, so a missed nudge is just a missing hint; the prose is inert either way (G2 does not depend on it).
|
|
42
|
+
- **Fail-open window.** During a lease-store error the gate fails open and G1 is explicitly not enforced — bounded, counted, and loudly alerted (HIGH attention item). Safe because prose is inert: the worst case is two of our own sessions briefly both speaking inert prose, never a binding.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 3. Level-of-abstraction fit
|
|
47
|
+
|
|
48
|
+
The lease lives on the **existing** `ConversationStore` — already the one CAS-protected, single-writer, atomic-write per-conversation record. No new store, no new lock, no new process. The send gate sits at the **single server-side chokepoint** (`/threadline/relay-send`), where the ConversationStore and the server-authoritative session identity both already live (the MCP stdio subprocess forwards `originSessionName` from `INSTAR_SESSION_NAME`; identity is never taken from a peer-supplied string). The ack funnel reuses the **existing** `A2ADeliveryTracker` and consolidates three previously-copy-pasted callsites into one helper. The authorization boundary reuses the **existing** operator-anchored primitives (Mandate / ReviewExchange / OperatorConfirm) as the only authority — it does not invent a new commitment wire protocol. This is the right layer: extend the durable record already on the path, not a parallel mechanism.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 4. Signal vs authority compliance
|
|
53
|
+
|
|
54
|
+
**Required reference:** docs/signal-vs-authority.md
|
|
55
|
+
|
|
56
|
+
- [x] **Yes — but the only blocking authority is a structural ownership check, not brittle content judgment.** The lease blocks on *who speaks* (a deterministic (sessionName, machineId, epoch) comparison over a CAS record), which the principle explicitly permits as a structural validator / safety guard, NOT a judgment about what a message *means*.
|
|
57
|
+
|
|
58
|
+
The brittle/content piece — `detectCommitmentClass` — is deliberately **signal-only**: it produces an advisory nudge surfaced on the send response, never blocks, fails open to no-nudge, and runs off the send path. This is the exact inversion convergence review demanded: an earlier draft proposed a content classifier with *blocking* authority to refuse "binding prose"; that was rejected as both a Signal-vs-Authority violation and unworkable (the incident's own evidence had no lexical keyword). Authority is now positive and structural; prose has no pathway to authority by construction. The G2 boundary (`requireAnchoredAuthorization`) fails *closed* on the authority path — correct for an irreversible-action authorizer — while the lease fails *open* for inert prose.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 5. Interactions
|
|
63
|
+
|
|
64
|
+
- **Shadowing:** the lease gate runs at the top of `/threadline/relay-send`, before the local/relay delivery branches and before the existing grounding gate (which lives in the MCP stdio process, upstream). When the gate withholds, it returns early — the delivery branches never run, which is the intent. When it allows (own / dry-run / fail-open / disabled) the handler proceeds unchanged. It does not shadow the grounding gate (different process, different concern).
|
|
65
|
+
- **Double-fire:** the holding-notice is emitted via a single best-effort `relayClient.sendAuto` and creates NO `A2ADeliveryTracker` awaiting-ack record (guarded — verified by the integration test asserting `pending().length === 0` after a held send), so it cannot inflate pending/stale or double-count. The ack funnel is idempotent (`recordAckByThread` flips awaiting→acked; repeats are no-ops).
|
|
66
|
+
- **Races:** lease acquire/renew is one `ConversationStore.mutate()` CAS transaction — the unit test proves two concurrent acquires yield exactly one winner; the loser sees the live foreign lease. No background renewal timer exists (renew-on-send), eliminating the "renewal silently succeeded while wedged" race.
|
|
67
|
+
- **Feedback loops:** none. The dry-run JSONL is write-only observability; the `/threadline/negotiator` route is read-only.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 6. External surfaces
|
|
72
|
+
|
|
73
|
+
- **Other agents (the peer):** ONE additive wire kind — `holding-notice`. An un-upgraded peer renders it as a harmless one-line text; safety depends on nothing the peer does (it is never ack/content/count-bearing on our side). No flag-day: Dawn's side needs no change. Ordinary conversation is byte-for-byte unchanged when the feature is dark (default).
|
|
74
|
+
- **The sending session:** new optional response fields (`held`, `note`, `advisory`, explicit `delivered:false`) flow back through `sendMessageViaHttp` → the MCP `threadline_send` result. Additive — existing callers that ignore them are unaffected.
|
|
75
|
+
- **Persistent state:** three additive OPTIONAL fields on the `Conversation` record (`negotiatorLease`, `lastHoldingNoticeEpoch`, `lastHoldingNoticeAt`). An existing `conversations.json` without them loads unchanged; the acquire path initializes them. A daily-rotated dry-run JSONL under `logs/` (retention-pruned).
|
|
76
|
+
- **Operator:** a HIGH Attention item only on a genuine fail-open (lease store unreadable) — a state that must never be silent.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 7. Rollback cost
|
|
81
|
+
|
|
82
|
+
Pure code change with a config kill-switch — the safest possible posture. `threadline.singleNegotiator.enabled:false` (the default) makes the gate pure pass-through with zero behavior change; flipping it off instantly disarms enforcement on the next session/server (the config is read live at the chokepoint, not cached at boot). No data migration on revert: the lease fields are inert optional data; stale lease state simply isn't read when the feature is off (no cleanup job needed). G2 + G3 ship live in core but are pure additions (a positive type boundary that only activates when a caller passes an `authorization`, and a recording-only ack call) — reverting is a code revert + patch, no persistent-state cleanup, no user-visible regression during the rollback window.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Conclusion
|
|
87
|
+
|
|
88
|
+
The review confirms the design holds the line the convergence review drew: the only blocking authority is the structural lease (who-speaks), the content classifier is demoted to a signal-only nudge, and G2 is enforced positively via a typed authorization boundary rather than a rot-prone negative audit. The change ships dark + dry-run-first for the one new blocking action, with G2/G3 (pure correctness additions) live in core. Over-block risk is bounded by the dark default, the dry-run measurement stage, the structural (not content) gate key, and the fail-open-on-uncertainty posture. The 3-tier test suite proves the incident is structurally reproduced-and-prevented, the F4 ack gap is closed and wiring-enforced, and the G2 boundary rejects prose while accepting anchored artifacts. Clear to ship pending second-pass concurrence (required — outbound messaging block/allow surface).
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Second-pass review (if required)
|
|
93
|
+
|
|
94
|
+
**Reviewer:** independent reviewer subagent (general-purpose)
|
|
95
|
+
**Independent read of the artifact: CONCUR**
|
|
96
|
+
|
|
97
|
+
The reviewer independently traced all seven surfaces and affirmed: (1) the sole blocking authority blocks on the structural `(ownerSessionName, ownerMachineId, epoch)` CAS comparison, never on content; (2) `detectCommitmentClass` is genuinely signal-only (surfaced as an `advisory` field, never wired to a block path, fails open to no-nudge); (3) `decision:'hold'` is structurally unreachable unless `enabled===true && dryRun===false` AND a foreign live lease exists; (4) missing store / blank session identity / any store-CAS throw all return `allow` + raise the HIGH alert — the gate never fails closed on the content path and never throws into the send path; (5) the held path returns before either `recordSent` callsite, so the holding-notice creates no awaiting-ack record (no pending/stale pollution); (6) `requireAnchoredAuthorization` correctly fails CLOSED on prose for an irreversible op — the right contrast with the lease failing open. No over-block, under-block, or single-voice bypass defect found; the cross-machine F2 gap is an explicitly-scoped Phase-3 boundary surfaced by `detectDuplicateLiveHolders`, not silently missed.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Evidence pointers
|
|
102
|
+
|
|
103
|
+
- Unit: `tests/unit/NegotiatorLease.test.ts` (20), `tests/unit/AnchoredAuthorization.test.ts` (7), `tests/unit/recordInboundAck.test.ts` (4).
|
|
104
|
+
- Integration: `tests/integration/threadline/negotiator-send-gate.test.ts` (5 — held/owner/dry-run/advisory/route), `tests/integration/threadline/inbound-ack-wiring.test.ts` (4 — funnel enumeration).
|
|
105
|
+
- E2E: `tests/e2e/threadline-negotiator-alive.test.ts` (3 — feature alive/bearer/lease state), `tests/e2e/threadline-g2-boundary.test.ts` (7 — import-boundary + gate reject/accept + holder-singularity).
|
|
106
|
+
- Spec: `docs/specs/THREADLINE-SINGLE-NEGOTIATOR-SPEC.md`; ELI16: `docs/specs/threadline-single-negotiator.eli16.md`; convergence report: `docs/specs/reports/threadline-single-negotiator-convergence.md`.
|