instar 1.3.43 → 1.3.45

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 (103) hide show
  1. package/dashboard/index.html +68 -14
  2. package/dashboard/process-health.js +204 -53
  3. package/dist/commands/server.d.ts.map +1 -1
  4. package/dist/commands/server.js +292 -3
  5. package/dist/commands/server.js.map +1 -1
  6. package/dist/core/HandoffReceiver.d.ts +56 -0
  7. package/dist/core/HandoffReceiver.d.ts.map +1 -0
  8. package/dist/core/HandoffReceiver.js +103 -0
  9. package/dist/core/HandoffReceiver.js.map +1 -0
  10. package/dist/core/HandoffWireTransport.d.ts +61 -0
  11. package/dist/core/HandoffWireTransport.d.ts.map +1 -0
  12. package/dist/core/HandoffWireTransport.js +114 -0
  13. package/dist/core/HandoffWireTransport.js.map +1 -0
  14. package/dist/core/HttpLeaseTransport.d.ts +68 -0
  15. package/dist/core/HttpLeaseTransport.d.ts.map +1 -0
  16. package/dist/core/HttpLeaseTransport.js +97 -0
  17. package/dist/core/HttpLeaseTransport.js.map +1 -0
  18. package/dist/core/HttpLiveTailTransport.d.ts +88 -0
  19. package/dist/core/HttpLiveTailTransport.d.ts.map +1 -0
  20. package/dist/core/HttpLiveTailTransport.js +95 -0
  21. package/dist/core/HttpLiveTailTransport.js.map +1 -0
  22. package/dist/core/LeaseCoordinator.d.ts +14 -0
  23. package/dist/core/LeaseCoordinator.d.ts.map +1 -1
  24. package/dist/core/LeaseCoordinator.js +38 -0
  25. package/dist/core/LeaseCoordinator.js.map +1 -1
  26. package/dist/core/LiveTailSource.d.ts +65 -0
  27. package/dist/core/LiveTailSource.d.ts.map +1 -0
  28. package/dist/core/LiveTailSource.js +87 -0
  29. package/dist/core/LiveTailSource.js.map +1 -0
  30. package/dist/core/MultiMachineCoordinator.d.ts +14 -0
  31. package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
  32. package/dist/core/MultiMachineCoordinator.js +22 -0
  33. package/dist/core/MultiMachineCoordinator.js.map +1 -1
  34. package/dist/core/ReplyMarkerTransport.d.ts +58 -0
  35. package/dist/core/ReplyMarkerTransport.d.ts.map +1 -0
  36. package/dist/core/ReplyMarkerTransport.js +66 -0
  37. package/dist/core/ReplyMarkerTransport.js.map +1 -0
  38. package/dist/core/handoffReceiverWiring.d.ts +57 -0
  39. package/dist/core/handoffReceiverWiring.d.ts.map +1 -0
  40. package/dist/core/handoffReceiverWiring.js +65 -0
  41. package/dist/core/handoffReceiverWiring.js.map +1 -0
  42. package/dist/core/handoffSentinelBootWiring.d.ts +53 -0
  43. package/dist/core/handoffSentinelBootWiring.d.ts.map +1 -0
  44. package/dist/core/handoffSentinelBootWiring.js +52 -0
  45. package/dist/core/handoffSentinelBootWiring.js.map +1 -0
  46. package/dist/core/handoffSentinelWiring.d.ts +60 -0
  47. package/dist/core/handoffSentinelWiring.d.ts.map +1 -0
  48. package/dist/core/handoffSentinelWiring.js +59 -0
  49. package/dist/core/handoffSentinelWiring.js.map +1 -0
  50. package/dist/core/seamlessnessConfig.d.ts +8 -0
  51. package/dist/core/seamlessnessConfig.d.ts.map +1 -1
  52. package/dist/core/seamlessnessConfig.js +1 -0
  53. package/dist/core/seamlessnessConfig.js.map +1 -1
  54. package/dist/core/types.d.ts +6 -0
  55. package/dist/core/types.d.ts.map +1 -1
  56. package/dist/core/types.js.map +1 -1
  57. package/dist/messaging/TelegramAdapter.d.ts +6 -0
  58. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  59. package/dist/messaging/TelegramAdapter.js +8 -0
  60. package/dist/messaging/TelegramAdapter.js.map +1 -1
  61. package/dist/messaging/ingressDedup.d.ts +55 -0
  62. package/dist/messaging/ingressDedup.d.ts.map +1 -0
  63. package/dist/messaging/ingressDedup.js +69 -0
  64. package/dist/messaging/ingressDedup.js.map +1 -0
  65. package/dist/messaging/stuckMessageRecovery.d.ts +46 -0
  66. package/dist/messaging/stuckMessageRecovery.d.ts.map +1 -0
  67. package/dist/messaging/stuckMessageRecovery.js +52 -0
  68. package/dist/messaging/stuckMessageRecovery.js.map +1 -0
  69. package/dist/server/AgentServer.d.ts +64 -0
  70. package/dist/server/AgentServer.d.ts.map +1 -1
  71. package/dist/server/AgentServer.js +24 -0
  72. package/dist/server/AgentServer.js.map +1 -1
  73. package/dist/server/handoffInitiateRoutes.d.ts +40 -0
  74. package/dist/server/handoffInitiateRoutes.d.ts.map +1 -0
  75. package/dist/server/handoffInitiateRoutes.js +57 -0
  76. package/dist/server/handoffInitiateRoutes.js.map +1 -0
  77. package/dist/server/machineRoutes.d.ts +47 -0
  78. package/dist/server/machineRoutes.d.ts.map +1 -1
  79. package/dist/server/machineRoutes.js +138 -0
  80. package/dist/server/machineRoutes.js.map +1 -1
  81. package/dist/server/routes.d.ts +20 -0
  82. package/dist/server/routes.d.ts.map +1 -1
  83. package/dist/server/routes.js +65 -0
  84. package/dist/server/routes.js.map +1 -1
  85. package/package.json +1 -1
  86. package/src/data/builtin-manifest.json +49 -49
  87. package/upgrades/1.3.44.md +72 -0
  88. package/upgrades/1.3.45.md +28 -0
  89. package/upgrades/side-effects/cross-machine-seamlessness-consent-acquire.md +52 -0
  90. package/upgrades/side-effects/cross-machine-seamlessness-exactly-once-ingress.md +73 -0
  91. package/upgrades/side-effects/cross-machine-seamlessness-handoff-ack-yield.md +82 -0
  92. package/upgrades/side-effects/cross-machine-seamlessness-handoff-begin.md +48 -0
  93. package/upgrades/side-effects/cross-machine-seamlessness-handoff-boot-wiring.md +68 -0
  94. package/upgrades/side-effects/cross-machine-seamlessness-handoff-receiver-wiring.md +52 -0
  95. package/upgrades/side-effects/cross-machine-seamlessness-handoff-receiver.md +46 -0
  96. package/upgrades/side-effects/cross-machine-seamlessness-handoff-sentinel-wiring.md +47 -0
  97. package/upgrades/side-effects/cross-machine-seamlessness-lease-wire.md +46 -0
  98. package/upgrades/side-effects/cross-machine-seamlessness-livetail-source.md +50 -0
  99. package/upgrades/side-effects/cross-machine-seamlessness-livetail-streaming.md +50 -0
  100. package/upgrades/side-effects/cross-machine-seamlessness-livetail-wire.md +65 -0
  101. package/upgrades/side-effects/cross-machine-seamlessness-noloss-recovery.md +61 -0
  102. package/upgrades/side-effects/cross-machine-seamlessness-reply-marker.md +65 -0
  103. package/upgrades/side-effects/process-health-tab-eli16-pass.md +49 -0
