instar 1.3.18 → 1.3.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +104 -6
- package/dist/commands/server.js.map +1 -1
- package/dist/core/FencedLease.d.ts +126 -0
- package/dist/core/FencedLease.d.ts.map +1 -0
- package/dist/core/FencedLease.js +189 -0
- package/dist/core/FencedLease.js.map +1 -0
- package/dist/core/GitLeaseStore.d.ts +46 -0
- package/dist/core/GitLeaseStore.d.ts.map +1 -0
- package/dist/core/GitLeaseStore.js +79 -0
- package/dist/core/GitLeaseStore.js.map +1 -0
- package/dist/core/GitSync.d.ts +19 -0
- package/dist/core/GitSync.d.ts.map +1 -1
- package/dist/core/GitSync.js +77 -0
- package/dist/core/GitSync.js.map +1 -1
- package/dist/core/HandoffSentinel.d.ts +80 -0
- package/dist/core/HandoffSentinel.d.ts.map +1 -0
- package/dist/core/HandoffSentinel.js +141 -0
- package/dist/core/HandoffSentinel.js.map +1 -0
- package/dist/core/LeaseCoordinator.d.ts +141 -0
- package/dist/core/LeaseCoordinator.d.ts.map +1 -0
- package/dist/core/LeaseCoordinator.js +226 -0
- package/dist/core/LeaseCoordinator.js.map +1 -0
- package/dist/core/LiveTailBuffer.d.ts +64 -0
- package/dist/core/LiveTailBuffer.d.ts.map +1 -0
- package/dist/core/LiveTailBuffer.js +122 -0
- package/dist/core/LiveTailBuffer.js.map +1 -0
- package/dist/core/MultiMachineCoordinator.d.ts +49 -0
- package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
- package/dist/core/MultiMachineCoordinator.js +136 -0
- package/dist/core/MultiMachineCoordinator.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +24 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/RegistrySyncDebouncer.d.ts +77 -0
- package/dist/core/RegistrySyncDebouncer.d.ts.map +1 -0
- package/dist/core/RegistrySyncDebouncer.js +131 -0
- package/dist/core/RegistrySyncDebouncer.js.map +1 -0
- package/dist/core/liveTailRedaction.d.ts +36 -0
- package/dist/core/liveTailRedaction.d.ts.map +1 -0
- package/dist/core/liveTailRedaction.js +57 -0
- package/dist/core/liveTailRedaction.js.map +1 -0
- package/dist/core/registryReplayGuard.d.ts +59 -0
- package/dist/core/registryReplayGuard.d.ts.map +1 -0
- package/dist/core/registryReplayGuard.js +86 -0
- package/dist/core/registryReplayGuard.js.map +1 -0
- package/dist/core/seamlessnessConfig.d.ts +79 -0
- package/dist/core/seamlessnessConfig.d.ts.map +1 -0
- package/dist/core/seamlessnessConfig.js +131 -0
- package/dist/core/seamlessnessConfig.js.map +1 -0
- package/dist/core/types.d.ts +117 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/wireRegistrySync.d.ts +32 -0
- package/dist/core/wireRegistrySync.d.ts.map +1 -0
- package/dist/core/wireRegistrySync.js +31 -0
- package/dist/core/wireRegistrySync.js.map +1 -0
- package/dist/feedback-factory/dispatch/dispatch.d.ts +47 -0
- package/dist/feedback-factory/dispatch/dispatch.d.ts.map +1 -0
- package/dist/feedback-factory/dispatch/dispatch.js +72 -0
- package/dist/feedback-factory/dispatch/dispatch.js.map +1 -0
- package/dist/messaging/FencedOutbox.d.ts +42 -0
- package/dist/messaging/FencedOutbox.d.ts.map +1 -0
- package/dist/messaging/FencedOutbox.js +60 -0
- package/dist/messaging/FencedOutbox.js.map +1 -0
- package/dist/messaging/MessageProcessingLedger.d.ts +105 -0
- package/dist/messaging/MessageProcessingLedger.d.ts.map +1 -0
- package/dist/messaging/MessageProcessingLedger.js +206 -0
- package/dist/messaging/MessageProcessingLedger.js.map +1 -0
- package/dist/messaging/TelegramAdapter.d.ts +18 -1
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +50 -0
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +7 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +6 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +64 -64
- package/src/scaffold/templates.ts +7 -0
- package/upgrades/1.3.19.md +23 -0
- package/upgrades/side-effects/PR-BODY-cross-machine-seamlessness.md +63 -0
- package/upgrades/side-effects/cross-machine-seamlessness-g1-lease.md +71 -0
- package/upgrades/side-effects/cross-machine-seamlessness-g2-foundation.md +75 -0
- package/upgrades/side-effects/cross-machine-seamlessness-g3-telegram-contract.md +36 -0
- package/upgrades/side-effects/cross-machine-seamlessness-g3a-ledger-handoff.md +56 -0
- package/upgrades/side-effects/cross-machine-seamlessness-g3bc-livetail-outbox.md +51 -0
- package/upgrades/side-effects/cross-machine-seamlessness-p4-awareness-observability.md +54 -0
- package/upgrades/side-effects/feedback-factory-dispatch.md +31 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Cross-Machine Seamlessness — one agent that follows the user across machines
|
|
2
|
+
|
|
3
|
+
Implements the converged + approved **Cross-Machine Seamlessness** spec
|
|
4
|
+
(`docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md`), grounded in the 2026-05-26
|
|
5
|
+
real-hardware Phase-0 run. Closes the gap between "a backup machine can take
|
|
6
|
+
over" and "the same agent follows you across machines with no amnesia and no
|
|
7
|
+
double-replies."
|
|
8
|
+
|
|
9
|
+
## What this fixes (the Phase-0 findings)
|
|
10
|
+
- **G2 — auto-sync never fired.** A self-electing machine changed its role but
|
|
11
|
+
nothing pushed it to git. Now `roleChange`/`leaseEpochChange` → a debounced,
|
|
12
|
+
single-writer `RegistrySyncDebouncer` push (the wiring is named + a
|
|
13
|
+
wiring-integrity test reproduces the original failure and proves it caught).
|
|
14
|
+
- **G1 — split-brain.** Two machines both showed `awake`. Replaced with a
|
|
15
|
+
**fenced lease** (clock-proof, epoch-fenced CAS); `shouldSkipProcessing` gates
|
|
16
|
+
on `holdsLease()`, so a wedged old-awake is structurally fenced out. A holder
|
|
17
|
+
that can't confirm its lease over a shared medium for > leaseTtlMs
|
|
18
|
+
self-suspends (no partitioned-old-awake split-brain).
|
|
19
|
+
- **G3 — the seamless channel experience.** Durable message-processing ledger
|
|
20
|
+
(no-loss / no-duplicate-reply, structural), fencing-gated outbox, encrypted-
|
|
21
|
+
live-tail buffer with sequence-dedup + secret redaction, and a HandoffSentinel
|
|
22
|
+
that yields the lease ONLY on a verified ack + passing validation (else stays
|
|
23
|
+
awake). Channel Seamlessness Contract implemented by Telegram (reference) +
|
|
24
|
+
Slack (second target).
|
|
25
|
+
|
|
26
|
+
## Layers (commit-by-commit)
|
|
27
|
+
1. Foundations + G2 auto-sync (types, config resolver/validator, FencedLease,
|
|
28
|
+
RegistrySyncDebouncer, registryReplayGuard, the named wiring).
|
|
29
|
+
2. G1 fenced-lease leader resolution (LeaseCoordinator + GitLeaseStore + live
|
|
30
|
+
coordinator integration).
|
|
31
|
+
3. G3 idempotent ledger + handoff lifecycle + adapter contract.
|
|
32
|
+
4. G3 live-tail buffer + redaction + fenced outbox.
|
|
33
|
+
5. Observability (`/health.multiMachine.syncStatus`) + agent awareness
|
|
34
|
+
(CLAUDE.md generate + migrate).
|
|
35
|
+
6. Telegram + Slack Channel Seamlessness Contract.
|
|
36
|
+
|
|
37
|
+
## Tunability, migration parity, observability
|
|
38
|
+
- All §9 knobs under `.instar/config.json` → `multiMachine`, with startup
|
|
39
|
+
invariant validation (a violating config is rejected, not run silently).
|
|
40
|
+
- Knobs default in code (existing agents get the feature without config bloat);
|
|
41
|
+
server code ships automatically; SQLite stores self-initialize; CLAUDE.md
|
|
42
|
+
awareness is migrated to existing agents.
|
|
43
|
+
- `/health.multiMachine.syncStatus` (leaseHolder, leaseEpoch, holdsLease,
|
|
44
|
+
splitBrainState, awakeMachineCount, protocolVersion) — the Phase-1
|
|
45
|
+
feature-alive surface.
|
|
46
|
+
|
|
47
|
+
## Tests
|
|
48
|
+
~100 new unit tests (real Ed25519, real SQLite): lease CAS/fencing/clock-skew/
|
|
49
|
+
livelock, registry replay/epoch/unknown-key guard, the Phase-0-catching push
|
|
50
|
+
wiring test, message-ledger redelivery/stuck-recovery/dual-medium marker,
|
|
51
|
+
fenced-outbox suppression, live-tail sequence-dedup/gap-discard, redaction,
|
|
52
|
+
HandoffSentinel verify-before-yield (every abort gate), feature-alive syncStatus,
|
|
53
|
+
and Telegram + Slack contract conformance.
|
|
54
|
+
|
|
55
|
+
## Deliberate staging (honest)
|
|
56
|
+
- The **live cross-machine handoff TRANSPORT** (server-to-server live-tail + ack
|
|
57
|
+
over the wire) and the **inbound-dispatch integration** of the ledger/outbox
|
|
58
|
+
are the validation-phase work proven by the real-hardware gate (test-as-self
|
|
59
|
+
over Telegram on two machines). The machinery is built + unit-tested here; the
|
|
60
|
+
wire integration lands with the hardware validation.
|
|
61
|
+
- Onboarding/self-propagation gaps are tracked to ACT-156 (companion spec).
|
|
62
|
+
|
|
63
|
+
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Side-Effects Review — Cross-Machine Seamlessness: G1 fenced-lease integration
|
|
2
|
+
|
|
3
|
+
**Spec:** docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §6 + §8 G1 (converged, approved)
|
|
4
|
+
**Increment:** wire the FencedLease primitive into the live coordinator as the
|
|
5
|
+
authority for awake/standby — closing the Phase-0 split-brain structurally.
|
|
6
|
+
|
|
7
|
+
## What changed
|
|
8
|
+
- `src/core/LeaseCoordinator.ts` (new) — drives FencedLease over a durable store
|
|
9
|
+
(+ optional tunnel): acquisition (bounded-retry CAS + livelock backoff),
|
|
10
|
+
renewal with a medium-agnostic confirmation requirement, the `holdsLease()`
|
|
11
|
+
fencing gate, epoch-advance signal, and unresolvable-split escalation. Tracks
|
|
12
|
+
`selfIssued` (the holder's freshest self-signed lease) so a renewal's new
|
|
13
|
+
expiry is honored without churning git.
|
|
14
|
+
- `src/core/GitLeaseStore.ts` (new) — durable CAS over git: pull-rebase →
|
|
15
|
+
epoch-check → write → push-or-reject-reread (never force-push). `refresh()`
|
|
16
|
+
does a same-epoch durable expiry bump and declines if superseded. Bumps the
|
|
17
|
+
holder's syncSequence/authoredUnderEpoch so the lease write passes peers'
|
|
18
|
+
replay guard.
|
|
19
|
+
- `src/core/GitSync.ts` — added `pullRebase()` (targeted, no auto-commit) for
|
|
20
|
+
the lease CAS. Additive.
|
|
21
|
+
- `src/core/MultiMachineCoordinator.ts` — optional `leaseCoordinator`;
|
|
22
|
+
`shouldSkipProcessing()` returns `!holdsLease()` when attached (the structural
|
|
23
|
+
demotion Phase-0 lacked); `attachLeaseCoordinator` / `initializeLease` /
|
|
24
|
+
`tickLease` / `reconcileRoleToLease`; the monitor tick drives the lease when
|
|
25
|
+
attached (heartbeat retained for liveness display only).
|
|
26
|
+
- `src/commands/server.ts` — constructs the lease crypto (signs with the
|
|
27
|
+
identity key whose pubkey is registered; verifies peers via the SAS-paired
|
|
28
|
+
registry), the GitLeaseStore, and the LeaseCoordinator; attaches + initializes
|
|
29
|
+
it; wires onEpochAdvance → `leaseEpochChange` (→ durable registry push).
|
|
30
|
+
|
|
31
|
+
## Over-block / under-block
|
|
32
|
+
- **Over-block:** `shouldSkipProcessing` now gates on `holdsLease()`. Risk: a
|
|
33
|
+
single git-backed machine that briefly can't read its own lease would skip
|
|
34
|
+
processing. Mitigated — a single machine acquires epoch 1 trivially at
|
|
35
|
+
`initializeLease()` (no peer to contend), and `holdsLease` reads its own
|
|
36
|
+
`selfIssued` lease (in-memory), not requiring git. Non-git / single-machine /
|
|
37
|
+
independent-mode meshes have NO leaseCoordinator → fall back to the existing
|
|
38
|
+
heartbeat path unchanged.
|
|
39
|
+
- **Under-block:** git-only (no tunnel) means lease transfer latency is bounded
|
|
40
|
+
by git cadence, not RTT. Acceptable for the launch increment (Phase 0 proved
|
|
41
|
+
pairing works over git alone). The split-brain SAFETY is preserved via the
|
|
42
|
+
refresh-confirmation self-suspend (a partitioned holder stops within
|
|
43
|
+
leaseTtlMs). The tunnel ACCELERATOR is a tracked follow-on, not a correctness
|
|
44
|
+
gap.
|
|
45
|
+
|
|
46
|
+
## Signal vs authority
|
|
47
|
+
- Detectors emit signals (`checkForUnresolvableSplit`, sync-health); only the
|
|
48
|
+
lease HOLDER mutates authority-bearing state. A non-holder fails its own
|
|
49
|
+
fencing check and self-fences — no cooperative demotion needed.
|
|
50
|
+
|
|
51
|
+
## Interactions
|
|
52
|
+
- The lease epoch advance fires `leaseEpochChange`, which the G2 wireRegistrySync
|
|
53
|
+
already subscribes → the new epoch is pushed durably. Closed loop.
|
|
54
|
+
- `reconcileRoleToLease` updates `_role` + StateManager read-only + heartbeat
|
|
55
|
+
writer to match lease holding — keeps existing role-dependent code paths
|
|
56
|
+
(scheduler `coordinator.isAwake`, etc.) coherent.
|
|
57
|
+
- Heartbeat failover logic is bypassed when a lease is attached (the lease is
|
|
58
|
+
the single authority) — avoids two mechanisms fighting over role.
|
|
59
|
+
|
|
60
|
+
## Rollback cost
|
|
61
|
+
- Low–moderate. The lease is only active when git-backed multi-machine is
|
|
62
|
+
enabled AND construction succeeds (inside the existing try/catch). Reverting
|
|
63
|
+
the server.ts block + the coordinator's lease methods restores the
|
|
64
|
+
heartbeat-only behavior; the new modules become dead code.
|
|
65
|
+
|
|
66
|
+
## Tests
|
|
67
|
+
- `tests/unit/LeaseCoordinator.test.ts` (real Ed25519): acquire, presumed-dead
|
|
68
|
+
takeover, CAS contention yield, tunnel + git-only self-suspend, escalation,
|
|
69
|
+
epoch-advance signal. FencedLease/config/wiring/replay suites still green (57
|
|
70
|
+
total). Coordinator-integration + git-CAS-under-real-contention land in the
|
|
71
|
+
Tier-3 e2e + the real-hardware gate.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Side-Effects Review — Cross-Machine Seamlessness: foundations + G2 auto-sync
|
|
2
|
+
|
|
3
|
+
**Spec:** docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md (converged, approved)
|
|
4
|
+
**Increment:** type/config foundations, the FencedLease primitive (logic), and G2
|
|
5
|
+
automated state sync with the named wiring + pull-side replay/freshness guard.
|
|
6
|
+
|
|
7
|
+
## What changed
|
|
8
|
+
|
|
9
|
+
New, self-contained modules (no behavior wired into existing flows except the two named seams):
|
|
10
|
+
- `src/core/seamlessnessConfig.ts` — resolve + validate the §9 tunable knobs; auto-derives
|
|
11
|
+
`standbyPullIntervalMs = min(failoverThresholdMs/4, leaseTtlMs/2)` to satisfy BOTH cross-knob
|
|
12
|
+
invariants (the bare `/4` the spec first stated violated the `< leaseTtlMs` invariant at default
|
|
13
|
+
ratios — caught by the unit test; spec table corrected to match).
|
|
14
|
+
- `src/core/FencedLease.ts` — pure lease logic (epoch CAS candidate, signing/verify, fencing check
|
|
15
|
+
`holdsValidLease`, `effectiveEpoch = max(tunnel,git)`, tunnel accept guard, presumed-dead
|
|
16
|
+
acquisition, livelock backoff). Transport-agnostic; not yet wired into the coordinator (that is the
|
|
17
|
+
G1 integration step).
|
|
18
|
+
- `src/core/RegistrySyncDebouncer.ts` — debounced, single-writer durable registry push + sync-health
|
|
19
|
+
signal.
|
|
20
|
+
- `src/core/wireRegistrySync.ts` — the NAMED G2 wiring (coordinator roleChange/leaseEpochChange →
|
|
21
|
+
markRegistryDirty), extracted as a testable seam.
|
|
22
|
+
- `src/core/registryReplayGuard.ts` — pure replay/freshness + epoch-floor + unknown-key-first-commit
|
|
23
|
+
validator.
|
|
24
|
+
|
|
25
|
+
Edits to existing files:
|
|
26
|
+
- `src/core/types.ts` — additive optional fields on `MachineRegistryEntry` (syncSequence,
|
|
27
|
+
authoredUnderEpoch, protocolVersion, rejoined), new `LeaseRecord`, optional `lease` on
|
|
28
|
+
`MachineRegistry`, and the seamlessness knobs on `MultiMachineConfig`. All optional → no existing
|
|
29
|
+
caller breaks.
|
|
30
|
+
- `src/core/GitSync.ts` — `sync()` snapshots the registry before pull and calls a new
|
|
31
|
+
`reconcilePulledRegistry()` after a successful pull to scrub stale/unauthorized entries. Additive;
|
|
32
|
+
on any error it warns and continues (never blocks sync).
|
|
33
|
+
- `src/commands/server.ts` — validates the seamlessness config at startup (rejects a violating
|
|
34
|
+
config), constructs `gitSync` for BOTH roles (so a self-electing standby can push), and wires the
|
|
35
|
+
RegistrySyncDebouncer. Stops it on shutdown.
|
|
36
|
+
|
|
37
|
+
## Over-block / under-block
|
|
38
|
+
- **Over-block risk:** startup config validation throws on a violating override. Mitigated: default
|
|
39
|
+
and absent configs resolve to valid values (unit-tested), so only an explicitly bad override fails —
|
|
40
|
+
which is the intended "reject, don't degrade silently" behavior (spec §9).
|
|
41
|
+
- **Under-block risk:** the replay guard only runs when `registryBefore` was readable and the pull
|
|
42
|
+
changed HEAD; a first-ever pull with no prior registry simply trusts the incoming (no local baseline
|
|
43
|
+
to compare). Acceptable — signed-commit verification still applies, and the unknown-key constraint
|
|
44
|
+
still rejects an unknown machine asserting awake.
|
|
45
|
+
|
|
46
|
+
## Level-of-abstraction fit
|
|
47
|
+
- The durable push lives in a debouncer (coarse, registry-only), NOT in the per-tick heartbeat path —
|
|
48
|
+
honoring the ephemeral/durable split so steady-state produces ~0 commits.
|
|
49
|
+
- FencedLease is pure logic with injected crypto; the transport (git/tunnel) is the coordinator's job.
|
|
50
|
+
This keeps the dangerous CAS/fencing logic unit-testable.
|
|
51
|
+
|
|
52
|
+
## Signal vs. authority
|
|
53
|
+
- RegistrySyncDebouncer is single-writer: it only pushes when `isAuthoritative()` (awake / lease
|
|
54
|
+
holder). A standby marking dirty is a no-op push — removes the O(N) thundering-herd.
|
|
55
|
+
- The replay guard and sync-health are SIGNALS (reject an entry / flip health); the authority to act
|
|
56
|
+
on unhealthy sync (self-suspend ingress) belongs to the lease layer, wired in the G1 step.
|
|
57
|
+
|
|
58
|
+
## Interactions
|
|
59
|
+
- `gitSync` now constructed for standby too. It does NOT pull/push at boot unless awake (the boot
|
|
60
|
+
`sync()` stays gated on `isAwake`); construction alone has no side effects beyond signing setup.
|
|
61
|
+
- `reconcilePulledRegistry` writes back a cleaned registry only when something was rejected — normal
|
|
62
|
+
pulls are untouched (no spurious writes).
|
|
63
|
+
- The lease object in the registry is explicitly NOT reconciled by the guard (the FencedLease epoch
|
|
64
|
+
CAS owns it) — avoids two components fighting over the same field.
|
|
65
|
+
|
|
66
|
+
## Rollback cost
|
|
67
|
+
- Low. New modules are unreferenced except the two seams in server.ts/GitSync.ts. Reverting the
|
|
68
|
+
server.ts wiring + the GitSync snapshot/reconcile block restores prior behavior exactly; the new
|
|
69
|
+
files become dead code. No schema/migration shipped in this increment (config knobs are read with
|
|
70
|
+
defaults; migrateConfig lands in P4).
|
|
71
|
+
|
|
72
|
+
## Tests
|
|
73
|
+
- `tests/unit/FencedLease.test.ts` (real Ed25519), `seamlessnessConfig.test.ts`,
|
|
74
|
+
`registry-sync-wiring.test.ts` (incl. the Phase-0-catching "role change triggers a push" test),
|
|
75
|
+
`registryReplayGuard.test.ts`. All green. Tier-2 (real git repo) reconcile + Tier-3 land in P5.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Side-Effects Review — Cross-Machine Seamlessness: Telegram adapter contract impl
|
|
2
|
+
|
|
3
|
+
**Spec:** docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §Channel Seamlessness Contract (converged, approved)
|
|
4
|
+
|
|
5
|
+
## What changed
|
|
6
|
+
- `src/messaging/TelegramAdapter.ts` — implements the Channel Seamlessness
|
|
7
|
+
Contract (the reference adapter):
|
|
8
|
+
- `dedupeKey(rawEvent)` → `telegram:<update_id>` (with a fallback to a
|
|
9
|
+
normalized Message's metadata.update_id).
|
|
10
|
+
- `getIngressPosition()` → `{ platform:'telegram', cursor:lastUpdateId, ... }`.
|
|
11
|
+
- `stopConsuming()` → halts the poll loop, persists the offset, returns the
|
|
12
|
+
DURABLE position (the saved offset, not the in-memory cursor).
|
|
13
|
+
- `resumeConsuming(position)` → restores the offset (never lowers it below
|
|
14
|
+
what we already know, so no replay) and restarts polling; rejects a
|
|
15
|
+
wrong-platform position.
|
|
16
|
+
|
|
17
|
+
## Over-block / under-block
|
|
18
|
+
- All four methods are additive (the interface declares them optional). No
|
|
19
|
+
existing TelegramAdapter behavior changes — start/stop/send/onMessage are
|
|
20
|
+
untouched. `resumeConsuming` uses `Math.max(lastUpdateId, cursor)` so it can
|
|
21
|
+
only advance the offset, never rewind into a replay.
|
|
22
|
+
|
|
23
|
+
## Signal vs authority / interactions
|
|
24
|
+
- These are pure adapter capabilities (where-am-I / stop / resume / identify);
|
|
25
|
+
they carry no authority. The seamless handoff path (a follow-on) calls them;
|
|
26
|
+
on their own they have no runtime effect until invoked.
|
|
27
|
+
- `stopConsuming` reuses the existing `saveOffset()` (atomic-rename), so the
|
|
28
|
+
durable position is consistent with the normal poll-offset persistence.
|
|
29
|
+
|
|
30
|
+
## Rollback cost
|
|
31
|
+
- Minimal — removing the four methods reverts to the prior adapter exactly.
|
|
32
|
+
|
|
33
|
+
## Tests
|
|
34
|
+
- `tests/unit/telegram-seamless-contract.test.ts` (6): all four methods present,
|
|
35
|
+
dedupeKey stability/distinctness + metadata fallback, ingress-position shape,
|
|
36
|
+
stop→durable-position→resume round-trip (no replay), wrong-platform rejection.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Side-Effects Review — Cross-Machine Seamlessness: G3 idempotent ledger + handoff lifecycle + adapter contract
|
|
2
|
+
|
|
3
|
+
**Spec:** docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §8 G3a/G3e + Channel Seamlessness Contract (converged, approved)
|
|
4
|
+
|
|
5
|
+
## What changed
|
|
6
|
+
- `src/messaging/MessageProcessingLedger.ts` (new) — SQLite-backed
|
|
7
|
+
received→processing→reply_committed→cursor_advanced lifecycle keyed by
|
|
8
|
+
dedupeKey. Redelivery of an acted-on event is dropped; cursor advances only on
|
|
9
|
+
durable completion; stuck-`processing` entries are re-runnable; a remote
|
|
10
|
+
reply-committed marker (dual-medium) prevents a failover re-send. Deterministic
|
|
11
|
+
`computeReplyIdempotencyKey(dedupeKey, replyIndex)`. Self-initializing schema,
|
|
12
|
+
per-agent-id isolation, WAL+busy_timeout (the PendingRelayStore pattern).
|
|
13
|
+
- `src/core/HandoffSentinel.ts` (new) — the planned-handoff state machine
|
|
14
|
+
(prepare→tail_synced→ingress_fenced→new_owner_active→old_owner_standby→
|
|
15
|
+
committed). Yields the lease ONLY on a verified ack (echo of tailSeq + ingress
|
|
16
|
+
position + thread-history hash) AND a passing validation; otherwise aborts and
|
|
17
|
+
stays awake. Anti-oscillation floor + a race guard (`inProgress`).
|
|
18
|
+
- `src/core/types.ts` — new `IngressPosition` type and OPTIONAL Channel
|
|
19
|
+
Seamlessness Contract methods on `MessagingAdapter`
|
|
20
|
+
(getIngressPosition/stopConsuming/resumeConsuming/dedupeKey).
|
|
21
|
+
|
|
22
|
+
## Over-block / under-block
|
|
23
|
+
- The contract methods are OPTIONAL, so every existing adapter
|
|
24
|
+
(Telegram/Slack/WhatsApp/iMessage) compiles unchanged — no adapter is forced
|
|
25
|
+
"seamless-ready" before it implements + passes the conformance suite.
|
|
26
|
+
- The ledger's `record` is INSERT OR IGNORE — a benign double-record is a no-op;
|
|
27
|
+
a genuinely new event is `firstSeen:true`. No over-block (it never refuses a
|
|
28
|
+
first-seen event).
|
|
29
|
+
|
|
30
|
+
## Signal vs authority
|
|
31
|
+
- The ledger is a pure substrate (records facts); it carries no authority. The
|
|
32
|
+
fencing epoch is STORED (reply_epoch) but the authority to send is the lease
|
|
33
|
+
layer's (the fencing-gated outbox, next increment).
|
|
34
|
+
- HandoffSentinel encodes the authority rule structurally: no yield without
|
|
35
|
+
verified ack + validation. The validator is a Tier-1 signal; the yield is the
|
|
36
|
+
authority act, gated on it.
|
|
37
|
+
|
|
38
|
+
## Interactions
|
|
39
|
+
- MessageProcessingLedger is standalone (not yet wired into the inbound path —
|
|
40
|
+
that wiring + the fencing-gated outbox is the next G3 increment). No live
|
|
41
|
+
behavior changes from this commit.
|
|
42
|
+
- HandoffSentinel is standalone (constructed + wired in the integration step);
|
|
43
|
+
its ops are injected, so it has no ambient dependencies.
|
|
44
|
+
- `applyRemoteReplyMarker` uses COALESCE so a local commit already present is
|
|
45
|
+
never downgraded — safe under concurrent local+remote marking.
|
|
46
|
+
|
|
47
|
+
## Rollback cost
|
|
48
|
+
- Minimal — both new modules are unreferenced by live code in this commit; the
|
|
49
|
+
types change is purely additive (optional fields). Reverting removes the files
|
|
50
|
+
with no behavioral impact.
|
|
51
|
+
|
|
52
|
+
## Tests
|
|
53
|
+
- `tests/unit/MessageProcessingLedger.test.ts` (real SQLite, 10 tests): redelivery
|
|
54
|
+
dropped, cursor-only-on-completion, idempotent commit, stuck re-run, dual-medium
|
|
55
|
+
marker. `tests/unit/HandoffSentinel.test.ts` (10 tests): every abort gate proven
|
|
56
|
+
to NOT yield. 77 unit tests green overall.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Side-Effects Review — Cross-Machine Seamlessness: live-tail buffer + redaction + fenced outbox
|
|
2
|
+
|
|
3
|
+
**Spec:** docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §8 G3a/G3b/G3c (converged, approved)
|
|
4
|
+
|
|
5
|
+
## What changed (all new, standalone modules)
|
|
6
|
+
- `src/core/LiveTailBuffer.ts` — standby-side persisted tail with strict
|
|
7
|
+
sequence-dedup: applies a flush only at lastAppliedSeq+1, holds out-of-order,
|
|
8
|
+
drops duplicates, and after liveTailOutOfOrderTimeoutMs declares an unfillable
|
|
9
|
+
gap (discards held, proceeds) — bounding the holdout and preventing context
|
|
10
|
+
corruption. Per-topic byte cap (drop-oldest).
|
|
11
|
+
- `src/core/liveTailRedaction.ts` — versioned (REDACTION_CATEGORY_VERSION) named
|
|
12
|
+
enum of credential categories + redactForLiveTail(); scrubs bearer tokens,
|
|
13
|
+
private-key blocks, AWS keys, JWT-like, api-keys, secret assignments before any
|
|
14
|
+
tail content leaves the machine.
|
|
15
|
+
- `src/messaging/FencedOutbox.ts` — structural no-duplicate-reply at the send
|
|
16
|
+
path: sends only while holding the lease at the stamped epoch; suppresses a
|
|
17
|
+
fenced (no-lease / stale-epoch) machine's reply; idempotent via the ledger's
|
|
18
|
+
acted-on check + deterministic idempotency key.
|
|
19
|
+
|
|
20
|
+
## Over-block / under-block
|
|
21
|
+
- FencedOutbox SUPPRESSES on no-lease/stale-epoch — intended over-block (a fenced
|
|
22
|
+
machine must not send). Risk of under-send if `holdsLease()` is wrongly false;
|
|
23
|
+
mitigated because a single machine trivially holds its lease, and the stamped
|
|
24
|
+
epoch equals the current epoch in the normal (non-handoff) case.
|
|
25
|
+
- Redaction is conservative/high-precision; over-redaction (a false positive) is
|
|
26
|
+
acceptable per spec ("false positives cheaper than a leak"). Ordinary prose is
|
|
27
|
+
left untouched (tested).
|
|
28
|
+
|
|
29
|
+
## Signal vs authority
|
|
30
|
+
- FencedOutbox consults the lease authority (holdsLease/currentEpoch injected) —
|
|
31
|
+
it does not itself decide authority, it enforces it at the send boundary.
|
|
32
|
+
- LiveTailBuffer is a pure data structure (no authority); it only guarantees
|
|
33
|
+
context integrity on the receiving side.
|
|
34
|
+
|
|
35
|
+
## Interactions
|
|
36
|
+
- All three are standalone in this commit (not yet wired into the live message
|
|
37
|
+
path — that integration is the next increment). No runtime behavior changes.
|
|
38
|
+
- FencedOutbox depends on MessageProcessingLedger (already shipped) — commitReply
|
|
39
|
+
is idempotent, so a double-send attempt is safe.
|
|
40
|
+
- Redaction is designed to run before the (encrypted) transport; carry-by-
|
|
41
|
+
reference for large tool output (a follow-on in the transport wiring) reduces
|
|
42
|
+
the raw sensitive surface structurally beyond pattern-matching.
|
|
43
|
+
|
|
44
|
+
## Rollback cost
|
|
45
|
+
- Minimal — unreferenced by live code. Reverting removes the files.
|
|
46
|
+
|
|
47
|
+
## Tests
|
|
48
|
+
- `tests/unit/LiveTailBuffer.test.ts` (9): order, dup-drop, hold+drain,
|
|
49
|
+
gap-discard, byte cap, redaction categories. `tests/unit/FencedOutbox.test.ts`
|
|
50
|
+
(5): send, already-replied, fenced-no-lease, fenced-stale-epoch, send-failed.
|
|
51
|
+
91 unit tests green overall.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Side-Effects Review — Cross-Machine Seamlessness: observability + agent awareness + migration parity
|
|
2
|
+
|
|
3
|
+
**Spec:** docs/specs/CROSS-MACHINE-SEAMLESSNESS-SPEC.md §11 (converged, approved)
|
|
4
|
+
|
|
5
|
+
## What changed
|
|
6
|
+
- `src/core/MultiMachineCoordinator.ts` — `getSyncStatus()`: the
|
|
7
|
+
/health.multiMachine.syncStatus surface (leaseHolder, leaseEpoch, holdsLease,
|
|
8
|
+
splitBrainState, awakeMachineCount, protocolVersion). Always returns valid
|
|
9
|
+
fields (never null/throws) — the Phase-1 "feature is alive" surface.
|
|
10
|
+
- `src/server/routes.ts` — RouteContext gains `coordinator`; authed `/health`
|
|
11
|
+
now includes `multiMachine: { enabled, syncStatus }`.
|
|
12
|
+
- `src/server/AgentServer.ts` — threads `options.coordinator` into the route
|
|
13
|
+
context.
|
|
14
|
+
- `src/scaffold/templates.ts` (`generateClaudeMd`) + `src/core/PostUpdateMigrator.ts`
|
|
15
|
+
(`migrateClaudeMd`) — a concise Cross-Machine Seamlessness awareness section
|
|
16
|
+
(one-agent-many-machines, no double-reply, compaction-pause handoff bar, honest
|
|
17
|
+
machine-provenance, where to read sync status, how to read a split-brain
|
|
18
|
+
escalation). New agents get it via generate; existing agents via the
|
|
19
|
+
content-sniffed migration.
|
|
20
|
+
|
|
21
|
+
## Migration parity decision (explicit)
|
|
22
|
+
- The `multiMachine` seamlessness KNOBS default safely in code
|
|
23
|
+
(`resolveSeamlessnessConfig` applies defaults for any absent key), so every
|
|
24
|
+
existing agent's server resolves valid values without a config write. I
|
|
25
|
+
DELIBERATELY did not write ~13 knobs into every agent's config.json — that is
|
|
26
|
+
noise, and functional migration parity is already achieved by the code
|
|
27
|
+
defaults + the server code shipping + the SQLite stores self-initializing +
|
|
28
|
+
the CLAUDE.md awareness migration. The dials remain user-settable under
|
|
29
|
+
`multiMachine` (documented in the awareness section). A literal config-knob
|
|
30
|
+
migration is a trivial follow-on if a reviewer wants it.
|
|
31
|
+
|
|
32
|
+
## Over-block / under-block
|
|
33
|
+
- `getSyncStatus` is read-only and defensive (try/catch around the registry
|
|
34
|
+
read → awakeMachineCount 0 on failure). It cannot block anything.
|
|
35
|
+
- `/health.multiMachine` is only in the AUTHED branch (unchanged auth posture);
|
|
36
|
+
the unauthenticated basic health is untouched.
|
|
37
|
+
|
|
38
|
+
## Signal vs authority / interactions
|
|
39
|
+
- Pure observability — no authority, no mutation. RouteContext.coordinator is
|
|
40
|
+
`| null`; single-machine installs report `{ enabled:false, syncStatus:null }`
|
|
41
|
+
or the trivially-held lease. Existing route tests still pass (tolerant
|
|
42
|
+
context builders).
|
|
43
|
+
- protocolVersion is reported (for partial-migration visibility); its
|
|
44
|
+
enforcement point (refusing to hand the awake lease to an old-version machine)
|
|
45
|
+
lands with the live two-machine handoff transport.
|
|
46
|
+
|
|
47
|
+
## Rollback cost
|
|
48
|
+
- Low. Removing the /health block + getSyncStatus + the template sections
|
|
49
|
+
reverts cleanly; the RouteContext field is additive (`| null`).
|
|
50
|
+
|
|
51
|
+
## Tests
|
|
52
|
+
- `tests/unit/multimachine-syncstatus.test.ts` — the feature-alive surface
|
|
53
|
+
returns valid non-null fields on a single-machine install and never throws.
|
|
54
|
+
119 unit tests green overall.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Side-Effects Review — dispatch (guidance-out) logic (Phase 1, increment 7)
|
|
2
|
+
|
|
3
|
+
**Slug:** `feedback-factory-dispatch`
|
|
4
|
+
**Date:** `2026-05-27`
|
|
5
|
+
**Author:** Echo (autonomous)
|
|
6
|
+
**Spec:** `docs/specs/feedback-factory-migration.md` (converged v2, approved by Justin 2026-05-26)
|
|
7
|
+
**Scope:** The pure logic of the dispatch endpoint (the channel that sends guidance back out to agents) — type/priority vocab, semver version-compat filtering, and the create-dedup title normalization. NOT the HTTP wiring/storage (app-placement decision, blocked).
|
|
8
|
+
|
|
9
|
+
## Summary of the change
|
|
10
|
+
|
|
11
|
+
Ports the pure logic of `the-portal/pages/api/instar/dispatches/index.ts` into `src/feedback-factory/dispatch/dispatch.ts`: the dispatch `DISPATCH_TYPES`/`DISPATCH_PRIORITIES` vocab + validators, `parseVersion`/`isVersionGte`/`isVersionLte` (the semver comparison), `filterDispatchesForVersion` (an agent only receives a dispatch whose `[minVersion, maxVersion]` window includes its version), and `normalizeDispatchTitle` (trim + 500-char cap, used for create-dedup). **Not wired into any route yet** — no behavioral change.
|
|
12
|
+
|
|
13
|
+
## Equivalence verification
|
|
14
|
+
|
|
15
|
+
Reference is TypeScript, so equivalence is by faithful transcription + exhaustive both-sides-of-boundary unit tests (9): semver compare at equal/greater/lesser across all three components; the version-compat filter with min-only / max-only / both-bounds / unbounded, boundaries inclusive; unparseable version → `[0,0,0]`; title trim + 500 cap. The comparison loop, the inclusive-equal semantics, and the filter predicate are copied verbatim from the reference.
|
|
16
|
+
|
|
17
|
+
## Seven-dimension review
|
|
18
|
+
|
|
19
|
+
1. **Over/under-reach** — Pure functions, no I/O, no state, not imported by any runtime path.
|
|
20
|
+
2. **Level-of-abstraction fit** — `src/feedback-factory/dispatch/` — the dispatch layer. HTTP/storage excluded (the blocked app-placement decision); this is the reusable core.
|
|
21
|
+
3. **Signal vs Authority** — N/A; pure filters/validators.
|
|
22
|
+
4. **Interactions** — None. New isolated module; nothing imports it yet.
|
|
23
|
+
5. **Rollback cost** — Trivial: delete the module + tests.
|
|
24
|
+
6. **Migration parity** — N/A. New internal library code; no agent-installed file touched.
|
|
25
|
+
7. **Failure modes** — (a) Semver-compare divergence (esp. inclusive-equal + multi-component ordering) → covered by both-direction tests across all components. (b) Unparseable version handling → tested ([0,0,0] fallback matches the reference, so a garbage version is treated as 0.0.0). (c) Title-dedup divergence → trim+slice(500) transcribed + tested.
|
|
26
|
+
|
|
27
|
+
## Tests
|
|
28
|
+
|
|
29
|
+
- Tier-1 unit (CI): `tests/unit/feedback-factory/dispatch.test.ts` — 9 tests (vocab, parseVersion, gte/lte, version-compat filter boundaries, title normalization).
|
|
30
|
+
- No cross-runtime parity harness (reference is TS; equivalence by transcription + boundary tests).
|
|
31
|
+
- No integration/E2E this increment: HTTP wiring/storage is the blocked app-placement decision; this logic attaches to a route when that's decided. Reasoned, documented.
|