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.
- package/dashboard/index.html +68 -14
- package/dashboard/process-health.js +204 -53
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +292 -3
- package/dist/commands/server.js.map +1 -1
- package/dist/core/HandoffReceiver.d.ts +56 -0
- package/dist/core/HandoffReceiver.d.ts.map +1 -0
- package/dist/core/HandoffReceiver.js +103 -0
- package/dist/core/HandoffReceiver.js.map +1 -0
- package/dist/core/HandoffWireTransport.d.ts +61 -0
- package/dist/core/HandoffWireTransport.d.ts.map +1 -0
- package/dist/core/HandoffWireTransport.js +114 -0
- package/dist/core/HandoffWireTransport.js.map +1 -0
- package/dist/core/HttpLeaseTransport.d.ts +68 -0
- package/dist/core/HttpLeaseTransport.d.ts.map +1 -0
- package/dist/core/HttpLeaseTransport.js +97 -0
- package/dist/core/HttpLeaseTransport.js.map +1 -0
- package/dist/core/HttpLiveTailTransport.d.ts +88 -0
- package/dist/core/HttpLiveTailTransport.d.ts.map +1 -0
- package/dist/core/HttpLiveTailTransport.js +95 -0
- package/dist/core/HttpLiveTailTransport.js.map +1 -0
- package/dist/core/LeaseCoordinator.d.ts +14 -0
- package/dist/core/LeaseCoordinator.d.ts.map +1 -1
- package/dist/core/LeaseCoordinator.js +38 -0
- package/dist/core/LeaseCoordinator.js.map +1 -1
- package/dist/core/LiveTailSource.d.ts +65 -0
- package/dist/core/LiveTailSource.d.ts.map +1 -0
- package/dist/core/LiveTailSource.js +87 -0
- package/dist/core/LiveTailSource.js.map +1 -0
- package/dist/core/MultiMachineCoordinator.d.ts +14 -0
- package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
- package/dist/core/MultiMachineCoordinator.js +22 -0
- package/dist/core/MultiMachineCoordinator.js.map +1 -1
- package/dist/core/ReplyMarkerTransport.d.ts +58 -0
- package/dist/core/ReplyMarkerTransport.d.ts.map +1 -0
- package/dist/core/ReplyMarkerTransport.js +66 -0
- package/dist/core/ReplyMarkerTransport.js.map +1 -0
- package/dist/core/handoffReceiverWiring.d.ts +57 -0
- package/dist/core/handoffReceiverWiring.d.ts.map +1 -0
- package/dist/core/handoffReceiverWiring.js +65 -0
- package/dist/core/handoffReceiverWiring.js.map +1 -0
- package/dist/core/handoffSentinelBootWiring.d.ts +53 -0
- package/dist/core/handoffSentinelBootWiring.d.ts.map +1 -0
- package/dist/core/handoffSentinelBootWiring.js +52 -0
- package/dist/core/handoffSentinelBootWiring.js.map +1 -0
- package/dist/core/handoffSentinelWiring.d.ts +60 -0
- package/dist/core/handoffSentinelWiring.d.ts.map +1 -0
- package/dist/core/handoffSentinelWiring.js +59 -0
- package/dist/core/handoffSentinelWiring.js.map +1 -0
- package/dist/core/seamlessnessConfig.d.ts +8 -0
- package/dist/core/seamlessnessConfig.d.ts.map +1 -1
- package/dist/core/seamlessnessConfig.js +1 -0
- package/dist/core/seamlessnessConfig.js.map +1 -1
- package/dist/core/types.d.ts +6 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/messaging/TelegramAdapter.d.ts +6 -0
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +8 -0
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/messaging/ingressDedup.d.ts +55 -0
- package/dist/messaging/ingressDedup.d.ts.map +1 -0
- package/dist/messaging/ingressDedup.js +69 -0
- package/dist/messaging/ingressDedup.js.map +1 -0
- package/dist/messaging/stuckMessageRecovery.d.ts +46 -0
- package/dist/messaging/stuckMessageRecovery.d.ts.map +1 -0
- package/dist/messaging/stuckMessageRecovery.js +52 -0
- package/dist/messaging/stuckMessageRecovery.js.map +1 -0
- package/dist/server/AgentServer.d.ts +64 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +24 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/handoffInitiateRoutes.d.ts +40 -0
- package/dist/server/handoffInitiateRoutes.d.ts.map +1 -0
- package/dist/server/handoffInitiateRoutes.js +57 -0
- package/dist/server/handoffInitiateRoutes.js.map +1 -0
- package/dist/server/machineRoutes.d.ts +47 -0
- package/dist/server/machineRoutes.d.ts.map +1 -1
- package/dist/server/machineRoutes.js +138 -0
- package/dist/server/machineRoutes.js.map +1 -1
- package/dist/server/routes.d.ts +20 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +65 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +49 -49
- package/upgrades/1.3.44.md +72 -0
- package/upgrades/1.3.45.md +28 -0
- package/upgrades/side-effects/cross-machine-seamlessness-consent-acquire.md +52 -0
- package/upgrades/side-effects/cross-machine-seamlessness-exactly-once-ingress.md +73 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-ack-yield.md +82 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-begin.md +48 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-boot-wiring.md +68 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-receiver-wiring.md +52 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-receiver.md +46 -0
- package/upgrades/side-effects/cross-machine-seamlessness-handoff-sentinel-wiring.md +47 -0
- package/upgrades/side-effects/cross-machine-seamlessness-lease-wire.md +46 -0
- package/upgrades/side-effects/cross-machine-seamlessness-livetail-source.md +50 -0
- package/upgrades/side-effects/cross-machine-seamlessness-livetail-streaming.md +50 -0
- package/upgrades/side-effects/cross-machine-seamlessness-livetail-wire.md +65 -0
- package/upgrades/side-effects/cross-machine-seamlessness-noloss-recovery.md +61 -0
- package/upgrades/side-effects/cross-machine-seamlessness-reply-marker.md +65 -0
- 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.
|