@@ -0,0 +1,73 @@
1
+ # Side-Effects Review — Cross-Machine Seamlessness: exactly-once ingress gate (D, no-duplicate half)
2
+
3
+ **Spec:** docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §8 G3a (converged, approved)
4
+
5
+ Wires the MessageProcessingLedger into the live Telegram message path so a redelivered /
6
+ handoff-window-replayed inbound event is recognized and NOT answered twice. This is the
7
+ no-DUPLICATE-reply half of G3a. It ships DARK behind `multiMachine.exactlyOnceIngress`
8
+ (default false): when off, the message path is byte-for-byte unchanged.
9
+
10
+ ## What changed
11
+ - `src/messaging/ingressDedup.ts` — NEW. `decideIngress(ledger, dedupeKey, opts)` → process | drop
12
+ (records received → claims processing under the lease epoch; drops already-replied or
13
+ in-flight-not-stuck; re-claims a fenced-holder's stuck entry). `commitInboundReply(...)` →
14
+ reply_committed + cursor_advanced with the deterministic idempotency key. `dedupeKeyFor(...)`.
15
+ - `src/server/routes.ts` — INBOUND gate in `/internal/telegram-forward`, placed AFTER the
16
+ sentinel intercept (emergency-stop/pause must never be deduped) and BEFORE routing: on a
17
+ drop, returns `{ok:true,deduped:true}` without routing. OUTBOUND commit in
18
+ `/telegram/reply/:topicId` after a successful `sendToTopic`: commits the topic's current
19
+ inbound dedupeKey (skipped for proxy/system sends). Both FAIL-OPEN. Added `messageLedger` +
20
+ `currentInboundByTopic` to RouteContext.
21
+ - `src/server/AgentServer.ts` — `messageLedger?` + `currentInboundByTopic?` options → routeCtx.
22
+ - `src/commands/server.ts` — constructs the ledger + per-topic map ONLY when
23
+ `seamlessness.exactlyOnceIngress` (fail-open if open() throws → gate stays dark).
24
+ - `src/core/seamlessnessConfig.ts` + `src/core/types.ts` — `exactlyOnceIngress` flag (default false).
25
+ - `src/core/MultiMachineCoordinator.ts` — `getLeaseEpoch()` accessor (the ledger's fencing token).
26
+
27
+ ## Over-block / under-block
28
+ - UNDER-block (the dangerous direction = a real message silently dropped): mitigated three ways.
29
+ (1) The gate ONLY drops when the ledger shows the SAME dedupeKey already reply_committed/
30
+ cursor_advanced, or processing-and-not-stuck — never a first-seen event. Proven both sides in
31
+ ingress-dedup.test.ts + the integration test. (2) FAIL-OPEN: any ledger throw routes normally
32
+ (tested). (3) Ships DARK (flag default false) — zero behavior change until a live test-as-self
33
+ flips it on.
34
+ - OVER-block: a genuine NEW message is always `first-seen` → process. The dedupeKey is
35
+ `telegram:<topic>:<messageId>`, unique per user message, so two distinct messages never collide.
36
+ - dedupeKey uses Telegram `message_id` (already on the forward, stable across redeliveries and
37
+ across machines). The spec names `update_id`; message_id is the functional v1 key (gate falls
38
+ back to it). update_id threading is a tracked refinement. <!-- tracked: topic-13481 -->
39
+
40
+ ## Signal vs authority
41
+ - decideIngress returns a decision; the route is the single authority that drops/routes. The
42
+ ledger is durable record + fencing, not a gate of its own.
43
+
44
+ ## Interactions
45
+ - Placed after the sentinel intercept so "stop everything" is never deduped (wiring-integrity
46
+ test asserts gate index is between classify and onTopicMessage).
47
+ - Outbound commit skipped for `isProxy` sends (PresenceProxy etc. are not replies to a user
48
+ inbound) so it never falsely commits an unrelated topic.
49
+ - `currentInboundByTopic` is best-effort association (last claimed inbound per topic). Concurrent
50
+ inbounds for one topic before the first reply → the earlier one stays processing (eventually
51
+ reclaimable / replayed). Acceptable for v1, behind the flag.
52
+
53
+ ## NOT in this increment (tracked, topic-13481)
54
+ - **No-LOSS-on-crash replay** (re-inject un-committed entries on restart) — the other G3a half.
55
+ Without it, this increment is a strict IMPROVEMENT (adds dedup, no regression: a crash after
56
+ inject-before-reply loses the reply exactly as today). Next increment.
57
+ - Cross-machine `applyRemoteReplyMarker` propagation (tunnel+git) — the lease already ensures one
58
+ forwarder, so cross-machine double-handle is already prevented; this hardens the failover window.
59
+ - CONTINUATION resume verification (D3); update_id dedupeKey (vs message_id).
60
+
61
+ ## Rollback cost
62
+ - Low. Flag default-off ⇒ inert in production. Revert = drop the gate block + commit block + the
63
+ ledger construction; no migration, no persisted schema beyond the opt-in SQLite file. The route
64
+ returns to identical behavior when `messageLedger` is null.
65
+
66
+ ## Tests
67
+ - `tests/unit/ingress-dedup.test.ts` (8) — decision both sides: first-seen, in-flight drop,
68
+ already-replied drop, stuck re-claim, within-window no-reclaim; commit idempotent + deterministic key.
69
+ - `tests/integration/exactly-once-ingress.test.ts` (5) — gate through the REAL route + REAL ledger:
70
+ dark routes both, first routes + claims, in-flight dup dropped, already-replied dup dropped,
71
+ fail-open; + wiring-integrity (gate between sentinel and routing).
72
+ - `tests/e2e/exactly-once-ingress-e2e.test.ts` (1) — gate ALIVE in a real booted AgentServer:
73
+ already-replied redelivery → 200 + deduped over real HTTP, not routed.
@@ -0,0 +1,82 @@
1
+ # Side-Effects Review — Cross-Machine Seamlessness: handoff ack/yield wire
2
+
3
+ **Spec:** docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §8 G3d/G3e (converged, approved)
4
+
5
+ Third piece of the wire-transport increment. The point-to-point ack/yield channel
6
+ the two machines use to negotiate a verified, lease-safe planned handoff.
7
+
8
+ ## What changed
9
+ - `src/core/HandoffWireTransport.ts` — NEW. Symmetric transport, used per-role:
10
+ - OUTGOING: `awaitAck(timeoutMs)` (resolves when the incoming POSTs its ack via
11
+ recordAck; null on timeout, timer unref'd), `sendYield()` (POST the explicit
12
+ yield to the incoming).
13
+ - INCOMING: `sendAck(ack)` (POST the "caught up" echo to the outgoing),
14
+ `onYield(cb)`/`recordYield()` (the yield route triggers the incoming's lease CAS).
15
+ All POSTs ride the authenticated channel (signRequest). Injected fetch/clock.
16
+ - `src/server/machineRoutes.ts` — NEW routes `POST /api/handoff/ack` and
17
+ `POST /api/handoff/yield` (both authMiddleware). `/ack` validates the echo shape
18
+ (tailSeq + ingressPosition + threadHistoryHash) and delivers it via `onHandoffAck`;
19
+ `/yield` delivers via `onHandoffYield`. Both return 503 when the callback is
20
+ absent (honest — consistent with the live-tail receiver), never a silent ok.
21
+ - `src/server/AgentServer.ts` — NEW `handoffWireTransport?` option
22
+ (`{ recordAck, recordYield }`, mirrors `leaseTransport`). When present, the mount
23
+ wires `onHandoffAck → transport.recordAck` and `onHandoffYield → transport.recordYield`;
24
+ absent → the routes stay 503. This is what flips the routes from dangling handlers
25
+ to LIVE on a mesh machine.
26
+ - `src/commands/server.ts` — instantiate ONE `HandoffWireTransport` in the fenced-lease
27
+ block (gated on `coordinator.enabled && isGitRepo && gitBackupEnabled`), with a 1:1
28
+ `peer()` resolver (first reachable non-self machine; null for a solo agent → sends are
29
+ reachable no-ops) and a monotonic `nextSequence`. Passed straight into AgentServer
30
+ options. A single-machine mesh behaves exactly as before (no peer → no sends).
31
+
32
+ ## Over-block / under-block
33
+ - The yield is the SOLE trigger for the incoming's lease CAS (the design closes the
34
+ two-holders-same-epoch window). A dropped/missing yield → the incoming simply never
35
+ acquires → the outgoing stays awake (safe under-action, no double-leader).
36
+ - `awaitAck` supersedes any stale pending wait and always resolves (ack or null) — it
37
+ cannot wedge the HandoffSentinel.
38
+ - recordAck with no pending wait, recordYield with no handler, and no-peer sends are all
39
+ safe no-ops / false returns (tested).
40
+
41
+ ## Signal vs authority
42
+ - The transport carries no authority — it moves the ack and the yield. The DECISION to
43
+ yield (verified ack + passing validation) lives in HandoffSentinel (§8 G3e), which is
44
+ the next integrating step that constructs these ops; the lease CAS itself is the
45
+ authority. This transport only delivers the negotiated signals.
46
+
47
+ ## Interactions
48
+ - Reuses signRequest/machineAuthMiddleware (the same authenticated machine channel as
49
+ /api/lease and /api/live-tail). 1:1 with the single peer (resolved by the caller).
50
+ - Consumes `HandoffAck`/`IngressPosition` from HandoffSentinel/types (already shipped).
51
+ - **Done here:** the transport is now instantiated in server.ts and bolted into the
52
+ live route mount via the AgentServer option — both routes are LIVE on a mesh machine
53
+ (e2e-proven through the real booted server). The transport carries the ack/yield; with
54
+ no handler registered yet, recordAck-with-no-pending and recordYield-with-no-handler are
55
+ safe no-ops (logged, 200 at the route).
56
+ - **Next piece (next increment):** construct the OUTGOING HandoffSentinel (drives flush →
57
+ awaitAck-on-this-transport → verify → sendYield → demoteSelf) and the INCOMING
58
+ HandoffReceiver (buildAck from buffer+ingress+threadHash; `onYield` registers the
59
+ receiver's lease-CAS via `recordYield`), plus the `inProgress` race guard so the
60
+ reaper/scheduler don't act mid-handoff. <!-- tracked: topic-13481 -->
61
+
62
+ ## Rollback cost
63
+ - Low. Two additive routes + one new standalone class + one optional AgentServer option +
64
+ one gated server.ts instantiation. Reverting removes them with no behavior change to a
65
+ solo agent (the transport is a reachable no-op with no peer) and only disables the
66
+ not-yet-orchestrated ack/yield channel on a mesh.
67
+
68
+ ## Tests
69
+ - `tests/unit/HandoffWireTransport.test.ts` (7): awaitAck resolves on recordAck, awaitAck
70
+ times out → null, sendYield/sendAck POST to the right endpoints with signed headers,
71
+ onYield handler fires, no-peer → false, recordAck-with-no-pending safe no-op. tsc clean.
72
+ - `tests/integration/machine-routes.test.ts` (+6, route surface): a valid signed ack is
73
+ delivered to `onHandoffAck` with the authenticated machine id; a malformed echo (missing
74
+ ingressPosition/threadHistoryHash) → 400 and the callback never fires; an unauthenticated
75
+ ack/yield → 401; a valid signed yield is delivered to `onHandoffYield` with the machine id;
76
+ and — the honest-503 contract — a route set built WITHOUT the callbacks returns 503 for
77
+ both ack and yield (never a silent ok). Full machine-routes suite green (20/20), tsc clean.
78
+ - `tests/e2e/multi-machine-http.test.ts` (+3, feature-is-alive): two AgentServers boot for
79
+ real; a signed ack POSTed from machine B over HTTP resolves machine A's pending
80
+ `awaitAck` with the echoed tailSeq/hash; a signed yield fires A's registered yield
81
+ handler; an unsigned ack → 401 and a malformed echo → 400. This is the Tier-3 proof that
82
+ the routes are ALIVE through the production AgentServer mount, not silently 503. (12/12)
@@ -0,0 +1,48 @@
1
+ # Side-Effects Review — Cross-Machine Seamlessness: handoff begin signal
2
+
3
+ **Spec:** docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §8 G3d (converged, approved)
4
+
5
+ The begin signal the spec's planned-handoff protocol needs but left implicit: the
6
+ point-to-point message by which the OUTGOING machine opens a handoff and hands the
7
+ INCOMING machine the flush manifest it must echo. Symmetric with the ack/yield routes
8
+ already shipped (commit 5fc5a1008). This increment lands the ROUTE SURFACE only; the
9
+ incoming HandoffReceiver that consumes it and the outgoing HandoffSentinel that POSTs it
10
+ are the next sub-increments (C-receiver, C2 — tracked in docs/SEAMLESSNESS-WIRING-PLAN.md).
11
+
12
+ ## What changed
13
+ - `src/server/machineRoutes.ts` — NEW route `POST /api/handoff/begin` (authMiddleware).
14
+ Validates the manifest shape (tailSeq:number + ingressPosition + threadHistoryHash:string;
15
+ topic is carried for the incoming's hash recomputation) and delivers it via `onHandoffBegin`.
16
+ Returns 400 on a malformed manifest, 401 unauthenticated, and 503 when the callback is
17
+ absent (honest not-wired — never a silent ok).
18
+ - `src/server/AgentServer.ts` — NEW `onHandoffBegin?` option forwarded into the route mount.
19
+ Absent → the begin route 503s. server.ts will bind it (C-receiver) to store the manifest
20
+ and drive HandoffReceiver.onBeginHandoff.
21
+
22
+ ## Over-block / under-block
23
+ - The begin route carries no authority — it only delivers a manifest to the incoming. The
24
+ incoming still attempts NO lease action on begin (it only builds + sends its ack); the lease
25
+ CAS happens solely on the later explicit yield. So a spurious/forged begin (rejected anyway
26
+ by machineAuth) cannot move the lease.
27
+ - Manifest validation is shape-only here; the semantic echo-verification (does the incoming's
28
+ ack match what the outgoing flushed?) is the HandoffSentinel's job (§8 G3e), unchanged.
29
+
30
+ ## Signal vs authority
31
+ - Pure transport/signal. No role mutation, no lease mutation. The authority (yield → CAS)
32
+ is untouched.
33
+
34
+ ## Interactions
35
+ - Same authenticated machine channel (signRequest/machineAuthMiddleware) as
36
+ /api/lease, /api/live-tail, /api/handoff/{ack,yield}. 1:1 with the single peer.
37
+ - Consumes nothing new; produces a delivered manifest for the (next-increment) receiver.
38
+
39
+ ## Rollback cost
40
+ - Minimal. One additive route + one optional option. Reverting removes them with no behavior
41
+ change (the route 503s today since server.ts does not yet supply the callback).
42
+
43
+ ## Tests
44
+ - `tests/integration/machine-routes.test.ts` (+3): a valid signed begin → onHandoffBegin with
45
+ the manifest + machine id; a malformed manifest → 400 (callback never fires); unauthenticated
46
+ → 401; plus the unwired route → 503 (extended the honest-503 case). 23/23.
47
+ - `tests/e2e/multi-machine-http.test.ts` (+1, feature-is-alive): a signed begin POSTed from
48
+ machine B reaches machine A's onHandoffBegin through the real booted server. 13/13. tsc clean.
@@ -0,0 +1,68 @@
1
+ # Side-Effects Review — Cross-Machine Seamlessness: handoff sentinel boot-wiring + operator trigger (C2b/C3)
2
+
3
+ **Spec:** docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §8 G3e (converged, approved)
4
+
5
+ Bolts the outgoing planned-handoff conductor (HandoffSentinel, built + unit-tested as a
6
+ factory in the prior increment) into the live server boot, and exposes the explicit
7
+ operator/test trigger. The protocol pieces (lease, live-tail, ack/yield/begin wire, receiver)
8
+ were already wired in earlier increments; this closes the outgoing-side gap so a planned
9
+ handoff can actually be initiated and run end-to-end. C2b (boot wiring + trigger route) and C3
10
+ (two-server e2e) land together because the trigger route touches `src/server/`, which the
11
+ e2e-pairing gate requires to ship with a `tests/e2e/*.test.ts`.
12
+
13
+ ## What changed
14
+ - `src/core/handoffSentinelBootWiring.ts` — NEW. `createHandoffSentinelBootWiring(deps)` binds
15
+ the sentinel to the live components (Telegram adapter, coordinator, LiveTailSource, wire
16
+ transport) and owns the active-topic selection (`pickActiveTopic` — most-recently-active
17
+ topic; getTopicHistory is chronological so the last entry is newest). Extracted from server.ts
18
+ so the boot glue is a tested unit, mirroring `handoffReceiverWiring`.
19
+ - `src/server/handoffInitiateRoutes.ts` — NEW. `POST /handoff/initiate` (bearer-authed, mounted
20
+ after the global auth middleware) → runs the sentinel to a terminal outcome; `failed`→500,
21
+ `handed-off`/`aborted-stay-awake`→200, both carry `{outcome,inProgress}`. `GET /handoff/status`
22
+ exposes `inProgress`. Unwired (solo / multi-machine off) → honest 503, never a silent ok.
23
+ - `src/server/AgentServer.ts` — added `onHandoffInitiate?` + `handoffInProgress?` options;
24
+ imports + mounts `createHandoffInitiateRoutes` after the usher routes.
25
+ - `src/commands/server.ts` — inside the live-tail block (where `liveTailSource` is in scope,
26
+ additionally gated on `handoffWireTransport`), constructs the boot wiring and exposes
27
+ `handoffInitiate` + `handoffSentinelInProgress` to the AgentServer constructor.
28
+
29
+ ## Over-block / under-block
30
+ - The lease is NEVER yielded unless the incoming's echo verifies (HandoffSentinel.ackMatches)
31
+ AND validation passes — unchanged from the sentinel's own guarantee. The two-server e2e proves
32
+ it over real HTTP: a caught-up standby hands off (B acquires, A demotes); a stale standby
33
+ (divergent history → hash mismatch) makes A abort and stay awake with ZERO yield and B never
34
+ acquiring. No two-holders window.
35
+ - The trigger is explicit-only. There is deliberately no sleep/wake auto-trigger:
36
+ SleepWakeDetector emits only a `wake` event (verified), so there is no pre-sleep hook for v1.
37
+ An automatic trigger is a tracked follow-on, NOT silently half-built here.
38
+ - Unwired → 503 (honest), proven by the e2e against a server with no `onHandoffInitiate`.
39
+
40
+ ## Signal vs authority
41
+ - The route only pulls the trigger and surfaces the outcome. The DECISION to yield lives in
42
+ HandoffSentinel; the lease CAS authority is the incoming coordinator's, fired solely by the
43
+ explicit yield. The boot wiring supplies ops; it holds no authority of its own.
44
+
45
+ ## Interactions
46
+ - Reuses the exact factories server.ts already boots (`createHandoffSentinelWiring`,
47
+ `hashTopicHistory`) so outgoing-flush and incoming-echo hash identical bytes.
48
+ - `handoffInProgress` / `HandoffSentinel.inProgress` race guard: there is currently NO
49
+ `holdsLease`-style gate in the scheduler or reaper to wire it into, so it is exposed
50
+ (AgentServer option + `GET /handoff/status`) for future consumers rather than threaded into a
51
+ nonexistent gate — wiring into a gate that doesn't exist would be scope creep. <!-- tracked: topic-13481 -->
52
+ - The trigger route is bearer-authed via the existing global `authMiddleware`; no new auth
53
+ surface. The machine-to-machine begin/ack/yield routes (signed) are unchanged.
54
+
55
+ ## Rollback cost
56
+ - Low. Two new standalone modules + two AgentServer options + one boot block. Reverting the
57
+ boot block + the route mount fully disables the trigger; the protocol primitives (already on
58
+ main from #419 + earlier wire increments) are untouched. No migration, no persisted state, no
59
+ schema. The route 503s by construction when the option is absent, so a partial revert
60
+ degrades safely.
61
+
62
+ ## Tests
63
+ - `tests/unit/handoff-sentinel-boot-wiring.test.ts` (6) — wiring-integrity: non-null sentinel;
64
+ full happy `initiate()` delegates pushTick→sendBegin→awaitAck→sendYield→demote in order (not a
65
+ no-op); abort-on-echo-mismatch never yields/demotes; `pickActiveTopic` on all three branches.
66
+ - `tests/e2e/planned-handoff-e2e.test.ts` (4) — two real booted AgentServers with real peer
67
+ resolvers: caught-up handoff (handed-off + B acquires with A's id + A demotes), stale-standby
68
+ abort (no yield, A keeps lease), route alive (200 outcome), route 503 when unwired.
@@ -0,0 +1,52 @@
1
+ # Side-Effects Review — Cross-Machine Seamlessness: incoming handoff receiver wiring
2
+
3
+ **Spec:** docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §8 G3d/G3e (converged, approved)
4
+
5
+ Wires the INCOMING side of the planned handoff into the live server: the begin route
6
+ now drives a real HandoffReceiver that builds + sends the caught-up ack, and the yield
7
+ route now drives the lease CAS. Closes the gap where HandoffReceiver was a tested class
8
+ with no production caller (spec §10 mandates it be constructed in startup, not dead code).
9
+
10
+ ## What changed
11
+ - `src/core/handoffReceiverWiring.ts` — NEW. `createHandoffReceiverWiring(deps)` composes a
12
+ HandoffReceiver with ops bound to injected deps (sendAck, acquireLeaseOnConsent,
13
+ getTopicHistory) and returns `{ receiver, onBegin, yieldHandler }`. Extracted as a unit
14
+ (not inline in server.ts) per the Testing Integrity Standard so the DI wiring is testable.
15
+ Also exports `hashTopicHistory(getTopicHistory, topic)` — the canonical thread-history hash
16
+ the OUTGOING flush must reuse so the echo verifies. The hash uses the SAME content
17
+ formatting as LiveTailSource (`[ts] text\n`).
18
+ - `src/commands/server.ts` — in the mesh/git block, when a handoffWireTransport + telegram +
19
+ coordinator exist, build the wiring, register `transport.onYield(wiring.yieldHandler)`, and
20
+ pass `wiring.onBegin` as the AgentServer `onHandoffBegin`. Gated on coordinator.enabled; a
21
+ solo/non-mesh agent constructs nothing.
22
+
23
+ ## Over-block / under-block
24
+ - The lease CAS (`acquireLeaseOnConsent`) is invoked ONLY by the yield handler, and the
25
+ HandoffReceiver ignores a yield unless it is in `ack_sent` (a handoff is genuinely underway).
26
+ A bare/forged yield with no prior begin → no CAS (tested). The ack never moves the lease.
27
+ - buildAck RECOMPUTES the hash from this machine's own synced history — it never echoes the
28
+ manifest's own hash. A not-caught-up standby therefore produces a non-matching echo, and the
29
+ outgoing's verify (HandoffSentinel.ackMatches) refuses to yield (tested: caught-up matches,
30
+ stale differs).
31
+ - Manifest absent at buildAck time → throws → receiver goes `failed`, no ack sent (safe).
32
+
33
+ ## Signal vs authority
34
+ - The wiring carries signals (manifest in, ack out) and triggers the consent-gated CAS, which
35
+ is the coordinator's authority. The wiring itself mutates no role/registry state.
36
+
37
+ ## Interactions
38
+ - Reuses telegram.getTopicHistory (same call LiveTailSource uses for the tail content),
39
+ coordinator.acquireLeaseOnConsent (already shipped), and the HandoffWireTransport (sendAck /
40
+ onYield). The OUTGOING HandoffSentinel (next increment C2) POSTs the begin + drives the
41
+ flush and must reuse `hashTopicHistory` for the echo to verify. <!-- tracked: topic-13481 -->
42
+
43
+ ## Rollback cost
44
+ - Low. One new standalone module + a gated wiring block. Reverting leaves the begin/ack/yield
45
+ routes 503 (no callback supplied) — exactly the prior state, no behavior change to a solo agent.
46
+
47
+ ## Tests
48
+ - `tests/unit/handoff-receiver-wiring.test.ts` (3): begin → caught-up ack echoes tailSeq +
49
+ ingressPosition + a hash matching the outgoing's (caught-up) ; a stale local history yields a
50
+ NON-matching hash (so the echo won't verify) ; a yield with no handoff is ignored, and a yield
51
+ after a begin drives acquireLeaseOnConsent with the begin originator's machine id, reaching
52
+ `acquired`. tsc clean.
@@ -0,0 +1,46 @@
1
+ # Side-Effects Review — Cross-Machine Seamlessness: HandoffReceiver (incoming side)
2
+
3
+ **Spec:** docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §8 G3d/G3e (converged, approved)
4
+
5
+ The INCOMING-machine half of the planned handoff — the counterpart to the
6
+ outgoing-only HandoffSentinel. Completes the two-sided handoff protocol at the
7
+ component level (both sides now exist + tested; only server wiring remains).
8
+
9
+ ## What changed
10
+ - `src/core/HandoffReceiver.ts` — NEW, standalone state machine:
11
+ idle → catching_up → ack_sent → acquired | failed.
12
+ - onBeginHandoff(): builds the caught-up ack (tailSeq + ingressPosition +
13
+ threadHistoryHash), sends it to the outgoing machine. Build/send failure → failed.
14
+ - onYield(): acquires the lease via the consented path — but ONLY while a handoff
15
+ is genuinely in progress (state ack_sent). A stray yield is ignored.
16
+ All I/O (buildAck/sendAck/acquireOnYield) injected → unit-testable, channel-agnostic.
17
+
18
+ ## Over-block / under-block
19
+ - Acting on yield ONLY in ack_sent is a deliberate over-guard: a stray/replayed yield
20
+ with no handoff underway is ignored (and the lease's own consent guard already refuses
21
+ a yield from a non-holder — defense in depth).
22
+ - A failed consent acquire leaves the machine STANDBY (failed state), never a partial/
23
+ ambiguous role — pairs with the outgoing's "stay awake on no verified handoff", so a
24
+ broken handoff degrades to "no change", never to two-leaders or no-leader.
25
+
26
+ ## Signal vs authority
27
+ - No authority of its own — it sequences signals (ack out, yield in) and delegates the
28
+ authority mutation to coordinator.acquireLeaseOnConsent (which is itself guarded). The
29
+ DECISION to yield lives on the outgoing (HandoffSentinel).
30
+
31
+ ## Interactions
32
+ - Pairs with HandoffSentinel (outgoing), HandoffWireTransport (sendAck/onYield delivery),
33
+ LiveTailBuffer (tailSeq for the ack), and coordinator.acquireLeaseOnConsent (the acquire).
34
+ - **Next (final integration):** server.ts constructs both HandoffSentinel + HandoffReceiver,
35
+ binds their ops to the live transports/coordinator/telegram, wires the routes live
36
+ (onHandoffAck→sentinel.recordAck via HandoffWireTransport; onHandoffYield→receiver.onYield;
37
+ a begin-handoff signal→receiver.onBeginHandoff), adds the handoff trigger + the live-tail
38
+ streaming cadence + the content provider, and ships the boots-the-server e2e test.
39
+
40
+ ## Rollback cost
41
+ - Minimal — one new standalone file, unreferenced by the live path until the integration step.
42
+
43
+ ## Tests
44
+ - `tests/unit/HandoffReceiver.test.ts` (6): begin→build+send ack; ack-not-accepted → failed;
45
+ buildAck-throws → failed; yield-after-ack → acquired; stray-yield-ignored; failed-acquire
46
+ → standby. tsc clean.
@@ -0,0 +1,47 @@
1
+ # Side-Effects Review — Cross-Machine Seamlessness: outgoing handoff sentinel wiring
2
+
3
+ **Spec:** docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §8 G3e (converged, approved)
4
+
5
+ The OUTGOING-machine orchestration logic for a planned handoff, as a tested unit: drive the
6
+ live tail → POST begin → await + verify the ack → validate → yield → demote. Counterpart to
7
+ the incoming receiver wiring (commit 79e9bf23f). This increment lands the LOGIC + its
8
+ transport primitive; bolting the sentinel into server.ts boot + the operator/test trigger
9
+ route + the two-server e2e are the next sub-increment (C2b/C3, tracked in the wiring plan).
10
+
11
+ ## What changed
12
+ - `src/core/HandoffWireTransport.ts` — NEW `sendBegin(manifest)` → POST /api/handoff/begin to
13
+ the peer. Symmetric with the existing sendAck/sendYield; rides the same signed channel.
14
+ - `src/core/handoffSentinelWiring.ts` — NEW `createHandoffSentinelWiring(deps)` composes a real
15
+ HandoffSentinel with ops bound to injected deps: flush (pushTick the live tail, build the
16
+ manifest with `hashTopicHistory` reused from the receiver wiring so the echo verifies, POST
17
+ begin), awaitAck, validate (default deterministic — ackMatches is the substantive gate; Haiku
18
+ Tier-1 is the spec's tracked upgrade), sendYield, demoteSelf. Returns `{ sentinel, initiate }`.
19
+
20
+ ## Over-block / under-block
21
+ - The lease is NEVER yielded unless the ack echo verifies (HandoffSentinel.ackMatches) AND
22
+ validate passes — proven by tests: a mismatched echo, an absent ack, a failed validation, and
23
+ an unreachable-peer begin all return `aborted-stay-awake`/`failed` with ZERO sendYield and
24
+ ZERO demoteSelf calls. The outgoing simply stays awake. No two-holders window.
25
+ - flush throws if the begin POST is not accepted (no reachable peer / rejected) → the sentinel
26
+ reports `failed` before any ack wait — it never proceeds to yield on a peer it can't reach.
27
+
28
+ ## Signal vs authority
29
+ - The wiring supplies ops only. The DECISION to yield lives in HandoffSentinel (verify + validate
30
+ gate); the lease CAS authority is the incoming coordinator's, triggered solely by the yield.
31
+
32
+ ## Interactions
33
+ - Reuses hashTopicHistory (handoffReceiverWiring) so outgoing-flush and incoming-echo hash the
34
+ SAME bytes → the caught-up check is meaningful. Consumes HandoffWireTransport (sendBegin/
35
+ awaitAck/sendYield) + telegram ingress/history + coordinator.demoteToStandby (all shipped).
36
+ - NOT yet constructed in server.ts boot; no trigger route yet → no runtime behavior change.
37
+ Bolting in + the trigger + the e2e are C2b/C3. <!-- tracked: topic-13481 -->
38
+
39
+ ## Rollback cost
40
+ - Minimal. Two new standalone additions (one method + one module). No boot wiring yet, so a
41
+ revert is a pure no-op on the running pipeline.
42
+
43
+ ## Tests
44
+ - `tests/unit/handoff-sentinel-wiring.test.ts` (5): verified+validated ack → flush+yield+demote+
45
+ handed-off ; mismatched echo → abort, no yield ; no ack → abort, no yield ; failed validate →
46
+ abort, no yield ; unreachable-peer begin → failed, no yield. tsc clean; HandoffWireTransport
47
+ unit suite still green (7).
@@ -0,0 +1,46 @@
1
+ # Side-Effects Review — Cross-Machine Seamlessness: lease wire transport (§6)
2
+
3
+ **Spec:** CROSS-MACHINE-SEAMLESSNESS-SPEC.md §6 ("the low-latency authoritative copy of the lease travels over the tunnel"). First piece of the wire-transport increment (live-tail-flush + handoff-ack transport follow).
4
+
5
+ ## What changed
6
+ - `src/core/HttpLeaseTransport.ts` (new) — implements LeaseTransport over the
7
+ existing authenticated machine channel (signRequest + machineAuth). broadcast()
8
+ POSTs the signed lease to each peer's /api/lease; observed() returns the
9
+ freshest received lease + per-holder nonce map; isReachable() reflects recent
10
+ broadcast success; recordObserved() ingests a peer lease (highest-epoch wins,
11
+ nonce watermark advances, replays dropped).
12
+ - `src/server/machineRoutes.ts` — new auth-verified POST /api/lease receiver
13
+ (lease holder must match the authenticated machine) → ctx.onLeaseReceived.
14
+ - `src/server/AgentServer.ts` — new optional `leaseTransport` option; wires
15
+ onLeaseReceived → leaseTransport.recordObserved.
16
+ - `src/commands/server.ts` — constructs HttpLeaseTransport, passes it as the
17
+ LeaseCoordinator's `tunnel`, and into AgentServer.
18
+
19
+ ## Over/under-block, safety
20
+ - SINGLE-MACHINE SAFE: with no peers, broadcast() is a reachable no-op and
21
+ isReachable() stays true, so the lease behaves EXACTLY as git-only (Echo and
22
+ all single-machine agents are unaffected — no behavior change).
23
+ - Multi-machine: the lease now also travels over the wire (RTT-bounded
24
+ acquisition) and renewal requires reaching a medium (tunnel OR git) — the
25
+ spec's split-authority guard. FencedLease.acceptTunnelLease re-verifies the
26
+ Ed25519 signature + git-epoch floor + nonce before trusting a wire lease, so a
27
+ forged/replayed/below-floor broadcast is rejected regardless of the endpoint.
28
+
29
+ ## Signal vs authority / interactions
30
+ - The endpoint is signature-gated (machineAuth) AND holder-must-match-auth; the
31
+ transport is a fast COPY, never the authority (git CAS + FencedLease fencing
32
+ remain authoritative; max(tunnel,git) never drops below the git floor).
33
+ - recordObserved keeps only the highest-epoch lease + advances the nonce
34
+ watermark → an at-least-once wire redelivery cannot regress the view.
35
+
36
+ ## Rollback cost
37
+ - Low. leaseTransport is optional; removing the server.ts wiring reverts the
38
+ LeaseCoordinator to git-only (the prior, tested behavior). The endpoint is
39
+ inert without onLeaseReceived. New module unreferenced if unwired.
40
+
41
+ ## Tests
42
+ - tests/unit/HttpLeaseTransport.test.ts (6): broadcast reachability, single-machine
43
+ no-op, observed recording, nonce watermark + replay drop, highest-epoch wins,
44
+ reachability window. LeaseCoordinator + FencedLease suites still green (35 total).
45
+ - Live two-machine wire behavior is exercised by the real-hardware gate (the
46
+ over-Telegram test increment).
@@ -0,0 +1,50 @@
1
+ # Side-Effects Review — Cross-Machine Seamlessness: LiveTailSource (holder flush producer)
2
+
3
+ **Spec:** docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §8 G3b (converged, approved)
4
+
5
+ Fourth piece of the wire-transport increment. The holder-side producer that drives
6
+ the live-tail SENDER (HttpLiveTailTransport.broadcast) — the missing primitive that
7
+ makes the encrypted stream actually fire, and that the handoff flush() builds its
8
+ manifest from.
9
+
10
+ ## What changed
11
+ - `src/core/LiveTailSource.ts` — NEW, standalone. Per-topic delta accounting: tracks
12
+ the content already streamed to the standby and emits only the NEW suffix as a
13
+ monotonic-sequence flush (the standby's LiveTailBuffer APPENDS contiguous flushes,
14
+ so the source must send deltas, not the full tail). Drivers: pushTick() (cadence,
15
+ keeps the standby fresh to liveTailMaxStalenessMs) and flushTopic/flushAll (handoff,
16
+ returns per-topic seqs for the manifest). currentSeq() exposes the high-water for
17
+ the manifest. Content provider + transport injected → channel-agnostic + testable.
18
+
19
+ ## Over-block / under-block
20
+ - A failed broadcast does NOT advance the streamed/seq state, so the same delta retries
21
+ on the next tick (the buffer dedups on seq, so a retry is harmless). This is the
22
+ correct under-action: never mark content delivered that the standby didn't ack.
23
+ - No new content → no flush, no seq bump — duplicate ticks can't inflate the standby's
24
+ sequence or push empty flushes.
25
+ - Content divergence (history rewrite/compaction shrinks or changes the prefix) → resend
26
+ the full content as a fresh delta, rather than send a broken suffix.
27
+
28
+ ## Signal vs authority
29
+ - Pure producer — no authority. It decides only WHAT content is new; it does not decide
30
+ who leads or who may send (the lease/FencedOutbox do). It is gated upstream: the wiring
31
+ step only runs the cadence/handoff flush on the machine that holds the lease.
32
+
33
+ ## Interactions
34
+ - Drives HttpLiveTailTransport.broadcast (already shipped, redacts+encrypts). The
35
+ correctness test feeds the source's deltas through a REAL LiveTailBuffer and asserts
36
+ the original conversation is reconstructed exactly — the delta model and the buffer's
37
+ append model are proven consistent.
38
+ - **Next piece (same increment):** the integrating commit wires LiveTailSource into the
39
+ server (content provider = recent topic messages; cadence timer on the holder; handoff
40
+ flush() = flushAll() + manifest from currentSeq), takes the handoff routes live, binds
41
+ HandoffSentinel ops, and adds the boots-the-server e2e test.
42
+
43
+ ## Rollback cost
44
+ - Minimal — one new standalone file, unreferenced by the live path until the wiring step.
45
+
46
+ ## Tests
47
+ - `tests/unit/LiveTailSource.test.ts` (6): first-flush-full / subsequent-delta, no-new-
48
+ content no-op, divergence resends full, failed-broadcast-does-not-advance (retry-safe),
49
+ **deltas reconstruct the original tail through a real LiveTailBuffer**, flushAll covers
50
+ all topics. tsc clean.
@@ -0,0 +1,50 @@
1
+ # Side-Effects Review — Cross-Machine Seamlessness: live-tail streaming wired live
2
+
3
+ **Spec:** docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §8 G3b (converged, approved)
4
+
5
+ Makes the encrypted live-tail stream END-TO-END LIVE: the holder now actually pushes
6
+ the conversation tail to the standby on a cadence (the receiver has been live since the
7
+ live-tail-wire commit). First true integration into the running server.
8
+
9
+ ## What changed
10
+ - `src/messaging/TelegramAdapter.ts` — NEW `getKnownTopicIds(): number[]` accessor
11
+ (keys of topicToSession) — the set of live conversations to stream.
12
+ - `src/commands/server.ts`:
13
+ - In the lease block, constructs the holder-side `HttpLiveTailTransport` (sender) —
14
+ peers resolved from the registry, each peer's X25519 key via
15
+ `idMgr.getEncryptionPublicKeyPem` → SPKI-DER base64; content encrypted via encryptForSync.
16
+ - Before AgentServer construction, constructs `LiveTailSource` (content provider =
17
+ `telegram.getTopicHistory(topic, 500)` formatted append-only; active topics =
18
+ `telegram.getKnownTopicIds()`) and a cadence `setInterval(liveTailPushRateMs)` that
19
+ calls `pushTick()` — GATED on `coordinator.holdsLease()` so ONLY the awake machine
20
+ streams (mirrors scheduler/sentinel gating). Timer unref'd. Only when
21
+ `liveTailTransport: 'tunnel'` + coordinator enabled + telegram present.
22
+
23
+ ## Over-block / under-block
24
+ - Holder-gated: a standby that does not hold the lease never streams (no double-push).
25
+ - Solo agent / no peers: the sender's broadcast is a reachable no-op — zero behavior change
26
+ for single-machine installs (the dominant case). `liveTailTransport: 'git'` skips it entirely.
27
+ - Window-shift caveat: getTopicHistory is a bounded recent window (500), so a conversation
28
+ exceeding it triggers a one-off full resend (LiveTailSource divergence path) — the standby
29
+ buffer dedups by seq + caps by bytes, so this is correct, just occasionally redundant.
30
+ Documented; a growing/append-only provider is a tracked tuning follow-on. <!-- tracked: ACT-156 -->
31
+
32
+ ## Signal vs authority
33
+ - The cadence timer reads authority (coordinator.holdsLease()) to decide whether to stream;
34
+ it does not mutate authority. The sender carries redacted+encrypted content only.
35
+
36
+ ## Interactions
37
+ - Sender (HttpLiveTailTransport) + LiveTailSource + the already-live receiver
38
+ (/api/live-tail → decrypt → LiveTailBuffer) now form the complete one-directional stream.
39
+ - The handoff flush() (next commit) will reuse this same LiveTailSource for its manifest.
40
+
41
+ ## Rollback cost
42
+ - Low. The accessor is additive; the streaming block is guarded and unref'd. Reverting removes
43
+ the block + accessor with no effect on single-machine installs.
44
+
45
+ ## Tests
46
+ - Components already unit-tested: HttpLiveTailTransport (6), LiveTailSource (6, incl. the
47
+ reconstruct-through-LiveTailBuffer correctness proof), live-tail-receive-roundtrip (3).
48
+ 101 seamlessness unit tests green; tsc clean. The true holder→standby streaming is
49
+ exercised by the two-machine hardware test-as-self (task 8) — a single-machine e2e cannot
50
+ drive a no-peer no-op sender.
@@ -0,0 +1,65 @@
1
+ # Side-Effects Review — Cross-Machine Seamlessness: live-tail wire (receiver) + transport
2
+
3
+ **Spec:** docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §8 G3b/G3c (converged, approved)
4
+
5
+ Second piece of the wire-transport increment (after the lease wire transport). Stands
6
+ up the encrypted holder→standby live-tail channel: the SENDER transport class and the
7
+ RECEIVER end-to-end (route → decrypt → sequence-deduped buffer, fully live).
8
+
9
+ ## What changed
10
+ - `src/core/HttpLiveTailTransport.ts` — NEW. Holder-side sender: redacts content
11
+ (liveTailRedaction) BEFORE anything leaves the machine, encrypts per-peer with the v3
12
+ machine scheme (encryptForSync: ephemeral X25519 + AES-256-GCM, forward-secret per
13
+ flush), and POSTs to each standby's `/api/live-tail` over the authenticated machine
14
+ channel (signRequest). No peers → reachable no-op (solo agent unaffected). Injected
15
+ fetch/encryptor/clock for testability.
16
+ - `src/server/machineRoutes.ts` — NEW route `POST /api/live-tail` (authMiddleware) +
17
+ `onLiveTailReceived` ctx callback. Validates the flush shape, rejects an unverifiable
18
+ peer at the auth boundary BEFORE any content is accepted (§8 G3c), hands the encrypted
19
+ flush to the server-lifecycle decryptor, returns 400 on decrypt/verify failure (logged
20
+ to SecurityLog as `live_tail_rejected`).
21
+ - `src/server/AgentServer.ts` — NEW `liveTailReceiver` option, passed through to the
22
+ machine-routes context as `onLiveTailReceived`.
23
+ - `src/commands/server.ts` — constructs the standby-side `LiveTailBuffer` + the decrypt
24
+ receiver closure (decrypts with THIS machine's X25519 private key, then sequence-deduped
25
+ applyFlush), wired only when `liveTailTransport: 'tunnel'`. The holder-side sender
26
+ (HttpLiveTailTransport.broadcast) is constructed/driven by the flush producer in the
27
+ NEXT increment piece (inbound-dispatch + handoff) — the class ships unit-tested here.
28
+
29
+ ## Over-block / under-block
30
+ - The receiver REJECTS (400) any flush it cannot decrypt/verify — intended over-block; a
31
+ forged or wrong-key flush must never enter the context window. Replay/duplicate seqs are
32
+ dropped by LiveTailBuffer (no double-append corrupting history).
33
+ - Redaction is conservative/high-precision (false positives acceptable per §8 G3c); ordinary
34
+ prose is left intact (tested).
35
+ - Solo / no-peer: the sender is a no-op and the receiver simply never fires — identical to a
36
+ disabled mesh. `liveTailTransport: 'git'` skips the tunnel receiver entirely.
37
+
38
+ ## Signal vs authority
39
+ - The transport carries no authority — it does not decide who is leader or who may send;
40
+ it only moves (redacted, encrypted) context. The FencedOutbox/lease (separate) hold send
41
+ authority. The receiver's only judgment is cryptographic (auth tag + machine-auth), not policy.
42
+
43
+ ## Interactions
44
+ - Reuses the established v3 machine path: `signRequest`/`machineAuthMiddleware` (mutual auth),
45
+ `encryptForSync`/`decryptFromSync` (the same scheme as secret sync), and `MachineIdentity`'s
46
+ dedicated X25519 encryption keys — no new crypto invented.
47
+ - Feeds `LiveTailBuffer` (already shipped), whose sequence-dedup is the context-integrity guard.
48
+ - **Tracked follow-on (not this commit):** carry-by-reference for large tool output (§8 G3b) —
49
+ depends on the durable encrypted work-ledger reference mechanism not yet built; redaction
50
+ (the actual security requirement) is fully in place. <!-- tracked: ACT-156 -->
51
+ - **Next piece (same increment):** the holder flush producer drives `broadcast()` from the live
52
+ message path; the handoff-ack transport + HandoffSentinel live-wiring consume the buffer.
53
+
54
+ ## Rollback cost
55
+ - Minimal. The route is additive (unknown route → 404 previously); the option is optional;
56
+ the sender class is unreferenced by the live path until the next piece. Reverting removes
57
+ the new file + the four additive edits.
58
+
59
+ ## Tests
60
+ - `tests/unit/HttpLiveTailTransport.test.ts` (6): no-peer no-op, signed POST to /api/live-tail,
61
+ **redaction-before-encryption** (no secret leaves in the clear — security-negative), per-peer
62
+ encryption, unreachable-on-all-error, non-ok-not-reachable.
63
+ - `tests/unit/live-tail-receive-roundtrip.test.ts` (3): encrypt→decrypt→apply round-trip,
64
+ replayed-seq dropped (no double-append), **non-recipient cannot decrypt** (security-negative).
65
+ - Full seamlessness suite green (76 unit tests across 9 files); tsc clean.