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.
Files changed (67) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +6 -12
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  5. package/dist/config/ConfigDefaults.js +16 -0
  6. package/dist/config/ConfigDefaults.js.map +1 -1
  7. package/dist/coordination/AnchoredAuthorization.d.ts +66 -0
  8. package/dist/coordination/AnchoredAuthorization.d.ts.map +1 -0
  9. package/dist/coordination/AnchoredAuthorization.js +71 -0
  10. package/dist/coordination/AnchoredAuthorization.js.map +1 -0
  11. package/dist/core/ExternalOperationGate.d.ts +12 -0
  12. package/dist/core/ExternalOperationGate.d.ts.map +1 -1
  13. package/dist/core/ExternalOperationGate.js +6 -0
  14. package/dist/core/ExternalOperationGate.js.map +1 -1
  15. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  16. package/dist/core/PostUpdateMigrator.js +27 -0
  17. package/dist/core/PostUpdateMigrator.js.map +1 -1
  18. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  19. package/dist/core/devGatedFeatures.js +5 -0
  20. package/dist/core/devGatedFeatures.js.map +1 -1
  21. package/dist/messaging/TelegramAdapter.d.ts +7 -0
  22. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  23. package/dist/messaging/TelegramAdapter.js.map +1 -1
  24. package/dist/scaffold/templates.d.ts.map +1 -1
  25. package/dist/scaffold/templates.js +9 -0
  26. package/dist/scaffold/templates.js.map +1 -1
  27. package/dist/server/routes.d.ts.map +1 -1
  28. package/dist/server/routes.js +278 -19
  29. package/dist/server/routes.js.map +1 -1
  30. package/dist/threadline/ContentClassifier.d.ts +15 -0
  31. package/dist/threadline/ContentClassifier.d.ts.map +1 -1
  32. package/dist/threadline/ContentClassifier.js +39 -0
  33. package/dist/threadline/ContentClassifier.js.map +1 -1
  34. package/dist/threadline/ConversationStore.d.ts +32 -0
  35. package/dist/threadline/ConversationStore.d.ts.map +1 -1
  36. package/dist/threadline/ConversationStore.js +74 -0
  37. package/dist/threadline/ConversationStore.js.map +1 -1
  38. package/dist/threadline/NegotiatorGate.d.ts +88 -0
  39. package/dist/threadline/NegotiatorGate.d.ts.map +1 -0
  40. package/dist/threadline/NegotiatorGate.js +148 -0
  41. package/dist/threadline/NegotiatorGate.js.map +1 -0
  42. package/dist/threadline/NegotiatorLease.d.ts +156 -0
  43. package/dist/threadline/NegotiatorLease.d.ts.map +1 -0
  44. package/dist/threadline/NegotiatorLease.js +202 -0
  45. package/dist/threadline/NegotiatorLease.js.map +1 -0
  46. package/dist/threadline/ThreadlineEndpoints.d.ts +2 -1
  47. package/dist/threadline/ThreadlineEndpoints.d.ts.map +1 -1
  48. package/dist/threadline/ThreadlineEndpoints.js +21 -1
  49. package/dist/threadline/ThreadlineEndpoints.js.map +1 -1
  50. package/dist/threadline/ThreadlineMCPServer.d.ts +8 -0
  51. package/dist/threadline/ThreadlineMCPServer.d.ts.map +1 -1
  52. package/dist/threadline/ThreadlineMCPServer.js +14 -1
  53. package/dist/threadline/ThreadlineMCPServer.js.map +1 -1
  54. package/dist/threadline/mcp-http-client.d.ts.map +1 -1
  55. package/dist/threadline/mcp-http-client.js +6 -0
  56. package/dist/threadline/mcp-http-client.js.map +1 -1
  57. package/dist/threadline/recordInboundAck.d.ts +48 -0
  58. package/dist/threadline/recordInboundAck.d.ts.map +1 -0
  59. package/dist/threadline/recordInboundAck.js +46 -0
  60. package/dist/threadline/recordInboundAck.js.map +1 -0
  61. package/package.json +1 -1
  62. package/src/data/builtin-manifest.json +63 -63
  63. package/src/scaffold/templates.ts +9 -0
  64. package/upgrades/1.3.507.md +46 -0
  65. package/upgrades/1.3.508.md +83 -0
  66. package/upgrades/side-effects/multi-machine-seamlessness-ws41-attention-pool-read.md +83 -0
  67. 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`.